Compare commits

...

317 Commits

Author SHA1 Message Date
Eric Bailey 15e51c09bc Add support for autofill of one-time-code for email verification 2024-09-06 10:42:40 -05:00
Hailey a1969faf8e [Video] Fix alt text dialog on iOS and Android (#5177)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2024-09-06 08:26:37 -07:00
Eric Bailey 64b50ba69a Go full width on native and mobile web (#5184) 2024-09-06 17:19:29 +02:00
Eric Bailey b90cd68359 Remove record-with-media side-by-side layout (#5182) 2024-09-06 09:58:27 -05:00
Samuel Newman 55468595d0 [Video] Error banner improvements (#5163) 2024-09-05 21:25:56 -07:00
Hailey 18133483fe [Video] Bump expo-video (#5173) 2024-09-05 15:13:53 -07:00
Hailey 6eabedd037 [Video] More adjustments for loading state jank (#5171) 2024-09-05 14:54:09 -07:00
Hailey 7eb4cd89c5 Downgrade MMKV to 2.x (#5170) 2024-09-05 12:55:10 -07:00
Eric Bailey 8a66883df8 Add MMKV interface (#5169) 2024-09-05 12:31:24 -07:00
Eric Bailey 2265fedd2a Constrain image heights in feeds and threads (#5129)
* Limit height of images within posts

* Add some future-proofness

* Comments, improve a11y

* Adjust ALT, add crop icon

* Fix disableCrop in record-with-media posts

* Clean up aspect ratios, handle very tall images

* Handle record-with-media separately, clarify intent using enums

* Adjust spacing

* Adjust rwm embed image size on mobile

* Only do reduced layout if images embed

* Adjust gap in small embed variant

* Clean up grid layout

* Hide badge on small variant with one image

* Remove crop icon from image grid, leave on single image

* Fix sizing in Firefox

* Fix fullBleed variant
2024-09-05 13:45:13 -05:00
Samuel Newman 117926357d [Video] require email to post videos (#5152)
Co-authored-by: Hailey <me@haileyok.com>
2024-09-05 11:36:19 -07:00
Hailey 91c3dbd812 [Video] Fix CI (#5168) 2024-09-05 10:21:52 -07:00
Marco Buono 824206b95f Load number formatting data when activating locales (#5128) 2024-09-05 10:34:24 -05:00
Hailey 93c171b403 [Video] Use expo-video from fork (#5159) 2024-09-05 08:27:28 -07:00
Marco Buono 6d8ed5c3c8 Add quick access to quote action on long press (#5123) 2024-09-05 10:14:04 -05:00
Samuel Newman 4e6b6740f7 [Video] Enter/exit animations for video in composer (#5164)
* enter/exit animations for video in composer

* use zoom out animation

* unify margin between different steps

* skip animation when posting
2024-09-05 16:07:06 +01:00
Samuel Newman 428607d9a3 [Video] throw HLS errors to be caught by error boundary (#5166)
* throw HLS errors to be caught by error boundary

* wording tweak

* do the same on native

* fix type error
2024-09-05 16:03:00 +01:00
Samuel Newman 60b74f7ab8 [Video] Disable autoplay option (preview + web player) (#5167)
* rename setting

* preview (web)

* preview (native)

* improve autoplay disabled behaviour on web
2024-09-05 15:56:10 +01:00
gabrielsiilva d846f5bbf0 fix on 'reposted by you' translation to ptbr (#5146) 2024-09-04 21:00:07 -07:00
Hailey 2556698427 [Video] Add loading state to player (#5149) 2024-09-04 16:46:01 -07:00
Eric Bailey 76f493c279 Ensure profile labels can be appealed separately from account labels (#5154) 2024-09-04 18:34:19 -05:00
Eric Bailey 4d97a2aa16 Add misleading report type to posts (#5150)
* Add misleading report type to posts

* Update copy

* Update copy
2024-09-04 18:22:57 -05:00
Hailey 86de0dda02 Tweak animation to not roll 0 -> 1, overflow hidden (#5148) 2024-09-04 12:36:20 -07:00
Hailey 0ef17a464d Use new player icon for external video embeds (#5147) 2024-09-04 12:20:21 -07:00
Samuel Newman 5dcb52015c add video to embed (#5145) 2024-09-04 19:57:16 +01:00
Samuel Newman fcf27f0512 [Video] content fit cover on native (#5140) 2024-09-04 19:56:02 +01:00
Samuel Newman e8eaf2f4a7 allow only posting video (#5142) 2024-09-04 19:42:28 +01:00
Marco Buono c36c47d49a Add slight spacing between Post and CW button (#5125) 2024-09-04 11:04:08 -07:00
Hailey 6382a91fb0 [Video] Use same play button for gifs and videos (#5144) 2024-09-04 10:59:06 -07:00
Samuel Newman 5f5c14d044 Replace ImageHorzList 🤮 with MediaPreview (#5143) 2024-09-04 10:52:41 -07:00
Frudrax Cheng 82ca0b16b6 Fix a missing curly brace in pt-BR (#5130)
thanks!
2024-09-04 10:50:54 -07:00
Hailey 12b4a250d2 [Video] withRepeat for spinner (#5141) 2024-09-04 09:39:34 -07:00
Hailey 45bb2477d8 [Video] Show better progress (#5133) 2024-09-04 09:17:14 -07:00
Hailey d94ff2695d [Video] Throw error when playback fails (#5132) 2024-09-04 08:06:58 -07:00
Hailey dee28f378a [Video] Only allow one VideoView to be active at a time, regardless of source (#5131) 2024-09-04 08:06:45 -07:00
Hailey 21e48bb2d8 [Video] Tweak playback handling (#5127) 2024-09-04 08:00:53 -07:00
Samuel Newman 515f87ed24 fail video if cannot load preview (#5138) 2024-09-04 15:56:29 +01:00
Samuel Newman 3eef62d995 log errors (#5139) 2024-09-04 15:29:20 +01:00
dan e2a244b998 Disable in-thread deduping for reposted replies (#5135) 2024-09-04 15:42:22 +02:00
dan 8860890a85 Don't log extra background events (#5134) 2024-09-04 15:41:42 +02:00
Samuel Newman 39f74ced5c close keyboard before opening modal (#5124) 2024-09-03 23:13:25 +01:00
Samuel Newman 3ee5ef32d9 [Video] Error handling in composer, fix auto-send (#5122)
* tweak

* error state for upload toolbar

* catch errors in upload status query

* stop query on error

---------

Co-authored-by: Hailey <me@haileyok.com>
2024-09-03 22:49:19 +01:00
Samuel Newman 0bd0146efb fix mime checks (#5118) 2024-09-03 20:55:10 +01:00
Fabio Nobre ea4d8bc1ab Updating some messages in the pt-BR locale file - [2] (#5099)
Co-authored-by: Gleydson Rodrigues <gleydsonsr@gmail.com>
Co-authored-by: Hailey <me@haileyok.com>
2024-09-03 12:14:38 -07:00
Eric Bailey ddda612c11 Fix border on some listing screens (#5115)
* Fix border on PostQuotes, add debug atom

* Add util_screen_outer, apply to h_full_vh here needed
2024-09-03 13:50:48 -05:00
Samuel Newman 5c2e9f09bf reset playback rate on exit fullscreen (#5116) 2024-09-03 19:46:37 +01:00
Hailey 7f26ad508e [Video] Toggle controls when entering fullscreen (#5113) 2024-09-03 11:04:46 -07:00
Hailey 5d300bec90 Adjust render properties for Feed List (#5080) 2024-09-03 10:56:00 -07:00
Hailey 0014d4363f Only animate the like icon when from an actual toggle (#5096) 2024-09-03 10:55:53 -07:00
Samuel Newman bd42f770b8 show video in quote (#5093) 2024-09-03 18:33:16 +01:00
Hailey dde72b48e1 [Video] Manage foreground/background playback on the native side (#5104) 2024-09-03 08:41:14 -07:00
Hailey 05e61346b8 [Video] Say No to Audio (Disable expo-video AudioSession management) (#5101) 2024-09-03 08:33:21 -07:00
Samuel Newman 0e1de19903 [Video] Upload errors and UI improvements (#5092)
* surface errors in UI

* style progress indicator

* remove job status progress

* rm log

* fix webm ext
2024-09-03 15:09:09 +01:00
Samuel Newman f9d736653c [Video] Hide mouse when inactive (#5094) 2024-09-03 11:51:16 +01:00
Samuel Newman 0469ca6cb4 [Embeds] Format big numbers (#5087) 2024-09-02 13:04:26 -07:00
Fabio Nobre f3f7dfc3e6 Add some Portuguese (PT-BR) translation (#5057) 2024-09-02 04:09:17 -07:00
Hailey 4abcd65ccf More tweaks to animation (#5082) 2024-09-02 03:15:31 -07:00
Mary 05ac76fc89 Don't eagerly load all JS assets (#3929)
Co-authored-by: Hailey <me@haileyok.com>
2024-09-02 01:59:04 -07:00
Hailey 1225e84485 Improve animations for like button (#5074) 2024-09-02 01:37:24 -07:00
Samuel Newman eb868a042a [Video] Misc player style tweaks (#5064)
* use actual black rather than theme black

* adjust time/mute indicators
2024-09-02 09:33:46 +01:00
Samuel Newman 17d82a64a6 [Video] Remove hack from scrubber (#5063)
* remove mouseleave, add pointer cancel

* don't show time indicator if duration is 0
2024-09-02 09:32:51 +01:00
dan bf15fad240 Mark unhydrated replies as orphaned (#5048) 2024-08-31 13:14:53 -07:00
Hailey 2df22d8674 Add some Portuguese translation (#5052) 2024-08-31 12:52:18 -07:00
dan bff6aedecf Show some known OP replies in Following (#5049)
* Show known OP replies in Following

* Fiter by >0 parent likes
2024-08-31 03:54:14 +01:00
Eric Bailey 3d992f6bb5 Fix provider order (#5046) 2024-08-31 00:26:03 +01:00
dan ed232e69f7 Animate the like button (#5033)
* Animate the like button

* Respect reduced motion

* Move like count into animated component

* Animate text

* Fix layout on Android

* Animate text backwards too

* Fix bad copypasta

* Reflect nonlocal updates to animated values
2024-08-30 23:16:11 +01:00
Samuel Newman c41f372b3c [Video] Split 'videos' gate in two (#5037) 2024-08-30 12:53:27 -07:00
Samuel Newman ab260c3599 [Video] Minor player tweaks (#5044) 2024-08-30 12:48:23 -07:00
Samuel Newman 8647c8e9f5 [Videos] avoid using fetch for blob handling where possible (#5041)
* avoid using fetch where possible

* whoopsie wrong branch

* more import fixes
2024-08-30 19:05:38 +01:00
Samuel Newman c70ec1ce1a [Video] Captions and alt text (#5009)
* video settings modal in composer

* show done button on web

* rm download options

* fix logic for showing settings button

* add language picker (wip)

* subtitle list with language select

* send captions & alt text with video when posting

* style "ensure you have selected a language" text

* include aspect ratio with video

* filter out captions where the lang is not set

* rm log

* fix label and add hint

* minor scrubber fix
2024-08-30 18:45:49 +01:00
Samuel Newman e7954e590b [Videos] Fix uploads (#5042)
* fix pds url

* fix service auth exp

* whoopsie wrong branch
2024-08-30 18:44:00 +01:00
Eric Bailey c60e8d0772 Composer blocks (#5040)
* Move i18n provider up the stack

* Protect composer opening for a blocked post

* Protect ctrls from interacting with blocked user
2024-08-30 12:26:40 -05:00
dan dbbbba1d32 [Experiment] Suggest profiles in profile (#5030)
* Rename variable to disambiguate with parent scope

* More variables where they are used

* Inline variables

* Add suggestions in profile

* Gate it

* rm space

* Remove header suggestions under gate
2024-08-30 16:54:55 +01:00
Igor Adrov 46b7193a2b Hide back button in my profile (#4991) 2024-08-30 10:49:32 -05:00
Minseo Lee 2ef2ae1c6c Improve localization (#5008)
* Update QrCode.tsx

* Update generate-starterpack.ts
2024-08-30 09:59:49 -05:00
Samuel Newman 05b73d4d1e extend token lifetime to 30 minutes (#5019) 2024-08-30 08:32:18 +01:00
Hailey 8cddce5f30 Tweak rendering of top headers so they always appear even during load (#4982)
Co-authored-by: Eric Bailey <git@esb.lol>
2024-08-30 00:20:46 -07:00
Eric Bailey 8651f31ebb Localize dates, counts (#5027)
* refactor: consistent localized formatting

* refactor: localized date time

* refactor: localize relative time with strings

* chore: fix typo from copy-paste

* Clean up useTimeAgo

* Remove old ago

* Const

* Reuse

* Prettier

---------

Co-authored-by: Mary <git@mary.my.id>
2024-08-29 19:22:53 -05:00
Hailey d5a7618374 Force at least one item to always be initially rendered (#5029) 2024-08-29 17:20:07 -07:00
Hailey 497aacf113 fix hashtag screen side borders (#4983) 2024-08-29 15:55:17 -07:00
Hailey 69e896c221 [Video] Properly get the service auth aud from the session (#5025) 2024-08-29 13:44:47 -07:00
Hailey 91fe41670f tweak (#5024) 2024-08-29 21:44:06 +01:00
dan 0a75969475 Fix language filtering for feeds (#5026)
* Fix language filtering for feeds

* Simplify logic
2024-08-29 20:22:18 +01:00
dan e51eb391df Enable show_follow_back_label_v2 (#5022) 2024-08-29 20:09:11 +01:00
dan 9ebc2785e4 Remove logging of following prefs (#5021) 2024-08-29 20:08:48 +01:00
dan faf66f3178 Remove new_user_guided_tour and tour code (#5023) 2024-08-29 18:57:36 +01:00
Hailey 69053b1b0a [Video] Remove old env var (#5018) 2024-08-29 09:40:17 -07:00
Hailey e33b88ed7d Profile screen performance tweak - Adjust initial num to render based on header height (#5005) 2024-08-29 09:09:24 -07:00
Samuel Newman ea5ab99399 [Video] Make compress/upload cancelable (#4996)
* add abort controller to video upload system

* rm log

* rm log 2
2024-08-29 17:00:12 +01:00
Samuel Newman 551c4a4f32 [Video] Add uploaded video to post (#4884)
* video uploads!

* use video upload lexicons

* add missing postgate

* remove references to prerelease package

* fix scrubber showing a "0"

* Delete types.ts

* rm logs

* rm upload header

---------

Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
2024-08-29 16:34:41 +01:00
Hailey d52d29621e [Video] Add timeRemainingChange event to player in expo-video (#5013) 2024-08-29 08:28:41 -07:00
Samuel Newman d92731b1eb [Video] Lexicon implementation (#4881)
* implement AppBskyEmbedVideo lexicon in player

* add alt to native player

* add prerelease package

* update prerelease

* add video embed view manually from record

* fix type error on example video

* black bg + use aspect ratio on web

* add video to feeds

* fix video overflowing aspect ratio

* remove prerelease package

---------

Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
2024-08-29 15:58:22 +01:00
Eric Bailey b136c44287 Hide quote counts for quotegated posts (#5011) 2024-08-28 16:57:18 -05:00
Hailey 16d556c3c9 Ensure captcha verification code gets submitted in signup request (#5010)
Co-authored-by: Eric Bailey <git@esb.lol>
2024-08-28 11:56:04 -07:00
Samuel Newman 94d2180aaa clean up languages (#5007) 2024-08-28 16:58:19 +01:00
Hailey 5ae0d40a14 [Video] 🫧 Move logic around by platform (#5003) 2024-08-28 08:46:47 -07:00
Hailey 9aa2b2d14e bump 1.91.0 (#5002) 2024-08-28 12:23:30 +01:00
Samuel Newman b69c40da33 add indicator of time remaining (#5000) 2024-08-27 22:15:59 +01:00
Samuel Newman 9b534b968d [Video] add scrubber to the web player (#4943) 2024-08-26 22:28:45 +01:00
Paul Frazee def9dda29c Release 1.90 prep (#4988)
* Stop creating a mod-authority in e2e due to upstream conflict

* Dont require 3 interests when none come back

* Fix e2e login

* intl extract
2024-08-23 14:43:21 -07:00
Eric Bailey fc5cc189b5 Use moderatePost_wrapped for post embeds (#4981)
* Use moderatePost_wrapped

* Add lint rule
2024-08-23 13:55:18 -07:00
kodebanget b5ea87c981 Update Indonesian translation (#4875)
Co-authored-by: Indonesian <github-actions@github.com>
2024-08-23 13:45:41 -07:00
surfdude29 d646d590cc Update French localization (#4823) 2024-08-23 13:45:22 -07:00
Takayuki KUSANO 1d5e341ada Update Japanese translation (#4824)
* Updated translation

* Update translation

* Update translation

* Update translation

* Only show replies in Following if following all involved actors (#4869)

* Only show replies in Following for followed root and grandparent

* Remove now-unnecessary check

* Simplify condition

* Respect labels on feeds and lists (#4818)

* Prep

* Pass in optional moderation to FeedCard

* Compute moderation decision, filter contentList contexts, pass into card

* Let's go a different route

* Filter from within search queries

* Use same search query for starter packs

* Filter lists from profile tabs

* Cleanup

* Filter from profile feeds

* Moderate post embeds

* Memoize

* Use ScreenHider on lists

* Hide both list types

* Fix crash on iOS in screen hider, fix lineheight

* Memoize renderItem

* Reuse objects to prevent re-renders

* tweak list header (#4870)

Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>

* bskyweb: optional basic auth password middleware (#4759)

* Update translation

* Revert "Update translation"

This reverts commit 3a7b74f47b.

* Revert "bskyweb: optional basic auth password middleware (#4759)"

This reverts commit bc3a27d40f.

* Revert "tweak list header (#4870)"

This reverts commit 34e7e5cba2.

* Revert "Respect labels on feeds and lists (#4818)"

This reverts commit 9ec6fde288.

* Revert "Only show replies in Following if following all involved actors (#4869)"

This reverts commit e2cc4bb4af.

* Update translation

* Update translation

* Update translation

* Updated translation

* Update translation

* Update translation

* Unified existing translations of "hidden" and "community".

* Update translation

* Update translation

* Update translation

---------

Co-authored-by: dan <dan.abramov@gmail.com>
Co-authored-by: Eric Bailey <git@esb.lol>
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
Co-authored-by: bnewbold <bnewbold@robocracy.org>
2024-08-23 13:45:03 -07:00
Minseo Lee 632f71acc9 Update Korean localization (#4826)
* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po
2024-08-23 13:44:29 -07:00
Ivan Beà 80320f9d7d Update catalan localization (#4851)
* Update catalan localisation

More lines translated, It will be so kind of you to check it @jordimas @darccio @surfdude29

Thanks

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

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

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

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

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

Co-authored-by: GSMT <samaritanojr006@gmail.com>

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

Co-authored-by: GSMT <samaritanojr006@gmail.com>

* Update messages.po

apply @jordimas corrections

* Update messages.po

change traves per través

---------

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
Co-authored-by: GSMT <samaritanojr006@gmail.com>
2024-08-23 13:44:08 -07:00
Frudrax Cheng 3f98747925 Update Chinese Localization (#4947)
* CN: Update translates

* CN: Remove superseded strings

* TW: Update and Clean

* Both: Update translates

* CN: Update translates

* CN: Remove superseded strings

* CN: Update translates

* CN: Remove superseded strings

* TW: Run intl:extract

* CN: Update translates

* TW: Update and clean

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

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

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

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

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

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

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

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

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

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

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

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

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

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

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

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

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

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

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

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

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

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

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

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

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

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

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

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

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

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

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

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

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

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

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

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

* Both: Run intl:extract

* CN: Update translates

* CN: Update translates

* Both: Run intl:extract

* CN: fix typo

* CN: fix typo#2

* CN: Update translates

* CN: Update translates#2

* TW: Update translates

* Both: Remove superseded strings

* TW: fix typo

---------

Co-authored-by: Kuwa Lee <kuwalee1069@gmail.com>
Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>
2024-08-23 13:42:59 -07:00
Hailey fa12bf5d87 Revert "Make settings account buttons a little nicer" (#4987) 2024-08-23 13:28:56 -07:00
Hailey 1f657b3ac5 fix findAll* type in post-thread (#4986) 2024-08-23 13:20:05 -07:00
Eric Bailey 425dd5f27f Optimistic hidden replies (#4977) 2024-08-23 12:35:48 -07:00
Eric Bailey 5ec8761b29 Make settings account buttons a little nicer (#4980)
* Replace account dropdown with Menu

* Replace card row and add interaction state

* Remove testID copy pasta

* Sanitize handle

* Remove hover from row
2024-08-22 20:09:55 -05:00
Eric Bailey 990bf306c5 Use RichText for sp description (#4979)
* Use RichText for sp description

* `isRecord` above

---------

Co-authored-by: Hailey <me@haileyok.com>
2024-08-22 17:37:15 -05:00
dan b8dbb71781 Fix fixed footer experiment (#4969)
* Split minimal shell mode into headerMode and footerMode

For now, we'll always write them in sync. When we read them, we'll use headerMode as source of truth. This will let us keep footerMode independent in a future commit.

* Remove fixed_bottom_bar special cases during calculation

This isn't the right time to determine special behavior. Instead we'll adjust footerMode itself conditionally on the gate.

* Copy-paste setMode into MainScrollProvider

This lets us fork the implementation later just for this case.

* Gate footer adjustment in MainScrollProvider

This is the final piece. Normal calls to setMode() keep setting both header and footer, but MainScrollProvider adjusts the footer conditionally.
2024-08-22 23:27:33 +01:00
Hailey 2ae3ffcf78 have mock server run with development node env (#4976) 2024-08-22 14:51:35 -07:00
dan 27bb383268 Submit fix (#4978)
* Fix submit logic

* Fix type

* Align submit task creation 1:1 with callsites

* blegh. `useThrottledValue`

* make `useThrottledValue`'s time required

---------

Co-authored-by: Hailey <me@haileyok.com>
2024-08-22 22:43:23 +01:00
Hailey df5bf28e61 update usePostThreadQuery to check quote query data (#4975)
* update `usePostThreadQuery` to check quote query data

* search notifs before quotes

* oops
2024-08-22 19:11:51 +01:00
Eric Bailey 92989282ae Fetch it (#4974) 2024-08-22 10:27:34 -07:00
Hailey 9f1c411360 add quoteCount to view creators (#4972) 2024-08-22 10:26:49 -07:00
Hailey 85d70fe3ef tweak rqkey and cache search for useAllListMembersQuery (#4971) 2024-08-22 09:32:49 -07:00
Eric Bailey d5c78b9183 Prep threadgate shadow hack (#4970)
Co-authored-by: Hailey <me@haileyok.com>
2024-08-21 20:16:03 -07:00
Hailey 61f0be705d Change size (#4957) 2024-08-21 19:35:34 -07:00
Eric Bailey 6616a6467e Detached QPs and hidden replies (#4878)
Co-authored-by: Hailey <me@haileyok.com>
2024-08-21 19:20:45 -07:00
Samuel Newman 56ab5e177f Show quote posts (#4865)
* show quote posts

* fix filter

* fix keyExtractor

* move likedby and repostedby to new file structure

* use modern list component

* remove relative imports

* update quotes count after quoting

* call `onPost` after updating quote count

* Revert "update quotes count after quoting"

This reverts commit 1f1887730a.

* implement

* update like count in quotes list

* only add `onPostReply` where needed

* Filter quotes with detached embeds

* Bump SDK

* Don't show error for no results

---------

Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
Co-authored-by: Hailey <me@haileyok.com>
Co-authored-by: Eric Bailey <git@esb.lol>
2024-08-21 15:26:25 -05:00
Eric Bailey ddb0b80017 Fix hidden list for author (#4968) 2024-08-21 13:11:47 -07:00
Eric Bailey 5463e9718f Update joined stat (#4967) 2024-08-21 10:17:23 -07:00
Hailey 723896a45f Add list hidden screen (#4958)
Co-authored-by: Hailey <me@haileyok.com>
Co-authored-by: Eric Bailey <git@esb.lol>
2024-08-20 15:43:40 -07:00
Eric Bailey e54298ec2c Expose more methods, support disabled items (#4954) 2024-08-19 12:21:29 -07:00
Eric Bailey f235be9819 Expose more props from button (#4953) 2024-08-19 13:27:04 -05:00
Eric Bailey 3976d6738b Fix orphaned feed slices, handle blocks (#4944)
* Fix orphaned feed slices, handle blocks

* Revert to filerting out orphan threads

* Support NotFoundPost views too

* Just kidding, use ReplyRef.root as source of grandparent data

* Fixes
2024-08-19 11:20:42 -05:00
Hailey 2939ee7df7 Tweak expo-modules-core hack patch (#4955) 2024-08-18 13:24:41 -07:00
dan 40ab67fc4b [Experiment] Always show bottom bar (#4946) 2024-08-16 20:06:55 +01:00
Hailey a5af24b53b Revert "[Video] Download videos" (#4945) 2024-08-15 16:29:16 -07:00
dan b6e515c664 Move global "Sign out" out of the current account row (#4941)
* Rename logout to logoutEveryAccount

* Add logoutCurrentAccount()

* Make all "Log out" buttons refer to current account

Each of these usages is completely contextual and refers to a specific account.

* Add Sign out of all accounts to Settings

* Move single account Sign Out below as well

* Prompt on account removal

* Add Other Accounts header to reduce ambiguity

* Spacing fix

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2024-08-15 20:58:13 +01:00
Hailey f3b57dd456 Hack patch for testing OTA update crash behavior (#4942) 2024-08-15 12:03:19 -07:00
Hailey 11061b628e [Video] Download videos (#4886)
Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
2024-08-15 11:23:48 -07:00
Samuel Newman b9975697e2 swap control files (#4936)
Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
2024-08-14 21:08:17 +01:00
Samuel Newman b6fa0d2d04 [Embed] Starter pack embed embed (#4935)
* update @atproto/api

* add starter pack embed

* update depreciated BskyAgent to AtpAgent

* unrelated, but avoid direct import of type

* nits

* rm commented out code

---------

Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
2024-08-14 21:01:59 +01:00
Samuel Newman 21e214c235 [Video] set audio category to ambient every time a new player is made (#4934)
* set auto category to ambient every time a new player is made

* mute on foregrounding

* remember previous state

---------

Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
Co-authored-by: Hailey <me@haileyok.com>
2024-08-14 20:21:14 +01:00
Hailey 26d3777ecc Add /live/ to supported YouTube embed URLs (#4932) 2024-08-13 17:35:05 -07:00
Samuel Newman 630ebf523d [Video] Try/catch video play/pause (#4930)
Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
2024-08-13 14:00:03 -07:00
dan 57be2ea15b Don't kick to login screen on network error (#4911)
* Don't kick the user on network errors

* Track online status for RQ

* Use health endpoint

* Update test with new behavior

* Only poll while offline

* Handle races between the check and network events

* Reduce the poll kickoff interval

* Don't cache partially fetched pinned feeds

This isn't a new issue but it's more prominent with the offline handling. We're currently silently caching pinned infos that failed to fetch. This avoids showing a big spinner on failure but it also kills all feeds which is very confusing. If the request to get feed gens fails, let's fail the whole query.

Then it can be retried.
2024-08-13 18:51:49 +01:00
dan 7e11b862e9 Remove .withProxy() calls (#4929) 2024-08-13 00:20:39 -07:00
Hailey 1fce7a793d [Video] Audio duck off main thread (#4926) 2024-08-12 20:08:51 -07:00
Hailey 3c04d9bd84 subclass agent to add setPersistSessionHandler (#4928)
Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2024-08-12 19:43:06 -07:00
Hailey 99d1a881f2 [Video] Fix crash when switching tabs (#4925) 2024-08-12 16:49:17 -07:00
Hailey 134fcd35d8 [Video] Invert usage of setAudioActive (#4924) 2024-08-12 14:58:41 -07:00
Hailey 7df2327424 Upgrade API, implement XRPC rework (#4857)
Co-authored-by: Matthieu Sieben <matthieu.sieben@gmail.com>
2024-08-12 14:00:15 -07:00
Hailey ae883e2df7 rm from swift (#4923) 2024-08-12 21:46:33 +01:00
Hailey db7a744433 Fix Android composer cursor bug by removing setTimeout from native composer onChangeText (#4922) 2024-08-12 08:14:02 -07:00
Roland Crosby 75c19b2dc2 Show handle in recent searches and fix truncation (#4917)
Co-authored-by: Hailey <me@haileyok.com>
2024-08-11 16:12:36 -07:00
Minseo Lee 88f879ffe9 Improve styles (#4916)
Co-authored-by: Hailey <me@haileyok.com>
2024-08-11 14:30:18 -07:00
surfdude29 6f450b4982 Mark string for localization (#4920) 2024-08-11 12:41:33 -07:00
Shubh Porwal 8367542138 Fix occurred typo (#4919)
Co-authored-by: Hailey <hailey@blueskyweb.xyz>
2024-08-11 12:40:43 -07:00
Hailey 65d6e561d4 [Video] Resume background audio whenever muting video audio (#4915) 2024-08-09 16:52:23 -07:00
Samuel Newman c2131bb039 [Videos] Add error boundary to native (#4914)
* move error fallback to own component

* use error boundary on native

---------

Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
2024-08-10 00:49:11 +01:00
Samuel Newman ab0da7c892 [Videos] handle app backgrounding (#4912)
* play when returning from background

* play when unfullscreening

* play when entering fullscreen, just to be sure

* state -> ref

---------

Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
2024-08-09 23:35:21 +01:00
Samuel Newman 0a9782ac19 animate controls + fade out time after a while (#4913)
Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
2024-08-09 23:32:28 +01:00
Hailey 5bfe5aa503 [Video] More tweaks to AVAudioSession options (#4910) 2024-08-09 14:35:26 -07:00
Hailey dd0d50a6f0 [Video] Prevent pausing of background audio with expo-video on iOS (#4908)
* audio mixing pref

* lint

* patch expo video to add enter/exit fullscreen events

* rm logs

* fix audio problems

* toggle mute when enter/exiting fullscreen

---------

Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
2024-08-09 11:25:54 +01:00
Hailey 0f993a09c2 [Video] Add onEnterFullscreen and onExitFullscreen to VideoView (#4907)
* patch expo video to add enter/exit fullscreen events

* rm logs

* Update expo-video+1.2.4.patch.md
2024-08-09 11:05:43 +01:00
Hailey cb574b7be3 [Videos] Add iOS function to set AVAudioSession.CategoryOptions to .mixWithOthers (#4905)
* audio mixing pref

* lint
2024-08-09 11:05:12 +01:00
Samuel Newman a4f0c9c753 [Video] more minor tweaks (#4906)
* update expo-video

* pause when on a different screen

* rm collapsable

* add mute/unmute button

---------

Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
2024-08-09 03:24:38 +01:00
Samuel Newman 4350dbc853 [Video] Bundle of minor tweaks (#4904)
* fix bg color

* unique video urls for debug

* improve controls slightly

* mute until fullscreen

---------

Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
2024-08-09 02:33:42 +01:00
dan e782db33dc Show just-posted replies above OP replies (#4901)
* Unify onPostReply handler

* Show just-posted replies above OP replies

* Only do this for the highlighted post or thread mode

It's confusing to have your post displace OP thread or other people's leaf posts.
2024-08-08 19:19:58 +01:00
Hailey c1af767fa6 Remove client filtering of starter packs (#4753) 2024-08-08 10:51:54 -07:00
Eric Bailey a0756c53be Remove show_avi_follow_button (#4900) 2024-08-08 18:14:15 +01:00
Eric Bailey 3ea8eb6013 Remove native_pwi_disabled (#4896)
* Remove native_pwi_disabled

* Remove search button
2024-08-08 18:14:02 +01:00
Samuel Newman e7a0055a85 Fix overflow on posts (#4899)
* overflow posts, via contentHider

* margin->padding for a bit more leeway

* overflow notifications

* overflow on header

* revert from contenthider, put on text in all places

* fix zalgo text in handle in composer

---------

Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
2024-08-08 18:04:40 +01:00
dan ae25cb3391 Move onPressReply into child component (#4898)
* Move ComposePrompt to post-thread/

* Move onPressReply into child component
2024-08-08 17:58:58 +01:00
Eric Bailey 85fe95c988 Remove new_user_progress_guide (#4895) 2024-08-08 11:48:42 -05:00
Eric Bailey e1df2b0b16 Remove explore_page_profile_card_social_proof (#4894) 2024-08-08 11:48:32 -05:00
Eric Bailey f3a9f874d9 Remove ungroup_follow_backs gate (#4893) 2024-08-08 11:48:24 -05:00
dan 4b71950d99 Remove unnecessary state update for reply gate (#4897)
* Move mobile compose prompt to inner component

* Make canReply computed

* Use same clamp we use elsewhere
2024-08-08 17:05:51 +01:00
dan 2174feed44 Include follow-based suggestions in interstitial (#4889) 2024-08-08 15:49:34 +01:00
dan f1031d100b Cleanup flags (#4891)
* Remove launched gates

* Remove disabled gates
2024-08-08 15:25:42 +01:00
Eric Bailey 1e3b2d6f42 ALF suggested follows in profile header (#4828)
* Refactor ProfileHeaderSuggestedFollows

* Load fresh data every time

* Oops, missed a file

* Update ProfileCard.Link usage, tweak copy
2024-08-08 09:19:51 -05:00
Minseo Lee af5262682e Added trans (#4890) 2024-08-08 13:12:23 +01:00
dan a864f69849 Keep interstitial fresh on refresh (#4888) 2024-08-08 06:20:24 +01:00
dan 00fea10782 Include popcluster in suggestion ranking (#4887) 2024-08-08 05:56:22 +01:00
Hailey b3092413dd Add logging of selected feed preference when displaying the following feed (#4789) 2024-08-07 17:13:29 -07:00
Hailey 1b02f81cb8 [Video] Visibility detection view (#4741)
Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
2024-08-07 14:45:06 -07:00
Samuel Newman fff2c079c2 [Videos] Video player - PR #2 - better web support (#4732)
* attempt some sort of "usurping" system

* polling-based active video approach

* split into inner component again

* click to steal active video

* disable findAndActivateVideo on native

* new intersectionobserver approach - wip

* fix types

* disable perf optimisation to allow overflow

* make active player indicator subtler, clean up video utils

* partially fix double-playing

* start working on controls

* fullscreen API

* get buttons working somewhat

* rm source from where it shouldn't be

* use video elem as source of truth

* fix keyboard nav + mute state

* new icons, add fullscreen + time + fix play

* unmount when far offscreen + round 2dp

* listen globally to clicks rather than blur event

* move controls to new file

* reduce quality when not active

* add hover state to buttons

* stop propagation of videoplayer click

* move around autoplay effects

* increase background contrast

* add subtitles button

* add stopPropagation to root of video player

* clean up VideoWebControls

* fix chrome

* change quality based on focused state

* use autoLevelCapping instead of nextLevel

* get subtitle track from stream

* always use hlsjs

* rework hls into a ref

* render player earlier, allowing preload

* add error boundary

* clean up component structure and organisation

* rework fullscreen API

* disable fullscreen on iPhone

* don't play when ready on pause

* debounce buffering

* simplify giant list of event listeners

* update pref

* reduce prop drilling

* minimise rerenders in `ActiveViewContext`

* restore prop drilling

---------

Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
Co-authored-by: Hailey <me@haileyok.com>
2024-08-07 18:47:51 +01:00
Samuel Newman b701e8c68c [Video] Authed video upload (#4885)
* add service auth call

* update API package

---------

Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
2024-08-07 16:56:12 +01:00
Hailey 753a233408 Tweak feed manip to show cases of A -> B without further children (#4883) 2024-08-06 11:21:59 -07:00
dan 5845e08eee Show own replies before follows' replies in threads (#4882) 2024-08-06 17:12:27 +01:00
dan b291a1ed8a Show more replies in Following (different heuristic) (#4880) 2024-08-06 16:42:42 +01:00
dan 686d5ebb53 [Persisted] Make broadcast subscriptions granular by key (#4874)
* Add fast path for guaranteed noop updates

* Change persisted.onUpdate() API to take a key

* Implement granular broadcast listeners
2024-08-06 01:30:52 +01:00
dan 966f6c511f [Persisted] Fix the race condition causing clobbered writes between tabs (#4873)
* Broadcast the update in the same tick

The motivation for the original code is unclear. I was not able to reproduce the described behavior and have not seen it mentioned on the web. I'll assume that this was a misunderstanding.

* Remove defensive programming

The only places in this code that we can expect to throw are schema.parse(), JSON.parse(), JSON.stringify(), and localStorage.getItem/setItem/removeItem. Let's push try/catch'es where we expect them to be necessary.

* Don't write or clobber defaults

Writing defaults to local storage is unnecessary. We would write them as a part of next update anyway. So I'm removing that to reduce the number of moving pieces.

However, we do need to be wary of _state being set to defaults. Because _state gets mutated on write. We don't want to mutate the defaults object. To avoid having to think about this, let's copy on write. We don't write to this object very often.

* Refactor: extract tryParse

* Refactor: move string parsing into tryParse

* Extract tryStringify, split logging by platform

Shared data parsing/stringification errors are always logged. Storage errors are only logged on native because we trust the web APIs to work.

* Add a layer of caching to readFromStorage to web

We're going to be doing a read on every write so let's add a fast path that avoids parsing and validating.

* Fix the race condition causing clobbered writes between tabs
2024-08-06 01:03:27 +01:00
dan 5bf7f3769d [Persisted] Fork web and native, make it synchronous on the web (#4872)
* Delete logic for legacy storage

* Delete superfluous tests

At this point these tests aren't testing anything useful, let's just get rid of them.

* Inline store.ts methods into persisted/index.ts

* Fork persisted/index.ts into index.web.ts

* Remove non-essential code and comments from both forks

* Remove async/await from web fork of persisted/index.ts

* Remove unused return

* Enforce that forked types match
2024-08-06 00:30:58 +01:00
dan 74b0318d89 Show replies in context of their threads (#4871)
* Don't reconstruct threads from separate posts

* Remove post-level dedupe for now

* Change repost dedupe condition to look just at length

* Delete unused isThread

* Delete another isThread field

It is now meaningless because there's nothing special about author threads.

* Narrow down slice item shape so it does not need reply

* Consolidate slice validation criteria in one place

* Show replies in context

* Make fallback marker work

* Remove misleading and now-unused property

It was called rootUri but it was actually the leaf URI. Regardless, it's not used anymore.

* Add by-thread dedupe to non-author feeds

* Add post-level dedupe

* Always count from the start

This is easier to think about.

* Only tuner state need to be untouched on dry run

* Account for threads in reply filtering

* Remove repost deduping

This is already being taken care of by item-level deduping. It's also now wrong and removing too much (since it wasn't filtering for reposts directly).

* Calculate rootUri correctly

* Apply Following settings to all lists

* Don't dedupe intentional reposts by thread

* Show reply parent when ambiguous

* Explicitly remove orphaned replies from following/lists

* Fix thread dedupe to work across pages

* Mark grandparent-blocked as orphaned

* Guard tuner state change by dryRun

* Remove dead code

* Don't dedupe feedgen threads

* Revert "Apply Following settings to all lists"

This reverts commit aff86be6d37b60cc5d0ac38f22c31a4808342cf4.

Let's not do this yet and have a bit more discussion. This is a chunky change already.

* Reason belongs to a slice, not item

* Logically feedContext belongs to the slice

* Update comment to reflect latest behavior
2024-08-05 20:51:41 +01:00
Hailey 18b423396b Add PlatformInfo module (#4877) 2024-08-05 12:21:34 -07:00
bnewbold fb278384c6 bskyweb: optional basic auth password middleware (#4759) 2024-08-02 15:57:50 -07:00
Samuel Newman 6298e6897f tweak list header (#4870)
Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
2024-08-03 00:33:45 +02:00
Eric Bailey c3d8beee6d Respect labels on feeds and lists (#4818)
* Prep

* Pass in optional moderation to FeedCard

* Compute moderation decision, filter contentList contexts, pass into card

* Let's go a different route

* Filter from within search queries

* Use same search query for starter packs

* Filter lists from profile tabs

* Cleanup

* Filter from profile feeds

* Moderate post embeds

* Memoize

* Use ScreenHider on lists

* Hide both list types

* Fix crash on iOS in screen hider, fix lineheight

* Memoize renderItem

* Reuse objects to prevent re-renders
2024-08-02 13:05:33 -05:00
dan 293ac6fab2 Only show replies in Following if following all involved actors (#4869)
* Only show replies in Following for followed root and grandparent

* Remove now-unnecessary check

* Simplify condition
2024-08-02 17:13:31 +01:00
dan 7f292abf51 Always limit Following replies to the people you follow (#4868)
* Limit feed replies to people you follow

* Remove dead code
2024-08-01 22:05:40 +01:00
Hailey f056cb646e Fix missing header on Likes/Reposted By, add missing perf optimizations (#4867)
* fix liked by list

* fix lists

* tweaks to style

* change string
2024-08-01 10:32:36 -07:00
Samuel Newman c78e9e3147 Move theme controls to its own screen (#4866) 2024-08-01 19:14:32 +02:00
dan 388c157c36 Display second-to-last rather than second post in a slice (#4864) 2024-08-01 17:49:43 +01:00
Eric Bailey b0e130a4d8 Update muted words dialog with expiresAt and actorTarget (#4801)
* WIP not working dropdown

* Update MutedWords dialog

* Add i18n formatDistance

* Comments

* Handle text wrapping

* Update label copy

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

* Fix alignment

* Improve translation output

* Revert toggle changes

* Better types for useFormatDistance

* Tweaks

* Integrate new sdk version into TagMenu

* Use ampersand

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

* Bump SDK

---------

Co-authored-by: Hailey <me@haileyok.com>
Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
2024-08-01 10:29:27 -05:00
dan d2e88cc623 Fetch enough pages to fill a page's worth of items (#4863)
* Fetch enough pages to fill a page's worth of items

* Add failsafe in case of appview bug
2024-08-01 02:27:25 +01:00
Hailey 70ffd387e3 Only show "followed you back" when appropriate (#4849)
* only show followed back when we should

* try/catch

* log

* Update FeedItem.tsx

* tweak
2024-07-31 11:16:14 -07:00
dan 576cef88b5 [Web] Retrigger onEndReached if needed when content height changes (#4859)
* Extract EdgeVisibility

* Key Visibility by container height instead of item count
2024-07-31 19:10:24 +01:00
dan c75bb65bef Remove unused NoopFeedTuner (#4856) 2024-07-31 13:00:22 +01:00
GSMT c3e77b56ff useDedupe callback (#4855) 2024-07-30 15:19:23 -07:00
Hailey 8ddb28d3c5 [Video] Uploads (#4754)
* state for video uploads

* get upload working

* add a debug log

* add post progress

* progress

* fetch data

* add some progress info, web uploads

* post on finished uploading (wip)

* add a note

* add some todos

* clear video

* merge some stuff

* convert to `createUploadTask`

* patch expo modules core

* working native upload progress

* platform fork

* upload progress for web

* cleanup

* cleanup

* more tweaks

* simplify

* fix type errors

---------

Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
2024-07-30 08:25:31 -07:00
Eric Bailey 43ba0f21f6 Make label required in link components (#4844) 2024-07-25 18:34:21 -05:00
Dmitrii Kartashev 4437b9a557 Boolean filter improvement alternative: TS upgrade (#4840)
* upgrade typescript and use new feature

* fix: typing error
2024-07-26 00:31:59 +01:00
Eric Bailey 50769b0b6f Add label to profile card (#4843) 2024-07-25 18:11:31 -05:00
Eric Bailey 043e5cea64 Improve a11y on noty feed (#4842) 2024-07-25 18:11:16 -05:00
Eric Bailey 35165e3d9b Add labels in feed card (#4836) 2024-07-25 18:07:42 -05:00
Eric Bailey 1d827cebe4 Add labels to mod details dialog (#4839) 2024-07-25 18:07:23 -05:00
Eric Bailey 783fd351ba Add labels to a few missing places (#4838) 2024-07-25 18:07:15 -05:00
Eric Bailey e7b485f185 Add labels in list card (#4837) 2024-07-25 18:07:07 -05:00
dan ac1538baad Refactor feed slices (#4834)
* Copy FeedViewPost into FeedSliceItem

* Explicitly construct feed slice items by copying known fields

* Type rootItem as FeedViewPost for now

Mergefeed logic relies on that.

* Unify reason and __source for slice items

* Move feedContext out of FeedSliceItem

* Remove slice.isFlattenedReply

* Remove unnused slice.ts

* Inline slice.isFullThread

* Refactor condition for clarity

* Extract slice.includesThreadRoot

* Encapsulate more usages of slice.rootItem into slice

* Rename slice.rootItem so semi-private slice._feedPost

* Move reason into slice

* Simplify slice ctor argument

* Reorder getters to reduce diff

* Make feedContext a getter to reduce diff
2024-07-25 23:02:37 +01:00
Hailey 3914025227 true (#4833) 2024-07-25 21:26:37 +01:00
Samuel Newman 401e92ed42 Replace import hairlineWidth = with const (#4831)
* replace import with const

* just use `StyleSheet.hairlineWidth`

---------

Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
2024-07-25 20:45:15 +01:00
Samuel Newman 00240b95b9 [Videos] Video player - PR #1 - basic player (#4731)
* add ffmpeg-kit-react-native

* get select video button + compression working

* up res to 1080p

* add progress component

* move logic out of compressVideo

* (WIP) add lonestar compression

* rework web compression a bit

* mess around with adding a thumbnail

* 3mbps

* replace

* use 3mbps

* add expo-video

* remove unnecessary try/catch

* rm ToastAndroid

* fix web

* wrap lazy component in suspense

* gate video select button

* rm web compression

* flip sign

* remove expo-video from web

* review nits

* add video picker permissions + rm temp buttons

* add ffmpeg-kit-react-native

* replace

* hls-capable player

* start trying to hoist up video player instance

* hoist video player and move things around

* always show native controls

* fix controls on expo video android

* gate temp video player in feed

* rm IS_DEV, doesn't do what I thought it did

* use __DEV__ instead

---------

Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
Co-authored-by: Hailey <me@haileyok.com>
2024-07-25 20:41:50 +01:00
Hailey 4ec999cab7 Bump 1.90 (#4832) 2024-07-25 20:41:32 +01:00
dan 4291711f1d Fix sloppy filter(Boolean) types (#4830)
* Fix sloppy filter(Boolean) in threadgate

* Fix sloppy filter(Boolean) in Explore

* Fix sloppy filter(Boolean) in post-feed

* Harden FeedPostSliceItem.reason type def

* Harden parentAuthor types

* Fix lying component types, handle blocks
2024-07-25 19:53:12 +01:00
Eric Bailey fac1af43b0 Fuggedaboudit (#4829) 2024-07-25 13:16:21 -05:00
Paul Frazee a03622dd55 Release 1.89 prep (#4822)
* Fix curate-lists tests

* Run intl extract
2024-07-24 15:23:31 -07:00
surfdude29 2e7398b7c3 Update German localization (part 1) (#4742)
* Update German localization

* Apply suggestions from code review

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

* update string after #4743 merged

* Apply suggestions from code review

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

* Starterpaket –––> Startpaket

* Improve existing translations (#17)

* Improve existing translations

* Update more

* Update more

---------

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

* capitalise toast

---------

Co-authored-by: cdfzo <cdfzo@pm.me>
2024-07-24 14:41:43 -07:00
Takayuki KUSANO 8588a2ad51 Updated Japanese Translation (#4748)
* Updated translation

* Update translation

* Update translation
2024-07-24 14:41:05 -07:00
Kuwa Lee 7a0aa661a7 Update Chinese Localization (#4774)
* TW: Update

* TW: Clean

* TW: Update

* CN: Update translates

* Both: Remove superseded strings

* Both: Remove superseded strings#2

* TW: Update and clean

* TW: Update

* CN: Update translates

* TW: Improve

* Update messages.po

* CN: Update translates

---------

Co-authored-by: Frudrax Cheng <i@cynosura.one>
Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>
2024-07-24 14:40:32 -07:00
surfdude29 86ac3d687c Update French localization (#4781)
* Update French localization

* Apply suggestion from code review

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

---------

Co-authored-by: Stanislas Signoud <signez@stanisoft.net>
2024-07-24 14:39:38 -07:00
Hailey 11f2415942 make some settings screens scrollable for accessibility (#4819)
* make settings scrollable for accessibility

* nit
2024-07-24 14:39:01 -07:00
Hailey bfb7f6efef make toast shorter (#4821) 2024-07-24 14:23:37 -07:00
Samuel Newman efde018b13 special invalidation logic (#4820)
Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
2024-07-24 21:44:41 +01:00
Samuel Newman 8fe5ddfa49 Modernise thread/following feed settings screen (#4797)
* fix web

* show back button on tablet for certain settings screens

* move headers to inside of scrollview

---------

Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
2024-07-24 20:40:06 +01:00
Samuel Newman cfb8a3160e Priority notifications (#4798)
* new settings screen

* bring back the spinner

* add experimental language

* fix typo, change leading

* integrate priority notifications API

* update package

* use refetch instead of invalidateQueries

* fix read-after-write issue by polling for update

* add spinner for initial load

* rm onmutate, it's overcomplicated

* set error state eagerly

* Change language in description

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

* prettier

* add `Toggle.Platform`

* extract out mutation hook + error state

* rm useless cache mutation

* disambiguate isError and isPending

* rm unused isError

---------

Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
Co-authored-by: Hailey <me@haileyok.com>
2024-07-24 20:09:20 +01:00
dan 9bd8393685 Use consistent index for reading last item (#4817) 2024-07-24 02:32:05 +01:00
Paul Frazee 4af6bcb11a Hack-modify avi path to use smaller thumbs behind gate (#4816)
* Hack-modify avi path to use smaller thumbs behind gate

* Only use small thumbs on smaller rendering
2024-07-23 16:57:51 -07:00
Samuel Newman 67ce18a113 swap flex-start/end (#4815)
Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
2024-07-23 23:03:20 +01:00
Samuel Newman 3755807b5e Fix switch style and animate (#4814)
* set height to 20+(2*hairline)

* animate switch

* Try to align on line height across platforms

* Use border 1px

---------

Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
Co-authored-by: Eric Bailey <git@esb.lol>
2024-07-23 22:49:37 +01:00
Samuel Newman abb709d209 Native toast rework (#4808)
* rework toast to use reanimated

* fix animation on iOS

---------

Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
2024-07-23 19:50:47 +01:00
Samuel Newman 27d712290a Use appropriate icons for toasts (#4803)
* use appropriate icons for toasts

* use info for session expiry

* tweak size

* message -> safeMessage

---------

Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
2024-07-23 15:01:04 +01:00
Hailey 59bafb426f dont filter exact matches from typeahead (#4802) 2024-07-19 15:25:09 -07:00
Eric Bailey 3627a249ff Remove invalid labelers when subscribing/unsubscribing (#4771)
* Remove invalid labelers when subscribing/unsubscribing

* Let the async lock cook

* Use link to associate, leave copy as is
2024-07-12 14:55:34 -05:00
Eric Bailey 7c1c24ef1b Replace incongruous buttons in drawer (#4772)
* Replace incongruous buttons in drawer

* Improve a11y help
2024-07-12 12:12:57 -05:00
Eric Bailey f85df425f7 Invert saturation scale as well (#4783) 2024-07-12 10:58:12 -05:00
Hailey 306f162639 Make all referrer info sync (#4782) 2024-07-12 07:37:43 -07:00
Hailey f021c06468 Return a promise for getReferrerInfoAsync on web (#4777) 2024-07-11 19:51:54 -07:00
Michał Gołda cd9bba4b69 Fix: emoji picker closes when hitting spacebar (#4422) (#4441)
Co-authored-by: Hailey <me@haileyok.com>
2024-07-11 19:42:41 -07:00
Minseo Lee a71134f467 Reposted by you and Reply to you (#4478)
Co-authored-by: Hailey <me@haileyok.com>
2024-07-11 19:42:27 -07:00
Hailey 8b121af2e4 referrers for all platforms (#4514) 2024-07-11 18:43:10 -07:00
Hailey 83e8522e0a Create shared preferences API (#4654) 2024-07-11 18:37:43 -07:00
Hailey 2397104ad6 Lint native files (#4768) 2024-07-11 18:15:35 -07:00
Hailey b433469ab9 bump 1.89.0 (#4776) 2024-07-12 02:14:47 +01:00
Eric Bailey 119b4d3e0a Fix search hashtags by author (#4773) 2024-07-11 23:35:13 +01:00
Eric Bailey 74186950b2 [ALF] Theme & palette cleanup (#4769)
* Invert primary scale

* Invert negative palette

* Replace theme specific styles in Toggle

* Remove theme specific colors from Button, improves secondary solid on dark mode

* TextField

* Remove from MessageItem

* Threadgate editor

* IconCircle

* Muted words

* Generate themes from hues

* Cleanup

* Deprecate more values, fix circular import

* Invert positive too, hardly use

* Button tweaks, some theme diffs

* Match disabled state for negative button

* Fix unread noty bg
2024-07-11 16:59:12 -05:00
Eric Bailey ea0586cd67 Filter yourself out of follows interstitial (#4770) 2024-07-11 21:20:22 +01:00
Hailey 15a00b1864 ALF post button (#4767) 2024-07-10 14:48:34 -07:00
Hailey 65b5d31b58 Fix alt text for GIFs (#4766)
* fix alt text dialog for gifs

* only apply to native
2024-07-10 20:25:44 +01:00
dan fa64d9393b [Session] Include stack with the debug event (#4763) 2024-07-10 00:51:12 +01:00
dan 9b9e09d002 [Session] Experiment: Don't use withProxy (#4762)
* Reorder statements

* Remove withProxy() usage behind a gate
2024-07-10 00:12:26 +01:00
dan ce0bf867ff Rename session event, scope to errors (#4761) 2024-07-09 23:03:59 +01:00
dan 7a47656816 Log session events unconditionally (#4758) 2024-07-09 21:13:32 +01:00
Samuel Newman 2d0eefebc3 Add social proof to suggested follows (#4602)
* replace unused `followers` prop with social proof

* Introduce 'minimal' version

* Gate social proof one explore page, fix space if no desc

* Use smaller avis for minimal

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2024-07-09 11:10:50 -05:00
A Malik. Muhd 4360087ced Update README.md (#4394) 2024-07-08 19:02:26 -07:00
Hailey ca0a327052 tweak top padding external (#4755) 2024-07-09 02:16:34 +01:00
Hailey a3c43a7471 change contentVisibility to contain (#4752) 2024-07-08 15:16:43 -07:00
Hailey f8a59e10dd Fix RTL text rendering for display names (#4747)
* header display name rtl support

* highlighted post rtl

* move `NON_BREAKING_SPACE` to an external constant

* rtl support in search dropdown

* profile card rtl

* old profile card rtl

* hover card

* wizard list card

* new chat

* account card

* chat header

* clean up notifications

* just force LTR on meta display name
2024-07-08 18:06:00 +01:00
Hailey d5503d17de Reduce the size of the inner logo in the QR code (#4746)
* use a scale of `1` on QR code inner logo

* remove logo entirely

* Revert "remove logo entirely"

This reverts commit dee57f48c7.

* use scale of 0.95
2024-07-08 16:57:40 +01:00
Joel ea7afecf28 Fix misplaced '@' in RTL post meta. (#4531)
Co-authored-by: Joel <joel.garplind+github@gmail.com>
Co-authored-by: Hailey <me@haileyok.com>
2024-07-07 15:27:30 -07:00
Josh ea37298cdd Remove broken and void back button (#4744) 2024-07-07 13:10:26 -07:00
Hailey a6b3c97df0 Ensure /start navigates to /starter-pack when clicking a link internally (#4745)
* remove temporary metadata removal

* ensure proper navigation
2024-07-07 13:07:28 +01:00
surfdude29 6b5e3181fb Add missing to in StarterPackScreen.tsx string (#4743) 2024-07-06 16:39:04 -07:00
Samuel Newman 8f06ba70bb Video compression in composer (#4638)
Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
Co-authored-by: Hailey <me@haileyok.com>
2024-07-05 17:50:03 -07:00
Hailey 56b688744e fix slop (#4739) 2024-07-05 22:44:06 +01:00
Paul Frazee 09bc4e95d8 Update stats 2024-07-05 12:37:06 -07:00
Paul Frazee 149446a26e Run intl:extract 2024-07-05 12:33:48 -07:00
Takayuki KUSANO 8c52d74925 Update Japanese translation (#4665)
* Update Japanese translation

* Updated Japanese translation

* Update translation

* Updated translation

* Update translation

* Update translation

* Update translation.

* Updated translation
2024-07-05 12:32:31 -07:00
Minseo Lee f45193783e Update Korean localization (#4646)
* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po
2024-07-05 12:32:02 -07:00
Kuwa Lee adcd066733 Update Chinese Localization (#4695)
* CN: Update translates

* CN: Remove superseded strings

* CN: Update translates

* CN: Run intl:extract

* CN: Remove superseded strings

* CN: Optimize translation of starter pack

* CN: Run intl:extract

* CN: Remove superseded strings

* CN: Update translates

* CN: fix typo

* CN: Optimize Translations

* CN: hot fix

* TW: Update

* TW: Update and clean

* CN: hot fix

* BOTH: commit as LF

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

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

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

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

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

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

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

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

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

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

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

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

* TW: Apply suggestions

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>

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

* CN: Update translates

* CN: Update translates

* TW: Update and clean

---------

Co-authored-by: Frudrax Cheng <i@cynosura.one>
Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>
2024-07-05 12:28:50 -07:00
Ivan Beà b10a2b9a8e Update catalan (#4702)
* Update catalan

New lines added, new lines localized.

Check it please @jordimas @darccio @surfdude29

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

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

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

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

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

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

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

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

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

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

* Update messages.po

Apply  @surfdude29 corrections

---------

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
2024-07-05 12:28:29 -07:00
kodebanget 75da5f4965 Update Indonesian translation (#4706)
Co-authored-by: Indonesian <github-actions@github.com>
2024-07-05 12:28:03 -07:00
Paul Frazee baa788de38 Tweak checkmark size 2024-07-05 12:26:58 -07:00
dan 09dfc9edf8 Show feedback for Follow button in interstitials (#4738)
* Fix Follow in interstitials

* Show feedback in toast
2024-07-05 20:17:47 +01:00
surfdude29 d5fd19df8f Update French localization (#4662)
* Update French localization

* écriture inclusive for other usage of `amis`

* add one more string

* Apply suggestions from code review

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

* Update revision date

* translate new strings

* Apply suggestions from code review

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

---------

Co-authored-by: Stanislas Signoud <signez@stanisoft.net>
2024-07-05 10:31:12 -07:00
Samuel Newman 58e48fd31b Feed interstitial snapping (#4737)
Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
2024-07-05 18:01:13 +01:00
Eric Bailey 3407206f52 [D1X] Use user action and viewing history to inform suggested follows (#4727)
* Use user action and viewing history to inform suggested follows

* Remove dynamic spreads

* Track more info about seen posts

* Add ranking

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2024-07-04 22:28:38 +01:00
dan 1c6bfc02fb Fix order of checks in experiment (#4734) 2024-07-04 21:15:47 +01:00
dan d03dd8c815 Feed interstitial tweaks (#4733)
* Swap interstitial positions

* Fix color
2024-07-04 20:54:49 +01:00
Hailey ca7386967a Fix onEndReached not firing sometimes on web (#4728)
* handle off screen visibility observer.

* Revert "handle off screen visibility observer."

This reverts commit e499ea0ed6.

* key ftw

* Remove special case

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2024-07-04 20:12:15 +01:00
Paul Frazee d837f96478 Fix responsiveness of dismissing the progress guide (#4729) 2024-07-04 20:08:33 +01:00
dan 12bf796293 Fix feed feedback (#4730) 2024-07-04 20:07:42 +01:00
Eric Bailey 4f02da96c8 [D1X] Pull out follow-backs for higher signal (#4719)
* Pull out follow-backs for higher signal

* Gate it

* Fix early gate check

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2024-07-04 04:13:47 +01:00
Paul Frazee 0ed99b840d New user progress guides (#4716)
* Add the animated checkmark svg

* Add progress guide list and task components

* Add ProgressGuide Toast component

* Implement progress-guide controller

* Add 7 follows to the progress guide

* Wire up action captures

* Wire up progress-guide persistence

* Trigger progress guide on account creation

* Clear the progress guide from storage on complete

* Add progress guide interstitial, put behind gate

* Fix: read progress guide state from prefs

* Some defensive type checks

* Create separate toast for completion

* List tweaks

* Only show on Discover

* Spacing and progress tweaks

* Completely hide when complete

* Capture the progress guide in local state, and only render toasts while guide is active

* Fix: ensure persisted hydrates into local state

* Gate

---------

Co-authored-by: Eric Bailey <git@esb.lol>
Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2024-07-04 03:05:19 +01:00
Hailey aa7117edb6 Add starter pack embeds to posts (#4699)
* starter pack embeds

* revert test code

* Types

* add `BaseLink`

* precache on click

* rm log

* add a comment

* loading state

* top margin

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2024-07-04 02:15:08 +01:00
Paul Frazee a3d4fb652b Guided tour for new users (#4690)
* Add home guided tour (WIP)

* Add web handling of the tour

* Switch to our fork of rn-tourguide

* Bump guided-tour

* Fix alignment on android

* Implement home page tour trigger after account creation

* Add new_user_guided_tour gate

* Add a title line to the tour tooltips

* A11y improvements: proper labels, focus capture, scroll capture

* Silence type error

* Native a11y

* Use FocusScope

* Switch to useWebBodyScrollLock()

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2024-07-02 21:25:19 -07:00
Hailey 6694a33603 Don't follow self, blocks or mute when following all; don't show blocks in list (#4715)
* don't follow self when following all

* also filter blocks

* add more filtering to follow all

* extract logic to functions
2024-07-03 03:50:04 +01:00
Samuel Newman dc3c81c4e2 change maxWidth -> width to fix GIFs in DMs (#4717)
Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
2024-07-03 03:48:55 +01:00
Eric Bailey 04cfd06639 [D1X] Integrate interstitials (#4698)
* Use discriminated union

* Integrate interstitials

* Add gates and handling for variants

* Only show interstitials for logged in accounts since flags are based on user ID

* Nit

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2024-07-03 03:43:54 +01:00
Eric Bailey 0598fc2faa [D1X] Add interstitials, component tweaks, placeholders (#4697)
* Add interstitials, component tweaks, placeholders

* Tweak feed card styles

* Port over same fix to ProfileCard

* Add browse more link on desktop

* Rm Gemfile

* Update logContext

* Update logContext

* Add click metric to cards

* Pass through props to ProfileCard.Link

* 2-up grid for profile cards on desktop web

* Add secondary_inverted button color

* Use inverted button color

* Adjust follow button layout

* Update skeleton

* Use round button

* Translate
2024-07-03 03:34:18 +01:00
Takayuki KUSANO 6af78de9ee Make tab names translatable (#4724) 2024-07-03 02:21:33 +01:00
Eric Bailey cacc4c5068 Remove search from disabled PWI state (#4723) 2024-07-03 01:15:04 +01:00
Eric Bailey 14c2d75d49 Unify label pills (#4676)
* New label pills

* Fix type errors, add default case

* Remove negative margin, only works in some places

* Fix alignment edge case

* Add a bit of padding

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2024-07-03 00:15:20 +01:00
Eric Bailey c133661768 Add music interest (#4722) 2024-07-02 17:11:28 -05:00
Paul Frazee 63bb8fda2d Improve textinput performance in login and account creation (#4673)
* Change login form to use uncontrolled inputs

* Debounce state updates in account creation to reduce flicker

* Refactor state-control of account creation forms to fix perf without relying on debounces

* Remove canNext and enforce is13

* Re-add live validation to signup form (#4720)

* Update validation in real time

* Disable on invalid

* Clear server error on typing

* Remove unnecessary clearing of error

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2024-07-02 22:43:34 +01:00
Eric Bailey 4bb4452f08 [D1X] Minimum interest experiment (#4653)
* Change up copy

* Add min # prompt

* Improve style

* Add gate

* Tweak padding

* Translate

* Revert string change

---------

Co-authored-by: dan <dan.abramov@gmail.com>
2024-07-02 20:19:03 +01:00
Hailey 0012c6d40f Add events to signup for captcha results (#4712) 2024-07-01 15:11:04 -07:00
dan a9fe87b842 Add dismiss backdrop to native dropdowns (#4711) 2024-07-01 18:45:15 +01:00
Eric Bailey 1a037d3542 FeedCard & ListCard cleanups (#4644)
* Extract ListCard from FeedCard

* Export FeedCard.Action and optionally include in ListCard

* Remove list dual usage from most of FeedCard

* Update usages of FeedCard and ListCard

* Add back list purpose logic

* Make Action comp easier to use, clarify list purpose

* Rename Action to SaveButton
2024-06-28 08:27:54 -05:00
Hailey 58a97db5b8 Revert animation change in signup (#4693) 2024-06-27 22:01:02 -07:00
Hailey 8ebf9cc4b1 Handle pushing to starterpack screen when unauthed (#4692) 2024-06-27 21:44:26 -07:00
Hailey 91c4aa7c2d Handle pressing all go.bsky.app links in-app w/ resolution (#4680) 2024-06-27 19:35:20 -07:00
Hailey 030c8e268e Bump 1.88.0 (#4688) 2024-06-27 16:25:21 -07:00
Hailey d5ca95233e offer a json response for grabbing short links (#4671) 2024-06-27 11:31:24 -07:00
Eric Bailey fff3ae8f35 Refactor ProfileCard to be composable (#4622)
* Break up new profile card for easier re-use

* Break things up a bit more

* Add round variant support and other button props

* Handle blocks

* Add Outer export

* Tweak space
2024-06-27 13:27:37 -05:00
dan d26928a5d8 Remove reposts from the Replies tab (#4669) 2024-06-27 18:39:36 +01:00
dan 58102377fd Fix pasting images on web (#4670) 2024-06-27 18:36:06 +01:00
devin ivy 49396451ec bskyogcard: support emoji, more languages, long starter pack names (#4668) 2024-06-27 13:02:29 -04:00
Paul Frazee f6b138f709 Run intl extract 2024-06-26 19:03:52 -07:00
Hailey 0ab6d54093 Add some events to landing screen (#4664) 2024-06-26 19:00:35 -07:00
Hailey 5641a4393c update follows when pressing follow all (#4663) 2024-06-26 18:57:57 -07:00
Hailey 878b0476dd Better starterpack embed (#4659) 2024-06-26 17:24:33 -07:00
556 changed files with 56944 additions and 30114 deletions
+2
View File
@@ -0,0 +1,2 @@
[*.{kt,kts}]
indent_size=2
+13
View File
@@ -71,6 +71,19 @@ module.exports = {
'simple-import-sort/exports': 'warn',
// TODO: Reenable when we figure out why it gets stuck on CI.
// 'react-compiler/react-compiler': 'error',
'no-restricted-imports': [
'error',
{
paths: [
{
name: '@atproto/api',
importNames: ['moderatePost'],
message:
'Please use `moderatePost_wrapped` from `#/lib/moderatePost_wrapped` instead.',
},
],
},
],
},
ignorePatterns: [
'**/__mocks__/*.ts',
@@ -1,8 +1,6 @@
name: build-and-push-ogcard-aws
on:
push:
branches:
- divy/bskycard
workflow_dispatch:
env:
REGISTRY: ${{ secrets.AWS_ECR_REGISTRY_USEAST2_PACKAGES_REGISTRY }}
+3
View File
@@ -115,3 +115,6 @@ src/locale/locales/**/*.js
*.apk
*.aab
*.ipa
# ogcard assets
bskyogcard/src/assets/fonts/noto-*
+1 -1
View File
@@ -10,7 +10,7 @@ RUN yarn install --frozen-lockfile
COPY ./bskyogcard ./
# build then prune dev deps
RUN yarn build
RUN yarn install-fonts && yarn build
RUN yarn install --production --ignore-scripts --prefer-offline
# Uses assets from build stage to reduce build size
+4 -4
View File
@@ -10,7 +10,7 @@ Get the app itself:
## Development Resources
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).
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 open source, but in [a different git repository](https://github.com/bluesky-social/atproto).
There is a small amount of Go language source code (in `./bskyweb/`), for a web service that returns the React Native Web application.
@@ -42,10 +42,10 @@ The Bluesky Social application encompasses a set of schemas and APIs built in th
- Open an issue and give some time for discussion before submitting a PR.
- Stay away from PRs like...
- Changing "Post" to "Skeet."
- Refactoring the codebase, eg to replace mobx with redux or something.
- Refactoring the codebase, e.g., to replace MobX with Redux or something.
- Adding entirely new features without prior discussion.
Remember, we serve a wide community of users. Our day to day involves us constantly asking "which top priority is our top priority." If you submit well-written PRs that solve problems concisely, that's an awesome contribution. Otherwise, as much as we'd love to accept your ideas and contributions, we really don't have the bandwidth. That's what forking is for!
Remember, we serve a wide community of users. Our day-to-day involves us constantly asking "which top priority is our top priority." If you submit well-written PRs that solve problems concisely, that's an awesome contribution. Otherwise, as much as we'd love to accept your ideas and contributions, we really don't have the bandwidth. That's what forking is for!
## Forking guidelines
@@ -63,7 +63,7 @@ If you discover any security issues, please send an email to security@bsky.app.
## 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.
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 integration can be as seamless as first-party through custom feeds, federated services, clients, and more.
## License (MIT)
+3 -2
View File
@@ -132,7 +132,8 @@ appId: xyz.blueskyweb.app
id: "feedItem-by-bob.test"
- tapOn:
id: "e2eGotoFeeds"
- tapOn: "Good Ppl"
- tapOn:
id: "saved-feed-Good Ppl"
- assertVisible:
id: "feedItem-by-bob.test"
- tapOn:
@@ -168,7 +169,7 @@ appId: xyz.blueskyweb.app
id: "profilePager-selector"
direction: LEFT
- tapOn:
id: "profilePager-selector-5"
id: "profilePager-selector-6"
- tapOn: "Good Ppl"
- tapOn:
+31
View File
@@ -0,0 +1,31 @@
appId: xyz.blueskyweb.app
---
- runScript:
file: ../setupServer.js
env:
SERVER_PATH: "?users&posts&feeds"
- runFlow:
file: ../setupApp.yml
- tapOn:
id: "e2eSignInAlice"
- tapOn: "/sys/debug"
- tapOn:
id: "sharedPrefsTestOpenBtn"
- tapOn:
id: "setStringBtn"
- assertVisible: "Hello"
- tapOn:
id: "removeStringBtn"
- assertVisible: "null"
- tapOn:
id: "setBoolBtn"
- assertVisible: "true"
- tapOn:
id: "setNumberBtn"
- assertVisible: "123"
- tapOn:
id: "addToSetBtn"
- assertVisible: "true"
- tapOn:
id: "removeFromSetBtn"
- assertVisible: "false"
+8
View File
@@ -340,12 +340,14 @@ describe('parseEmbedPlayerFromUrl', () => {
'https://youtube.com/watch?v=videoId',
'https://youtube.com/watch?v=videoId&feature=share',
'https://youtube.com/shorts/videoId',
'https://youtube.com/live/videoId',
'https://m.youtube.com/watch?v=videoId',
'https://music.youtube.com/watch?v=videoId',
'https://youtube.com/shorts/',
'https://youtube.com/',
'https://youtube.com/random',
'https://youtube.com/live/',
'https://twitch.tv/channelName',
'https://www.twitch.tv/channelName',
@@ -475,10 +477,16 @@ describe('parseEmbedPlayerFromUrl', () => {
source: 'youtube',
playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=0',
},
{
type: 'youtube_video',
source: 'youtube',
playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=0',
},
undefined,
undefined,
undefined,
undefined,
{
type: 'twitch_video',
+4 -1
View File
@@ -191,7 +191,7 @@ module.exports = function (config) {
'expo-build-properties',
{
ios: {
deploymentTarget: '14.0',
deploymentTarget: '15.1',
newArchEnabled: false,
},
android: {
@@ -211,6 +211,8 @@ module.exports = function (config) {
sounds: PLATFORM === 'ios' ? ['assets/dm.aiff'] : ['assets/dm.mp3'],
},
],
'expo-video',
'react-native-compressor',
'./plugins/starterPackAppClipExtension/withStarterPackAppClip.js',
'./plugins/withAndroidManifestPlugin.js',
'./plugins/withAndroidManifestFCMIconPlugin.js',
@@ -219,6 +221,7 @@ module.exports = function (config) {
'./plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js',
'./plugins/shareExtension/withShareExtensions.js',
'./plugins/notificationsExtension/withNotificationsExtension.js',
'./plugins/withAppDelegateReferrer.js',
].filter(Boolean),
extra: {
eas: {
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M21 12a1 1 0 0 1-.293.707l-6 6a1 1 0 0 1-1.414-1.414L17.586 13H4a1 1 0 1 1 0-2h13.586l-4.293-4.293a1 1 0 0 1 1.414-1.414l6 6A1 1 0 0 1 21 12Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 284 B

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M20.957 3.043a1 1 0 0 1 0 1.414L16.414 9H20a1 1 0 1 1 0 2h-6a1 1 0 0 1-1-1V4a1 1 0 1 1 2 0v3.586l4.543-4.543a1 1 0 0 1 1.414 0ZM3 14a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 1 1-2 0v-3.586l-4.543 4.543a1 1 0 0 1-1.414-1.414L7.586 15H4a1 1 0 0 1-1-1Z" fill="#000"/></svg>

After

Width:  |  Height:  |  Size: 391 B

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M20.957 3.043a1 1 0 0 1 0 1.414L16.414 9H20a1 1 0 1 1 0 2h-5a2 2 0 0 1-2-2V4a1 1 0 1 1 2 0v3.586l4.543-4.543a1 1 0 0 1 1.414 0ZM3 14a1 1 0 0 1 1-1h5a2 2 0 0 1 2 2v5a1 1 0 1 1-2 0v-3.586l-4.543 4.543a1 1 0 0 1-1.414-1.414L7.586 15H4a1 1 0 0 1-1-1Z" fill="#000"/></svg>

After

Width:  |  Height:  |  Size: 392 B

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M14 5a1 1 0 1 1 0-2h6a1 1 0 0 1 1 1v6a1 1 0 1 1-2 0V6.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L17.586 5H14ZM4 13a1 1 0 0 1 1 1v3.586l4.293-4.293a1 1 0 0 1 1.414 1.414L6.414 19H10a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1Z" fill="#000"/></svg>

After

Width:  |  Height:  |  Size: 369 B

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M13 4a1 1 0 0 1 1-1h5a2 2 0 0 1 2 2v5a1 1 0 1 1-2 0V6.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L17.586 5H14a1 1 0 0 1-1-1Zm-9 9a1 1 0 0 1 1 1v3.586l4.293-4.293a1 1 0 0 1 1.414 1.414L6.414 19H10a1 1 0 1 1 0 2H5a2 2 0 0 1-2-2v-5a1 1 0 0 1 1-1Z" fill="#000"/></svg>

After

Width:  |  Height:  |  Size: 384 B

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm11.543 7.293a1 1 0 0 1 1.414 0 1 1 0 0 0 1.414-1.414 3 3 0 1 0 0 4.242 1 1 0 0 0-1.414-1.414 1 1 0 0 1-1.414-1.414Zm-6 0a1 1 0 0 1 1.414 0 1 1 0 0 0 1.414-1.414 3 3 0 1 0 0 4.243 1 1 0 0 0-1.414-1.415 1 1 0 0 1-1.414-1.414Z" fill="#000"/></svg>

After

Width:  |  Height:  |  Size: 440 B

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 1v14h14V5H5Zm10.957 6.293a1 1 0 1 0 0 1.414 1 1 0 0 1 1.414 1.414 3 3 0 1 1 0-4.242 1 1 0 0 1-1.414 1.414Zm-6.331-.22a1 1 0 1 0 .331 1.634 1 1 0 0 1 1.414 1.414 3 3 0 1 1 0-4.242 1 1 0 0 1-1.414 1.414.994.994 0 0 0-.331-.22Z" fill="#000"/></svg>

After

Width:  |  Height:  |  Size: 443 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M6 2a1 1 0 0 1 1 1v2h11a1 1 0 0 1 1 1v11h2a1 1 0 1 1 0 2h-2v2a1 1 0 1 1-2 0v-2H6a1 1 0 0 1-1-1V7H3a1 1 0 0 1 0-2h2V3a1 1 0 0 1 1-1Zm1 5v10h10V7H7Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 289 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M3.135 12C5.413 16.088 8.77 18 12 18s6.587-1.912 8.865-6C18.587 7.912 15.23 6 12 6c-3.228 0-6.587 1.912-8.865 6ZM12 4c4.24 0 8.339 2.611 10.888 7.54a1 1 0 0 1 0 .92C20.338 17.388 16.24 20 12 20c-4.24 0-8.339-2.611-10.888-7.54a1 1 0 0 1 0-.92C3.662 6.612 7.76 4 12 4Zm0 6a2 2 0 1 0 0 4 2 2 0 0 0 0-4Zm-4 2a4 4 0 1 1 8 0 4 4 0 0 1-8 0Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 476 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12.097 2.53a1 1 0 0 1-.041 1.07 6 6 0 0 0 8.345 8.344 1 1 0 0 1 1.563.908c-.434 5.122-4.728 9.144-9.962 9.144-5.522 0-9.998-4.476-9.998-9.998 0-5.234 4.021-9.528 9.144-9.962a1 1 0 0 1 .949.494ZM9.424 4.424a7.998 7.998 0 1 0 10.152 10.152A8 8 0 0 1 9.424 4.424Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 403 B

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M4 4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V4ZM14 4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1V4Z" fill="#000"/></svg>

After

Width:  |  Height:  |  Size: 247 B

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M4 6a3 3 0 0 1 6 0v12a3 3 0 1 1-6 0V6ZM14 6a3 3 0 1 1 6 0v12a3 3 0 1 1-6 0V6Z" fill="#000"/></svg>

After

Width:  |  Height:  |  Size: 183 B

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V4Zm2 1v14h2V5H6Zm8-1a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1V4Zm2 1v14h2V5h-2Z" fill="#000"/></svg>

After

Width:  |  Height:  |  Size: 315 B

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 6a3 3 0 0 1 6 0v12a3 3 0 1 1-6 0V6Zm3-1a1 1 0 0 0-1 1v12a1 1 0 1 0 2 0V6a1 1 0 0 0-1-1Zm7 1a3 3 0 1 1 6 0v12a3 3 0 1 1-6 0V6Zm3-1a1 1 0 0 0-1 1v12a1 1 0 1 0 2 0V6a1 1 0 0 0-1-1Z" fill="#000"/></svg>

After

Width:  |  Height:  |  Size: 326 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M5 4a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3v16a3 3 0 0 1-3 3H8a3 3 0 0 1-3-3V4Zm3-1a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H8Zm2 2a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2h-2a1 1 0 0 1-1-1Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 334 B

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.514 2.143A1 1 0 0 0 5 3v18a1 1 0 0 0 1.514.858l15-9a1 1 0 0 0 0-1.716l-15-9Z" fill="#000"/></svg>

After

Width:  |  Height:  |  Size: 184 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M9.576 2.534C7.578 1.299 5 2.737 5 5.086v13.828c0 2.35 2.578 3.787 4.576 2.552l11.194-6.914c1.899-1.172 1.899-3.932 0-5.104L9.576 2.534Z"/></svg>

After

Width:  |  Height:  |  Size: 239 B

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M5.507 2.13a1 1 0 0 1 1.008.013l15 9a1 1 0 0 1 0 1.714l-15 9A1 1 0 0 1 5 21V3a1 1 0 0 1 .507-.87ZM7 4.766v14.468L19.056 12 7 4.766Z" fill="#000"/></svg>

After

Width:  |  Height:  |  Size: 276 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M5 5.086C5 2.736 7.578 1.3 9.576 2.534L20.77 9.448c1.899 1.172 1.899 3.932 0 5.104L9.576 21.466C7.578 22.701 5 21.263 5 18.914V5.086Zm3.525-.85A1 1 0 0 0 7 5.085v13.828a1 1 0 0 0 1.525.85l11.194-6.913a1 1 0 0 0 0-1.702L8.525 4.235Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 374 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 1v2h2V5H5Zm4 0v6h6V5H9Zm8 0v2h2V5h-2Zm2 4h-2v2h2V9Zm0 4h-2v2.444h2V13Zm0 4.444h-2V19h2v-1.556ZM15 19v-6H9v6h6Zm-8 0v-2H5v2h2Zm-2-4h2v-2H5v2Zm0-4h2V9H5v2Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 370 B

+3 -2
View File
@@ -10,11 +10,12 @@
],
"rules": {
"simple-import-sort/imports": "warn",
"simple-import-sort/exports": "warn"
"simple-import-sort/exports": "warn",
'no-else-return': 'off'
},
"parserOptions": {
"sourceType": "module",
"ecmaVersion": "latest",
"project": "./bskyembed/tsconfig.json"
}
}
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#fff" d="M9.576 2.534C7.578 1.299 5 2.737 5 5.086v13.828c0 2.35 2.578 3.787 4.576 2.552l11.194-6.914c1.899-1.172 1.899-3.932 0-5.104L9.576 2.534Z"/></svg>

After

Width:  |  Height:  |  Size: 239 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><defs><linearGradient x1="0" y1="0" x2="100%" y2="0" gradientTransform="rotate(45)" id="sky_V5w1FF_xb91wVQ_1euhBX"><stop offset="0" stop-color="#0A7AFF"></stop><stop offset="1" stop-color="#59B9FF"></stop></linearGradient></defs><path fill="url(#sky_V5w1FF_xb91wVQ_1euhBX)" fill-rule="evenodd" clip-rule="evenodd" d="M11.26 5.227 5.02 6.899c-.734.197-1.17.95-.973 1.685l1.672 6.24c.197.734.951 1.17 1.685.973l6.24-1.672c.734-.197 1.17-.951.973-1.685L12.945 6.2a1.375 1.375 0 0 0-1.685-.973Zm-6.566.459a2.632 2.632 0 0 0-1.86 3.223l1.672 6.24a2.632 2.632 0 0 0 3.223 1.861l6.24-1.672a2.631 2.631 0 0 0 1.861-3.223l-1.672-6.24a2.632 2.632 0 0 0-3.223-1.861l-6.24 1.672Z"></path><path fill="url(#sky_V5w1FF_xb91wVQ_1euhBX)" fill-rule="evenodd" clip-rule="evenodd" d="M15.138 18.411a4.606 4.606 0 1 0 0-9.211 4.606 4.606 0 0 0 0 9.211Zm0 1.257a5.862 5.862 0 1 0 0-11.724 5.862 5.862 0 0 0 0 11.724Z"></path></svg>

After

Width:  |  Height:  |  Size: 982 B

+2 -2
View File
@@ -9,7 +9,7 @@
"lint": "eslint --cache --ext .js,.jsx,.ts,.tsx src"
},
"dependencies": {
"@atproto/api": "^0.12.2",
"@atproto/api": "0.13.6",
"@preact/preset-vite": "^2.8.2",
"@vitejs/plugin-legacy": "^5.3.2",
"preact": "^10.4.8",
@@ -22,7 +22,7 @@
"eslint-plugin-simple-import-sort": "^12.0.0",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.3",
"typescript": "^4.0.5",
"typescript": "^5.5.4",
"vite": "^5.2.8",
"vite-tsconfig-paths": "^4.3.2"
}
+113 -17
View File
@@ -3,15 +3,20 @@ import {
AppBskyEmbedImages,
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
AppBskyEmbedVideo,
AppBskyFeedDefs,
AppBskyFeedPost,
AppBskyGraphDefs,
AppBskyGraphStarterpack,
AppBskyLabelerDefs,
AtUri,
} from '@atproto/api'
import {ComponentChildren, h} from 'preact'
import {useMemo} from 'preact/hooks'
import infoIcon from '../../assets/circleInfo_stroke2_corner0_rounded.svg'
import playIcon from '../../assets/play_filled_corner2_rounded.svg'
import starterPackIcon from '../../assets/starterPack.svg'
import {CONTENT_LABELS, labelsToInfo} from '../labels'
import {getRkey} from '../utils'
import {Link} from './link'
@@ -105,7 +110,7 @@ export function Embed({
// Case 3.2: List
if (AppBskyGraphDefs.isListView(record)) {
return (
<GenericWithImage
<GenericWithImageEmbed
image={record.avatar}
title={record.name}
href={`/profile/${record.creator.did}/lists/${getRkey(record)}`}
@@ -122,7 +127,7 @@ export function Embed({
// Case 3.3: Feed
if (AppBskyFeedDefs.isGeneratorView(record)) {
return (
<GenericWithImage
<GenericWithImageEmbed
image={record.avatar}
title={record.displayName}
href={`/profile/${record.creator.did}/feed/${getRkey(record)}`}
@@ -134,31 +139,35 @@ export function Embed({
// Case 3.4: Labeler
if (AppBskyLabelerDefs.isLabelerView(record)) {
return (
<GenericWithImage
image={record.creator.avatar}
title={record.creator.displayName || record.creator.handle}
href={`/profile/${record.creator.did}`}
subtitle="Labeler"
description={`Liked by ${record.likeCount ?? 0} users`}
/>
)
// Embed type does not exist in the app, so show nothing
return null
}
// Case 3.5: Post not found
// Case 3.5: Starter pack
if (AppBskyGraphDefs.isStarterPackViewBasic(record)) {
return <StarterPackEmbed content={record} />
}
// Case 3.6: Post not found
if (AppBskyEmbedRecord.isViewNotFound(record)) {
return <Info>Quoted post not found, it may have been deleted.</Info>
}
// Case 3.6: Post blocked
// Case 3.7: Post blocked
if (AppBskyEmbedRecord.isViewBlocked(record)) {
return <Info>The quoted post is blocked.</Info>
}
throw new Error('Unknown embed type')
// Unknown embed type
return null
}
// Case 4: Record with media
// Case 4: Video
if (AppBskyEmbedVideo.isView(content)) {
return <VideoEmbed content={content} />
}
// Case 5: Record with media
if (
AppBskyEmbedRecordWithMedia.isView(content) &&
AppBskyEmbedRecord.isViewRecord(content.record.record)
@@ -182,7 +191,8 @@ export function Embed({
)
}
throw new Error('Unsupported embed type')
// Unknown embed type
return null
} catch (err) {
return (
<Info>{err instanceof Error ? err.message : 'An error occurred'}</Info>
@@ -314,7 +324,7 @@ function ExternalEmbed({
)
}
function GenericWithImage({
function GenericWithImageEmbed({
title,
subtitle,
href,
@@ -350,3 +360,89 @@ function GenericWithImage({
</Link>
)
}
// just the thumbnail and a play button
function VideoEmbed({content}: {content: AppBskyEmbedVideo.View}) {
let aspectRatio = 1
if (content.aspectRatio) {
const {width, height} = content.aspectRatio
aspectRatio = clamp(width / height, 1 / 1, 3 / 1)
}
return (
<div
className="w-full overflow-hidden rounded-lg aspect-square"
style={{aspectRatio: `${aspectRatio} / 1`}}>
<img
src={content.thumbnail}
alt={content.alt}
className="object-cover size-full"
/>
<div className="size-24 absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-black/50 flex items-center justify-center">
<img src={playIcon} className="object-cover size-3/5" />
</div>
</div>
)
}
function StarterPackEmbed({
content,
}: {
content: AppBskyGraphDefs.StarterPackViewBasic
}) {
if (!AppBskyGraphStarterpack.isRecord(content.record)) {
return null
}
const starterPackHref = getStarterPackHref(content)
const imageUri = getStarterPackImage(content)
return (
<Link
href={starterPackHref}
className="w-full rounded-lg overflow-hidden border flex flex-col items-stretch">
<img src={imageUri} className="aspect-[1.91/1] object-cover" />
<div className="py-3 px-4">
<div className="flex space-x-2 items-center">
<img src={starterPackIcon} className="w-10 h-10" />
<div>
<p className="font-semibold leading-[21px]">
{content.record.name}
</p>
<p className="text-sm text-textLight line-clamp-2 leading-[18px]">
Starter pack by{' '}
{content.creator.displayName || `@${content.creator.handle}`}
</p>
</div>
</div>
{content.record.description && (
<p className="text-sm mt-1">{content.record.description}</p>
)}
{!!content.joinedAllTimeCount && content.joinedAllTimeCount > 50 && (
<p className="text-sm font-semibold text-textLight mt-1">
{content.joinedAllTimeCount} users have joined!
</p>
)}
</div>
</Link>
)
}
// from #/lib/strings/starter-pack.ts
function getStarterPackImage(starterPack: AppBskyGraphDefs.StarterPackView) {
const rkey = new AtUri(starterPack.uri).rkey
return `https://ogcard.cdn.bsky.app/start/${starterPack.creator.did}/${rkey}`
}
function getStarterPackHref(
starterPack: AppBskyGraphDefs.StarterPackViewBasic,
) {
const rkey = new AtUri(starterPack.uri).rkey
const handleOrDid = starterPack.creator.handle || starterPack.creator.did
return `/starter-pack/${handleOrDid}/${rkey}`
}
function clamp(num: number, min: number, max: number) {
return Math.max(min, Math.min(num, max))
}
+8 -5
View File
@@ -11,7 +11,7 @@ import likeIcon from '../../assets/heart2_filled_stroke2_corner0_rounded.svg'
import logo from '../../assets/logo.svg'
import repostIcon from '../../assets/repost_stroke2_corner2_rounded.svg'
import {CONTENT_LABELS} from '../labels'
import {getRkey, niceDate} from '../utils'
import {getRkey, niceDate, prettyNumber} from '../utils'
import {Container} from './container'
import {Embed} from './embed'
import {Link} from './link'
@@ -78,7 +78,7 @@ export function Post({thread}: Props) {
<div className="flex items-center gap-2 cursor-pointer">
<img src={likeIcon} className="w-5 h-5" />
<p className="font-bold text-neutral-500 mb-px">
{post.likeCount}
{prettyNumber(post.likeCount)}
</p>
</div>
)}
@@ -86,7 +86,7 @@ export function Post({thread}: Props) {
<div className="flex items-center gap-2 cursor-pointer">
<img src={repostIcon} className="w-5 h-5" />
<p className="font-bold text-neutral-500 mb-px">
{post.repostCount}
{prettyNumber(post.repostCount)}
</p>
</div>
)}
@@ -97,7 +97,7 @@ export function Post({thread}: Props) {
<div className="flex-1" />
<p className="cursor-pointer text-brand font-bold hover:underline hidden min-[450px]:inline">
{post.replyCount
? `Read ${post.replyCount} ${
? `Read ${prettyNumber(post.replyCount)} ${
post.replyCount > 1 ? 'replies' : 'reply'
} on Bluesky`
: `View on Bluesky`}
@@ -132,7 +132,10 @@ function PostContent({record}: {record: AppBskyFeedPost.Record | null}) {
key={counter}
href={segment.link.uri}
className="text-blue-400 hover:underline"
disableTracking={!segment.link.uri.startsWith('https://bsky.app')}>
disableTracking={
!segment.link.uri.startsWith('https://bsky.app') &&
!segment.link.uri.startsWith('https://go.bsky.app')
}>
{segment.text}
</Link>,
)
+2 -2
View File
@@ -1,6 +1,6 @@
import '../index.css'
import {AppBskyFeedDefs, BskyAgent} from '@atproto/api'
import {AppBskyFeedDefs, AtpAgent} from '@atproto/api'
import {h, render} from 'preact'
import logo from '../../assets/logo.svg'
@@ -12,7 +12,7 @@ import {getRkey} from '../utils'
const root = document.getElementById('app')
if (!root) throw new Error('No root element')
const agent = new BskyAgent({
const agent = new AtpAgent({
service: 'https://public.api.bsky.app',
})
+10
View File
@@ -16,3 +16,13 @@ export function getRkey({uri}: {uri: string}): string {
const at = new AtUri(uri)
return at.rkey
}
const formatter = new Intl.NumberFormat('en-US', {
notation: 'compact',
maximumFractionDigits: 1,
roundingMode: 'trunc',
})
export function prettyNumber(number: number) {
return formatter.format(number)
}
+1 -1
View File
@@ -20,5 +20,5 @@
"jsxFragmentFactory": "Fragment",
"downlevelIteration": true
},
"include": ["src"]
"include": ["src", "vite.config.ts"]
}
+1 -1
View File
@@ -6,5 +6,5 @@
"strict": true,
"outDir": "dist"
},
"include": ["snippet"],
"include": ["snippet"]
}
+50 -24
View File
@@ -20,15 +20,16 @@
"@jridgewell/gen-mapping" "^0.3.5"
"@jridgewell/trace-mapping" "^0.3.24"
"@atproto/api@^0.12.2":
version "0.12.2"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.2.tgz#5df6d4f60dea0395c84fdebd9e81a7e853edf130"
integrity sha512-UVzCiDZH2j0wrr/O8nb1edD5cYLVqB5iujueXUCbHS3rAwIxgmyLtA3Hzm2QYsGPo/+xsIg1fNvpq9rNT6KWUA==
"@atproto/api@0.13.6":
version "0.13.6"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.13.6.tgz#2500e9d7143e6718089632300c42ce50149f8cd5"
integrity sha512-58emFFZhqY8nVWD3xFWK0yYqAmJ2un+NaTtZxBbRo00mGq1rz9VXTpVmfoHFcuXL1hoDQN3WyJfsub8r6xGOgg==
dependencies:
"@atproto/common-web" "^0.3.0"
"@atproto/lexicon" "^0.4.0"
"@atproto/lexicon" "^0.4.1"
"@atproto/syntax" "^0.3.0"
"@atproto/xrpc" "^0.5.0"
"@atproto/xrpc" "^0.6.1"
await-lock "^2.2.2"
multiformats "^9.9.0"
tlds "^1.234.0"
@@ -42,29 +43,29 @@
uint8arrays "3.0.0"
zod "^3.21.4"
"@atproto/lexicon@^0.4.0":
version "0.4.0"
resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.0.tgz#63e8829945d80c25524882caa8ed27b1151cc576"
integrity sha512-RvCBKdSI4M8qWm5uTNz1z3R2yIvIhmOsMuleOj8YR6BwRD+QbtUBy3l+xQ7iXf4M5fdfJFxaUNa6Ty0iRwdKqQ==
"@atproto/lexicon@^0.4.1":
version "0.4.1"
resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.4.1.tgz#19155210570a2fafbcc7d4f655d9b813948e72a0"
integrity sha512-bzyr+/VHXLQWbumViX5L7h1NKQObfs8Z+XZJl43OUK8nYFUI4e/sW1IZKRNfw7Wvi5YVNK+J+yP3DWIBZhkCYA==
dependencies:
"@atproto/common-web" "^0.3.0"
"@atproto/syntax" "^0.3.0"
iso-datestring-validator "^2.2.2"
multiformats "^9.9.0"
zod "^3.21.4"
zod "^3.23.8"
"@atproto/syntax@^0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.3.0.tgz#fafa2dbea9add37253005cb663e7373e05e618b3"
integrity sha512-Weq0ZBxffGHDXHl9U7BQc2BFJi/e23AL+k+i5+D9hUq/bzT4yjGsrCejkjq0xt82xXDjmhhvQSZ0LqxyZ5woxA==
"@atproto/xrpc@^0.5.0":
version "0.5.0"
resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.5.0.tgz#dacbfd8f7b13f0ab5bd56f8fdd4b460e132a6032"
integrity sha512-swu+wyOLvYW4l3n+VAuJbHcPcES+tin2Lsrp8Bw5aIXIICiuFn1YMFlwK9JwVUzTH21Py1s1nHEjr4CJeElJog==
"@atproto/xrpc@^0.6.1":
version "0.6.1"
resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.6.1.tgz#dcd1315c8c60eef5af2db7fa4e35a38ebc6d79d5"
integrity sha512-Zy5ydXEdk6sY7FDUZcEVfCL1jvbL4tXu5CcdPqbEaW6LQtk9GLds/DK1bCX9kswTGaBC88EMuqQMfkxOhp2t4A==
dependencies:
"@atproto/lexicon" "^0.4.0"
zod "^3.21.4"
"@atproto/lexicon" "^0.4.1"
zod "^3.23.8"
"@babel/code-frame@^7.23.5", "@babel/code-frame@^7.24.1", "@babel/code-frame@^7.24.2":
version "7.24.2"
@@ -1710,6 +1711,11 @@ available-typed-arrays@^1.0.7:
dependencies:
possible-typed-array-names "^1.0.0"
await-lock@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/await-lock/-/await-lock-2.2.2.tgz#a95a9b269bfd2f69d22b17a321686f551152bcef"
integrity sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==
babel-plugin-polyfill-corejs2@^0.4.10:
version "0.4.10"
resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.10.tgz#276f41710b03a64f6467433cab72cbc2653c38b1"
@@ -3730,8 +3736,16 @@ stack-trace@^1.0.0-pre2:
resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-1.0.0-pre2.tgz#46a83a79f1b287807e9aaafc6a5dd8bcde626f9c"
integrity sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A==
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0:
name string-width-cjs
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -3795,7 +3809,14 @@ string.prototype.trimstart@^1.0.8:
define-properties "^1.2.1"
es-object-atoms "^1.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -4003,10 +4024,10 @@ typed-array-length@^1.0.6:
is-typed-array "^1.1.13"
possible-typed-array-names "^1.0.0"
typescript@^4.0.5:
version "4.9.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
typescript@^5.5.4:
version "5.5.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba"
integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==
uint8arrays@3.0.0:
version "3.0.0"
@@ -4192,3 +4213,8 @@ zod@^3.21.4:
version "3.22.4"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff"
integrity sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==
zod@^3.23.8:
version "3.23.8"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d"
integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==
+17 -3
View File
@@ -11,6 +11,7 @@ export default function (ctx: AppContext, app: Express) {
'/:linkId',
handler(async (req, res) => {
const linkId = req.params.linkId
const contentType = req.accepts(['html', 'json'])
assert(
typeof linkId === 'string',
'express guarantees id parameter is a string',
@@ -21,9 +22,19 @@ export default function (ctx: AppContext, app: Express) {
.where('id', '=', linkId)
.executeTakeFirst()
if (!found) {
// potentially broken or mistyped link— send user to the app
res.setHeader('Location', `https://${ctx.cfg.service.appHostname}`)
// potentially broken or mistyped link
res.setHeader('Cache-Control', 'no-store')
if (contentType === 'json') {
return res
.status(404)
.json({
error: 'NotFound',
message: 'Link not found',
})
.end()
}
// send the user to the app
res.setHeader('Location', `https://${ctx.cfg.service.appHostname}`)
return res.status(302).end()
}
// build url from original url in order to preserve query params
@@ -32,8 +43,11 @@ export default function (ctx: AppContext, app: Express) {
`https://${ctx.cfg.service.appHostname}`,
)
url.pathname = found.path
res.setHeader('Location', url.href)
res.setHeader('Cache-Control', `max-age=${(7 * DAY) / SECOND}`)
if (contentType === 'json') {
return res.json({url: url.href}).end()
}
res.setHeader('Location', url.href)
return res.status(301).end()
}),
)
+39
View File
@@ -56,6 +56,26 @@ describe('link service', async () => {
)
})
it('returns json object with url when requested', async () => {
const link = await getLink('/start/did:example:carol/zzz/')
const [status, json] = await getJsonRedirect(link)
assert.strictEqual(status, 200)
assert(json.url)
const url = new URL(json.url)
assert.strictEqual(url.pathname, '/start/did:example:carol/zzz')
})
it('returns 404 for unknown link when requesting json', async () => {
const [status, json] = await getJsonRedirect(
'https://test.bsky.link/unknown',
)
assert(json.error)
assert(json.message)
assert.strictEqual(status, 404)
assert.strictEqual(json.error, 'NotFound')
assert.strictEqual(json.message, 'Link not found')
})
async function getRedirect(link: string): Promise<[number, string]> {
const url = new URL(link)
const base = new URL(baseUrl)
@@ -70,6 +90,25 @@ describe('link service', async () => {
return [res.status, res.headers.get('location') ?? '']
}
async function getJsonRedirect(
link: string,
): Promise<[number, {url?: string; error?: string; message?: string}]> {
const url = new URL(link)
const base = new URL(baseUrl)
url.protocol = base.protocol
url.host = base.host
const res = await fetch(url, {
redirect: 'manual',
headers: {accept: 'application/json,text/html'},
})
assert(
res.headers.get('content-type')?.startsWith('application/json'),
'content type was not json',
)
const json = await res.json()
return [res.status, json]
}
async function getLink(path: string): Promise<string> {
const res = await fetch(new URL('/link', baseUrl), {
method: 'post',
+6 -2
View File
@@ -5,7 +5,9 @@
"main": "src/index.ts",
"scripts": {
"start": "node --loader ts-node/esm ./src/bin.ts",
"build": "tsc && cp -r src/assets dist/assets"
"dev": "node --watch-path ./src --loader ts-node/esm ./src/bin.ts",
"build": "tsc && cp -r src/assets dist/",
"install-fonts": "node --loader ts-node/esm scripts/install-fonts.ts"
},
"dependencies": {
"@atproto/api": "0.12.19-next.0",
@@ -15,10 +17,12 @@
"http-terminator": "^3.2.0",
"pino": "^9.2.0",
"react": "^18.3.1",
"satori": "^0.10.13"
"satori": "^0.10.13",
"twemoji": "^14.0.2"
},
"devDependencies": {
"@types/node": "^20.14.3",
"ts-node": "^10.9.2",
"typescript": "^5.4.5"
}
}
+40
View File
@@ -0,0 +1,40 @@
import {writeFile} from 'node:fs/promises'
import * as path from 'node:path'
import {fileURLToPath} from 'node:url'
const __DIRNAME = path.dirname(fileURLToPath(import.meta.url))
const FONTS = [
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-jp@5.0/japanese-700-normal.ttf',
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-tc@5.0/chinese-traditional-700-normal.ttf',
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-sc@5.0/chinese-simplified-700-normal.ttf',
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-hk@5.0/chinese-hongkong-700-normal.ttf',
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-kr@5.0/korean-700-normal.ttf',
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-thai@5.0/thai-700-normal.ttf',
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-arabic@5.0/arabic-700-normal.ttf',
'https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-hebrew@5.0/hebrew-700-normal.ttf',
]
async function main() {
await Promise.all(
FONTS.map(async urlStr => {
const url = new URL(urlStr)
const res = await fetch(url)
const font = await res.arrayBuffer()
const filename = url.pathname
.split('/')
.slice(-2)
.join('/')
.replace(/@[\d.]+\//, '-')
if (!res.ok) {
throw new Error(`HTTP ${res.status}: fetching failed for ${filename}`)
}
await writeFile(
path.join(__DIRNAME, '..', 'src', 'assets', 'fonts', filename),
Buffer.from(font),
)
}),
)
}
main()
+4 -1
View File
@@ -43,6 +43,7 @@ export function StarterPack(props: {
} else {
imagesAcross.push(...imagesExceptCreator.slice(0, 7))
}
const isLongTitle = record ? record.name.length > 30 : false
return (
<div
style={{
@@ -130,7 +131,9 @@ export function StarterPack(props: {
<div
style={{
padding: '75px 30px 0px',
fontSize: 65,
fontSize: isLongTitle ? 55 : 65,
display: 'flex',
textAlign: 'center',
}}>
{record?.name || 'Starter Pack'}
</div>
+11 -9
View File
@@ -1,8 +1,8 @@
import {readFileSync} from 'node:fs'
import {readdirSync, readFileSync} from 'node:fs'
import * as path from 'node:path'
import {fileURLToPath} from 'node:url'
import {AtpAgent} from '@atproto/api'
import * as path from 'path'
import {fileURLToPath} from 'url'
import {Config} from './config.js'
@@ -28,12 +28,14 @@ export class AppContext {
static async fromConfig(cfg: Config, overrides?: Partial<AppContextOptions>) {
const appviewAgent = new AtpAgent({service: cfg.service.appviewUrl})
const fonts = [
{
name: 'Inter',
data: readFileSync(path.join(__DIRNAME, 'assets', 'Inter-Bold.ttf')),
},
]
const fontDirectory = path.join(__DIRNAME, 'assets', 'fonts')
const fontFiles = readdirSync(fontDirectory)
const fonts = fontFiles.map(file => {
return {
name: path.basename(file, path.extname(file)),
data: readFileSync(path.join(fontDirectory, file)),
}
})
return new AppContext({
cfg,
appviewAgent,
+1
View File
@@ -1,3 +1,4 @@
import {subsystemLogger} from '@atproto/common'
export const httpLogger = subsystemLogger('bskyogcard')
export const renderLogger = subsystemLogger('bskyogcard:render')
+6
View File
@@ -13,6 +13,7 @@ import {
} from '../components/StarterPack.js'
import {AppContext} from '../context.js'
import {httpLogger} from '../logger.js'
import {loadEmojiAsSvg} from '../util.js'
import {handler, originVerifyMiddleware} from './util.js'
export default function (ctx: AppContext, app: Express) {
@@ -65,6 +66,11 @@ export default function (ctx: AppContext, app: Express) {
fonts: ctx.fonts,
height: STARTERPACK_HEIGHT,
width: STARTERPACK_WIDTH,
loadAdditionalAsset: async (code, text) => {
if (code === 'emoji') {
return await loadEmojiAsSvg(text)
}
},
},
)
const output = await resvg.renderAsync(svg)
+37
View File
@@ -0,0 +1,37 @@
import twemoji from 'twemoji'
import {renderLogger} from './logger.js'
const U200D = String.fromCharCode(0x200d)
const UFE0F_REGEXP = /\uFE0F/g
export async function loadEmojiAsSvg(chars: string) {
const cached = emojiCache.get(chars)
if (cached) return cached
const iconCode = twemoji.convert.toCodePoint(
chars.indexOf(U200D) < 0 ? chars.replace(UFE0F_REGEXP, '') : chars,
)
const res = await fetch(getEmojiUrl(iconCode))
const body = await res.arrayBuffer()
if (!res.ok) {
renderLogger.warn(
{status: res.status, err: Buffer.from(body).toString()},
'could not fetch emoji',
)
return
}
const svg =
'data:image/svg+xml;base64,' + Buffer.from(body).toString('base64')
emojiCache.set(chars, svg)
return svg
}
const emojiCache = new Map<string, string>()
function getEmojiUrl(code: string) {
return (
'https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/svg/' +
code.toLowerCase() +
'.svg'
)
}
+303 -147
View File
File diff suppressed because it is too large Load Diff
+10 -3
View File
@@ -41,10 +41,10 @@ func run(args []string) {
EnvVars: []string{"ATP_APPVIEW_HOST", "ATP_PDS_HOST"},
},
&cli.StringFlag{
Name: "ogcard-host",
Usage: "scheme, hostname, and port of ogcard service",
Name: "ogcard-host",
Usage: "scheme, hostname, and port of ogcard service",
Required: false,
EnvVars: []string{"OGCARD_HOST"},
EnvVars: []string{"OGCARD_HOST"},
},
&cli.StringFlag{
Name: "http-address",
@@ -67,6 +67,13 @@ func run(args []string) {
Required: false,
EnvVars: []string{"DEBUG"},
},
&cli.StringFlag{
Name: "basic-auth-password",
Usage: "optional password to restrict access to web interface",
Required: false,
Value: "",
EnvVars: []string{"BASIC_AUTH_PASSWORD"},
},
},
},
}
+17
View File
@@ -2,6 +2,7 @@ package main
import (
"context"
"crypto/subtle"
"errors"
"fmt"
"io/fs"
@@ -48,6 +49,7 @@ func serve(cctx *cli.Context) error {
appviewHost := cctx.String("appview-host")
ogcardHost := cctx.String("ogcard-host")
linkHost := cctx.String("link-host")
basicAuthPassword := cctx.String("basic-auth-password")
// Echo
e := echo.New()
@@ -140,6 +142,18 @@ func serve(cctx *cli.Context) error {
},
}))
// optional password gating of entire web interface
if basicAuthPassword != "" {
e.Use(middleware.BasicAuth(func(username, password string, c echo.Context) (bool, error) {
// Be careful to use constant time comparison to prevent timing attacks
if subtle.ConstantTimeCompare([]byte(username), []byte("admin")) == 1 &&
subtle.ConstantTimeCompare([]byte(password), []byte(basicAuthPassword)) == 1 {
return true, nil
}
return false, nil
}))
}
// redirect trailing slash to non-trailing slash.
// all of our current endpoints have no trailing slash.
e.Use(middleware.RemoveTrailingSlashWithConfig(middleware.TrailingSlashConfig{
@@ -197,6 +211,7 @@ func serve(cctx *cli.Context) error {
e.GET("/search", server.WebGeneric)
e.GET("/feeds", server.WebGeneric)
e.GET("/notifications", server.WebGeneric)
e.GET("/notifications/settings", server.WebGeneric)
e.GET("/lists", server.WebGeneric)
e.GET("/moderation", server.WebGeneric)
e.GET("/moderation/modlists", server.WebGeneric)
@@ -210,6 +225,7 @@ func serve(cctx *cli.Context) error {
e.GET("/settings/threads", server.WebGeneric)
e.GET("/settings/external-embeds", server.WebGeneric)
e.GET("/settings/accessibility", server.WebGeneric)
e.GET("/settings/appearance", server.WebGeneric)
e.GET("/sys/debug", server.WebGeneric)
e.GET("/sys/debug-mod", server.WebGeneric)
e.GET("/sys/log", server.WebGeneric)
@@ -239,6 +255,7 @@ func serve(cctx *cli.Context) error {
e.GET("/profile/:handleOrDID/post/:rkey", server.WebPost)
e.GET("/profile/:handleOrDID/post/:rkey/liked-by", server.WebGeneric)
e.GET("/profile/:handleOrDID/post/:rkey/reposted-by", server.WebGeneric)
e.GET("/profile/:handleOrDID/post/:rkey/quotes", server.WebGeneric)
// starter packs
e.GET("/starter-pack/:handleOrDID/:rkey", server.WebStarterPack)
+5
View File
@@ -253,6 +253,11 @@
from { opacity: 1; }
to { opacity: 0; }
}
.force-no-clicks > *,
.force-no-clicks * {
pointer-events: none !important;
}
</style>
</style>
{% include "scripts.html" %}
+3 -6
View File
@@ -1,14 +1,11 @@
import 'react-native-gesture-handler' // must be first
import {LogBox} from 'react-native'
import '#/platform/polyfills'
import {IS_TEST} from '#/env'
import {LogBox} from 'react-native'
import {registerRootComponent} from 'expo'
import {doPolyfill} from '#/lib/api/api-polyfill'
import App from '#/App'
doPolyfill()
import {IS_TEST} from '#/env'
if (IS_TEST) {
LogBox.ignoreAllLogs() // suppress all logs in tests
+2 -3
View File
@@ -1,9 +1,8 @@
import '#/platform/markBundleStartTime'
import '#/platform/polyfills'
import {registerRootComponent} from 'expo'
import {doPolyfill} from '#/lib/api/api-polyfill'
import App from '#/App'
doPolyfill()
registerRootComponent(App)
+13
View File
@@ -95,3 +95,16 @@ jest.mock('expo-application', () => ({
nativeApplicationVersion: '1.0.0',
nativeBuildVersion: '1',
}))
jest.mock('expo-modules-core', () => ({
requireNativeModule: jest.fn().mockImplementation(moduleName => {
if (moduleName === 'ExpoPlatformInfo') {
return {
getIsReducedMotionEnabled: () => false,
}
}
}),
requireNativeViewManager: jest.fn().mockImplementation(moduleName => {
return () => null
}),
}))
+27 -26
View File
@@ -79,32 +79,33 @@ export async function createServer(
plc: {port: port2},
})
// DISABLED - looks like dev-env added this and now it conflicts
// add the test mod authority
const agent = new BskyAgent({service: pdsUrl})
const res = await agent.api.com.atproto.server.createAccount({
email: 'mod-authority@test.com',
handle: 'mod-authority.test',
password: 'hunter2',
})
agent.api.setHeader('Authorization', `Bearer ${res.data.accessJwt}`)
await agent.api.app.bsky.actor.profile.create(
{repo: res.data.did},
{
displayName: 'Dev-env Moderation',
description: `The pretend version of mod.bsky.app`,
},
)
// const agent = new BskyAgent({service: pdsUrl})
// const res = await agent.api.com.atproto.server.createAccount({
// email: 'mod-authority@test.com',
// handle: 'mod-authority.test',
// password: 'hunter2',
// })
// agent.api.setHeader('Authorization', `Bearer ${res.data.accessJwt}`)
// await agent.api.app.bsky.actor.profile.create(
// {repo: res.data.did},
// {
// displayName: 'Dev-env Moderation',
// description: `The pretend version of mod.bsky.app`,
// },
// )
await agent.api.app.bsky.labeler.service.create(
{repo: res.data.did, rkey: 'self'},
{
policies: {
labelValues: ['!hide', '!warn'],
labelValueDefinitions: [],
},
createdAt: new Date().toISOString(),
},
)
// await agent.api.app.bsky.labeler.service.create(
// {repo: res.data.did, rkey: 'self'},
// {
// policies: {
// labelValues: ['!hide', '!warn'],
// labelValueDefinitions: [],
// },
// createdAt: new Date().toISOString(),
// },
// )
const pic = fs.readFileSync(
path.join(__dirname, '..', 'assets', 'default-avatar.png'),
@@ -156,7 +157,7 @@ class Mocker {
}
async createUser(name: string) {
const agent = new BskyAgent({service: this.agent.service})
const agent = new BskyAgent({service: this.service})
const inviteRes = await agent.api.com.atproto.server.createInviteCode(
{useCount: 1},
@@ -332,7 +333,7 @@ class Mocker {
}
async createInvite(forAccount: string) {
const agent = new BskyAgent({service: this.agent.service})
const agent = new BskyAgent({service: this.service})
await agent.api.com.atproto.server.createInviteCode(
{useCount: 1, forAccount},
{
+9 -9
View File
@@ -13,43 +13,43 @@ class NotificationService: UNNotificationServiceExtension {
contentHandler(request.content)
return
}
if reason == "chat-message" {
mutateWithChatMessage(bestAttempt)
} else {
mutateWithBadge(bestAttempt)
}
contentHandler(bestAttempt)
}
override func serviceExtensionTimeWillExpire() {
// If for some reason the alloted time expires, we don't actually want to display a notification
}
func createCopy(_ content: UNNotificationContent) -> UNMutableNotificationContent? {
return content.mutableCopy() as? UNMutableNotificationContent
}
func mutateWithBadge(_ content: UNMutableNotificationContent) {
var count = prefs?.integer(forKey: "badgeCount") ?? 0
count += 1
// Set the new badge number for the notification, then store that value for using later
content.badge = NSNumber(value: count)
prefs?.setValue(count, forKey: "badgeCount")
}
func mutateWithChatMessage(_ content: UNMutableNotificationContent) {
if self.prefs?.bool(forKey: "playSoundChat") == true {
mutateWithDmSound(content)
}
}
func mutateWithDefaultSound(_ content: UNMutableNotificationContent) {
content.sound = UNNotificationSound.default
}
func mutateWithDmSound(_ content: UNMutableNotificationContent) {
content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "dm.aiff"))
}
@@ -30,12 +30,11 @@ class ShareViewController: UIViewController {
}
}
private func handleText(item: NSItemProvider) async -> Void {
private func handleText(item: NSItemProvider) async {
do {
if let data = try await item.loadItem(forTypeIdentifier: "public.text") as? String {
if let encoded = data.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)")
{
let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)") {
_ = self.openURL(url)
}
}
@@ -45,12 +44,11 @@ class ShareViewController: UIViewController {
}
}
private func handleUrl(item: NSItemProvider) async -> Void {
private func handleUrl(item: NSItemProvider) async {
do {
if let data = try await item.loadItem(forTypeIdentifier: "public.url") as? URL {
if let encoded = data.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)")
{
let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)") {
_ = self.openURL(url)
}
}
@@ -60,7 +58,7 @@ class ShareViewController: UIViewController {
}
}
private func handleImages(items: [NSItemProvider]) async -> Void {
private func handleImages(items: [NSItemProvider]) async {
let firstFourItems: [NSItemProvider]
if items.count < 4 {
firstFourItems = items
@@ -72,7 +70,7 @@ class ShareViewController: UIViewController {
var imageUris = ""
for (index, item) in firstFourItems.enumerated() {
var imageUriInfo: String? = nil
var imageUriInfo: String?
do {
if let dataUri = try await item.loadItem(forTypeIdentifier: "public.image") as? URL {
@@ -100,8 +98,7 @@ class ShareViewController: UIViewController {
if valid,
let encoded = imageUris.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
let url = URL(string: "\(self.appScheme)://intent/compose?imageUris=\(encoded)")
{
let url = URL(string: "\(self.appScheme)://intent/compose?imageUris=\(encoded)") {
_ = self.openURL(url)
}
@@ -119,13 +116,11 @@ class ShareViewController: UIViewController {
// extension does.
if let dir = FileManager()
.containerURL(
forSecurityApplicationGroupIdentifier: "group.app.bsky")
{
forSecurityApplicationGroupIdentifier: "group.app.bsky") {
let filePath = "\(dir.absoluteString)\(ProcessInfo.processInfo.globallyUniqueString).jpeg"
if let newUri = URL(string: filePath),
let jpegData = image.jpegData(compressionQuality: 1)
{
let jpegData = image.jpegData(compressionQuality: 1) {
try jpegData.write(to: newUri)
return "\(newUri.absoluteString)|\(image.size.width)|\(image.size.height)"
}
@@ -136,7 +131,7 @@ class ShareViewController: UIViewController {
}
}
private func completeRequest() -> Void {
private func completeRequest() {
self.extensionContext?.completeRequest(returningItems: nil)
}
@@ -5,7 +5,7 @@ import com.google.firebase.messaging.RemoteMessage
class BackgroundNotificationHandler(
private val context: Context,
private val notifInterface: BackgroundNotificationHandlerInterface
private val notifInterface: BackgroundNotificationHandlerInterface,
) {
fun handleMessage(remoteMessage: RemoteMessage) {
if (ExpoBackgroundNotificationHandlerModule.isForegrounded) {
@@ -8,67 +8,68 @@ class ExpoBackgroundNotificationHandlerModule : Module() {
var isForegrounded = false
}
override fun definition() = ModuleDefinition {
Name("ExpoBackgroundNotificationHandler")
override fun definition() =
ModuleDefinition {
Name("ExpoBackgroundNotificationHandler")
OnCreate {
NotificationPrefs(appContext.reactContext).initialize()
}
OnCreate {
NotificationPrefs(appContext.reactContext).initialize()
}
OnActivityEntersForeground {
isForegrounded = true
}
OnActivityEntersForeground {
isForegrounded = true
}
OnActivityEntersBackground {
isForegrounded = false
}
OnActivityEntersBackground {
isForegrounded = false
}
AsyncFunction("getAllPrefsAsync") {
return@AsyncFunction NotificationPrefs(appContext.reactContext).getAllPrefs()
}
AsyncFunction("getAllPrefsAsync") {
return@AsyncFunction NotificationPrefs(appContext.reactContext).getAllPrefs()
}
AsyncFunction("getBoolAsync") { forKey: String ->
return@AsyncFunction NotificationPrefs(appContext.reactContext).getBoolean(forKey)
}
AsyncFunction("getBoolAsync") { forKey: String ->
return@AsyncFunction NotificationPrefs(appContext.reactContext).getBoolean(forKey)
}
AsyncFunction("getStringAsync") { forKey: String ->
return@AsyncFunction NotificationPrefs(appContext.reactContext).getString(forKey)
}
AsyncFunction("getStringAsync") { forKey: String ->
return@AsyncFunction NotificationPrefs(appContext.reactContext).getString(forKey)
}
AsyncFunction("getStringArrayAsync") { forKey: String ->
return@AsyncFunction NotificationPrefs(appContext.reactContext).getStringArray(forKey)
}
AsyncFunction("getStringArrayAsync") { forKey: String ->
return@AsyncFunction NotificationPrefs(appContext.reactContext).getStringArray(forKey)
}
AsyncFunction("setBoolAsync") { forKey: String, value: Boolean ->
NotificationPrefs(appContext.reactContext).setBoolean(forKey, value)
}
AsyncFunction("setBoolAsync") { forKey: String, value: Boolean ->
NotificationPrefs(appContext.reactContext).setBoolean(forKey, value)
}
AsyncFunction("setStringAsync") { forKey: String, value: String ->
NotificationPrefs(appContext.reactContext).setString(forKey, value)
}
AsyncFunction("setStringAsync") { forKey: String, value: String ->
NotificationPrefs(appContext.reactContext).setString(forKey, value)
}
AsyncFunction("setStringArrayAsync") { forKey: String, value: Array<String> ->
NotificationPrefs(appContext.reactContext).setStringArray(forKey, value)
}
AsyncFunction("setStringArrayAsync") { forKey: String, value: Array<String> ->
NotificationPrefs(appContext.reactContext).setStringArray(forKey, value)
}
AsyncFunction("addToStringArrayAsync") { forKey: String, string: String ->
NotificationPrefs(appContext.reactContext).addToStringArray(forKey, string)
}
AsyncFunction("addToStringArrayAsync") { forKey: String, string: String ->
NotificationPrefs(appContext.reactContext).addToStringArray(forKey, string)
}
AsyncFunction("removeFromStringArrayAsync") { forKey: String, string: String ->
NotificationPrefs(appContext.reactContext).removeFromStringArray(forKey, string)
}
AsyncFunction("removeFromStringArrayAsync") { forKey: String, string: String ->
NotificationPrefs(appContext.reactContext).removeFromStringArray(forKey, string)
}
AsyncFunction("addManyToStringArrayAsync") { forKey: String, strings: Array<String> ->
NotificationPrefs(appContext.reactContext).addManyToStringArray(forKey, strings)
}
AsyncFunction("addManyToStringArrayAsync") { forKey: String, strings: Array<String> ->
NotificationPrefs(appContext.reactContext).addManyToStringArray(forKey, strings)
}
AsyncFunction("removeManyFromStringArrayAsync") { forKey: String, strings: Array<String> ->
NotificationPrefs(appContext.reactContext).removeManyFromStringArray(forKey, strings)
}
AsyncFunction("removeManyFromStringArrayAsync") { forKey: String, strings: Array<String> ->
NotificationPrefs(appContext.reactContext).removeManyFromStringArray(forKey, strings)
}
AsyncFunction("setBadgeCountAsync") { _: Int ->
// This does nothing on Android
AsyncFunction("setBadgeCountAsync") { _: Int ->
// This does nothing on Android
}
}
}
}
@@ -2,20 +2,24 @@ package expo.modules.backgroundnotificationhandler
import android.content.Context
val DEFAULTS = mapOf<String, Any>(
"playSoundChat" to true,
"playSoundFollow" to false,
"playSoundLike" to false,
"playSoundMention" to false,
"playSoundQuote" to false,
"playSoundReply" to false,
"playSoundRepost" to false,
"mutedThreads" to mapOf<String, List<String>>()
)
val DEFAULTS =
mapOf<String, Any>(
"playSoundChat" to true,
"playSoundFollow" to false,
"playSoundLike" to false,
"playSoundMention" to false,
"playSoundQuote" to false,
"playSoundReply" to false,
"playSoundRepost" to false,
"mutedThreads" to mapOf<String, List<String>>(),
)
class NotificationPrefs (private val context: Context?) {
private val prefs = context?.getSharedPreferences("xyz.blueskyweb.app", Context.MODE_PRIVATE)
?: throw Error("Context is null")
class NotificationPrefs(
private val context: Context?,
) {
private val prefs =
context?.getSharedPreferences("xyz.blueskyweb.app", Context.MODE_PRIVATE)
?: throw Error("Context is null")
fun initialize() {
prefs
@@ -41,94 +45,99 @@ class NotificationPrefs (private val context: Context?) {
}
}
}
}
.apply()
}.apply()
}
fun getAllPrefs(): MutableMap<String, *> {
return prefs.all
}
fun getAllPrefs(): MutableMap<String, *> = prefs.all
fun getBoolean(key: String): Boolean {
return prefs.getBoolean(key, false)
}
fun getBoolean(key: String): Boolean = prefs.getBoolean(key, false)
fun getString(key: String): String? {
return prefs.getString(key, null)
}
fun getString(key: String): String? = prefs.getString(key, null)
fun getStringArray(key: String): Array<String>? {
return prefs.getStringSet(key, null)?.toTypedArray()
}
fun getStringArray(key: String): Array<String>? = prefs.getStringSet(key, null)?.toTypedArray()
fun setBoolean(key: String, value: Boolean) {
fun setBoolean(
key: String,
value: Boolean,
) {
prefs
.edit()
.apply {
putBoolean(key, value)
}
.apply()
}.apply()
}
fun setString(key: String, value: String) {
fun setString(
key: String,
value: String,
) {
prefs
.edit()
.apply {
putString(key, value)
}
.apply()
}.apply()
}
fun setStringArray(key: String, value: Array<String>) {
fun setStringArray(
key: String,
value: Array<String>,
) {
prefs
.edit()
.apply {
putStringSet(key, value.toSet())
}
.apply()
}.apply()
}
fun addToStringArray(key: String, string: String) {
fun addToStringArray(
key: String,
string: String,
) {
prefs
.edit()
.apply {
val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf()
set.add(string)
putStringSet(key, set)
}
.apply()
}.apply()
}
fun removeFromStringArray(key: String, string: String) {
fun removeFromStringArray(
key: String,
string: String,
) {
prefs
.edit()
.apply {
val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf()
set.remove(string)
putStringSet(key, set)
}
.apply()
}.apply()
}
fun addManyToStringArray(key: String, strings: Array<String>) {
fun addManyToStringArray(
key: String,
strings: Array<String>,
) {
prefs
.edit()
.apply {
val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf()
set.addAll(strings.toSet())
putStringSet(key, set)
}
.apply()
}.apply()
}
fun removeManyFromStringArray(key: String, strings: Array<String>) {
fun removeManyFromStringArray(
key: String,
strings: Array<String>,
) {
prefs
.edit()
.apply {
val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf()
set.removeAll(strings.toSet())
putStringSet(key, set)
}
.apply()
}.apply()
}
}
}
@@ -2,16 +2,16 @@ import ExpoModulesCore
let APP_GROUP = "group.app.bsky"
let DEFAULTS: [String:Any] = [
"playSoundChat" : true,
let DEFAULTS: [String: Any] = [
"playSoundChat": true,
"playSoundFollow": false,
"playSoundLike": false,
"playSoundMention": false,
"playSoundQuote": false,
"playSoundReply": false,
"playSoundRepost": false,
"mutedThreads": [:] as! [String:[String]],
"badgeCount": 0,
"mutedThreads": [:] as! [String: [String]],
"badgeCount": 0
]
/*
@@ -23,10 +23,10 @@ let DEFAULTS: [String:Any] = [
*/
public class ExpoBackgroundNotificationHandlerModule: Module {
let userDefaults = UserDefaults(suiteName: APP_GROUP)
public func definition() -> ModuleDefinition {
Name("ExpoBackgroundNotificationHandler")
OnCreate {
DEFAULTS.forEach { p in
if userDefaults?.value(forKey: p.key) == nil {
@@ -34,57 +34,56 @@ public class ExpoBackgroundNotificationHandlerModule: Module {
}
}
}
AsyncFunction("getAllPrefsAsync") { () -> [String:Any]? in
AsyncFunction("getAllPrefsAsync") { () -> [String: Any]? in
var keys: [String] = []
DEFAULTS.forEach { p in
keys.append(p.key)
}
return userDefaults?.dictionaryWithValues(forKeys: keys)
}
AsyncFunction("getBoolAsync") { (forKey: String) -> Bool in
if let pref = userDefaults?.bool(forKey: forKey) {
return pref
}
return false
}
AsyncFunction("getStringAsync") { (forKey: String) -> String? in
if let pref = userDefaults?.string(forKey: forKey) {
return pref
}
return nil
}
AsyncFunction("getStringArrayAsync") { (forKey: String) -> [String]? in
if let pref = userDefaults?.stringArray(forKey: forKey) {
return pref
}
return nil
}
AsyncFunction("setBoolAsync") { (forKey: String, value: Bool) -> Void in
AsyncFunction("setBoolAsync") { (forKey: String, value: Bool) in
userDefaults?.setValue(value, forKey: forKey)
}
AsyncFunction("setStringAsync") { (forKey: String, value: String) -> Void in
AsyncFunction("setStringAsync") { (forKey: String, value: String) in
userDefaults?.setValue(value, forKey: forKey)
}
AsyncFunction("setStringArrayAsync") { (forKey: String, value: [String]) -> Void in
AsyncFunction("setStringArrayAsync") { (forKey: String, value: [String]) in
userDefaults?.setValue(value, forKey: forKey)
}
AsyncFunction("addToStringArrayAsync") { (forKey: String, string: String) in
if var curr = userDefaults?.stringArray(forKey: forKey),
!curr.contains(string)
{
!curr.contains(string) {
curr.append(string)
userDefaults?.setValue(curr, forKey: forKey)
}
}
AsyncFunction("removeFromStringArrayAsync") { (forKey: String, string: String) in
if var curr = userDefaults?.stringArray(forKey: forKey) {
curr.removeAll { s in
@@ -93,7 +92,7 @@ public class ExpoBackgroundNotificationHandlerModule: Module {
userDefaults?.setValue(curr, forKey: forKey)
}
}
AsyncFunction("addManyToStringArrayAsync") { (forKey: String, strings: [String]) in
if var curr = userDefaults?.stringArray(forKey: forKey) {
strings.forEach { s in
@@ -104,7 +103,7 @@ public class ExpoBackgroundNotificationHandlerModule: Module {
userDefaults?.setValue(curr, forKey: forKey)
}
}
AsyncFunction("removeManyFromStringArrayAsync") { (forKey: String, strings: [String]) in
if var curr = userDefaults?.stringArray(forKey: forKey) {
strings.forEach { s in
@@ -113,7 +112,7 @@ public class ExpoBackgroundNotificationHandlerModule: Module {
userDefaults?.setValue(curr, forKey: forKey)
}
}
AsyncFunction("setBadgeCountAsync") { (count: Int) in
userDefaults?.setValue(count, forKey: "badgeCount")
}
@@ -5,7 +5,10 @@ import android.graphics.Canvas
import android.graphics.drawable.Animatable
import androidx.appcompat.widget.AppCompatImageView
class AppCompatImageViewExtended(context: Context, private val parent: GifView): AppCompatImageView(context) {
class AppCompatImageViewExtended(
context: Context,
private val parent: GifView,
) : AppCompatImageView(context) {
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
@@ -34,4 +37,4 @@ class AppCompatImageViewExtended(context: Context, private val parent: GifView):
drawable.start()
}
}
}
}
@@ -6,49 +6,50 @@ import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
class ExpoBlueskyGifViewModule : Module() {
override fun definition() = ModuleDefinition {
Name("ExpoBlueskyGifView")
override fun definition() =
ModuleDefinition {
Name("ExpoBlueskyGifView")
AsyncFunction("prefetchAsync") { sources: List<String> ->
val activity = appContext.currentActivity ?: return@AsyncFunction
val glide = Glide.with(activity)
AsyncFunction("prefetchAsync") { sources: List<String> ->
val activity = appContext.currentActivity ?: return@AsyncFunction
val glide = Glide.with(activity)
sources.forEach { source ->
glide
.download(source)
.diskCacheStrategy(DiskCacheStrategy.DATA)
.submit()
sources.forEach { source ->
glide
.download(source)
.diskCacheStrategy(DiskCacheStrategy.DATA)
.submit()
}
}
View(GifView::class) {
Events(
"onPlayerStateChange",
)
Prop("source") { view: GifView, source: String ->
view.source = source
}
Prop("placeholderSource") { view: GifView, source: String ->
view.placeholderSource = source
}
Prop("autoplay") { view: GifView, autoplay: Boolean ->
view.autoplay = autoplay
}
AsyncFunction("playAsync") { view: GifView ->
view.play()
}
AsyncFunction("pauseAsync") { view: GifView ->
view.pause()
}
AsyncFunction("toggleAsync") { view: GifView ->
view.toggle()
}
}
}
View(GifView::class) {
Events(
"onPlayerStateChange"
)
Prop("source") { view: GifView, source: String ->
view.source = source
}
Prop("placeholderSource") { view: GifView, source: String ->
view.placeholderSource = source
}
Prop("autoplay") { view: GifView, autoplay: Boolean ->
view.autoplay = autoplay
}
AsyncFunction("playAsync") { view: GifView ->
view.play()
}
AsyncFunction("pauseAsync") { view: GifView ->
view.pause()
}
AsyncFunction("toggleAsync") { view: GifView ->
view.toggle()
}
}
}
}
@@ -1,6 +1,5 @@
package expo.modules.blueskygifview
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.Animatable
@@ -15,7 +14,10 @@ import expo.modules.kotlin.exception.Exceptions
import expo.modules.kotlin.viewevent.EventDispatcher
import expo.modules.kotlin.views.ExpoView
class GifView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
class GifView(
context: Context,
appContext: AppContext,
) : ExpoView(context, appContext) {
// Events
private val onPlayerStateChange by EventDispatcher()
@@ -44,8 +46,7 @@ class GifView(context: Context, appContext: AppContext) : ExpoView(context, appC
}
}
//<editor-fold desc="Lifecycle">
// <editor-fold desc="Lifecycle">
init {
this.setBackgroundColor(Color.TRANSPARENT)
@@ -70,80 +71,82 @@ class GifView(context: Context, appContext: AppContext) : ExpoView(context, appC
super.onDetachedFromWindow()
}
//</editor-fold>
// </editor-fold>
//<editor-fold desc="Loading">
// <editor-fold desc="Loading">
private fun load() {
if (placeholderSource == null || source == null) {
return
}
this.webpRequest = glide.load(source)
.diskCacheStrategy(DiskCacheStrategy.DATA)
.skipMemoryCache(false)
.listener(object: RequestListener<Drawable> {
override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: Target<Drawable>?,
dataSource: com.bumptech.glide.load.DataSource?,
isFirstResource: Boolean
): Boolean {
if (placeholderRequest != null) {
glide.clear(placeholderRequest)
}
return false
}
this.webpRequest =
glide
.load(source)
.diskCacheStrategy(DiskCacheStrategy.DATA)
.skipMemoryCache(false)
.listener(
object : RequestListener<Drawable> {
override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: Target<Drawable>?,
dataSource: com.bumptech.glide.load.DataSource?,
isFirstResource: Boolean,
): Boolean {
if (placeholderRequest != null) {
glide.clear(placeholderRequest)
}
return false
}
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>?,
isFirstResource: Boolean
): Boolean {
return true
}
})
.into(this.imageView)
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>?,
isFirstResource: Boolean,
): Boolean = true
},
).into(this.imageView)
if (this.imageView.drawable == null || this.imageView.drawable !is Animatable) {
this.placeholderRequest = glide.load(placeholderSource)
.diskCacheStrategy(DiskCacheStrategy.DATA)
// Let's not bloat the memory cache with placeholders
.skipMemoryCache(true)
.listener(object: RequestListener<Drawable> {
override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: Target<Drawable>?,
dataSource: com.bumptech.glide.load.DataSource?,
isFirstResource: Boolean
): Boolean {
// Incase this request finishes after the webp, let's just not set
// the drawable. This shouldn't happen because the request should get cancelled
if (imageView.drawable == null) {
imageView.setImageDrawable(resource)
}
return true
}
this.placeholderRequest =
glide
.load(placeholderSource)
.diskCacheStrategy(DiskCacheStrategy.DATA)
// Let's not bloat the memory cache with placeholders
.skipMemoryCache(true)
.listener(
object : RequestListener<Drawable> {
override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: Target<Drawable>?,
dataSource: com.bumptech.glide.load.DataSource?,
isFirstResource: Boolean,
): Boolean {
// Incase this request finishes after the webp, let's just not set
// the drawable. This shouldn't happen because the request should get cancelled
if (imageView.drawable == null) {
imageView.setImageDrawable(resource)
}
return true
}
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>?,
isFirstResource: Boolean
): Boolean {
return true
}
})
.submit()
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>?,
isFirstResource: Boolean,
): Boolean = true
},
).submit()
}
}
//</editor-fold>
// </editor-fold>
//<editor-fold desc="Controls">
// <editor-fold desc="Controls">
fun play() {
this.imageView.play()
@@ -165,16 +168,18 @@ class GifView(context: Context, appContext: AppContext) : ExpoView(context, appC
}
}
//</editor-fold>
// </editor-fold>
//<editor-fold desc="Util">
// <editor-fold desc="Util">
fun firePlayerStateChange() {
onPlayerStateChange(mapOf(
"isPlaying" to this.isPlaying,
"isLoaded" to this.isLoaded,
))
onPlayerStateChange(
mapOf(
"isPlaying" to this.isPlaying,
"isLoaded" to this.isLoaded,
),
)
}
//</editor-fold>
// </editor-fold>
}
@@ -5,11 +5,11 @@ import SDWebImageWebPCoder
public class ExpoBlueskyGifViewModule: Module {
public func definition() -> ModuleDefinition {
Name("ExpoBlueskyGifView")
OnCreate {
SDImageCodersManager.shared.addCoder(SDImageGIFCoder.shared)
}
AsyncFunction("prefetchAsync") { (sources: [URL]) in
SDWebImagePrefetcher.shared.prefetchURLs(sources, context: Util.createContext(), progress: nil)
}
@@ -18,27 +18,27 @@ public class ExpoBlueskyGifViewModule: Module {
Events(
"onPlayerStateChange"
)
Prop("source") { (view: GifView, prop: String) in
view.source = prop
}
Prop("placeholderSource") { (view: GifView, prop: String) in
view.placeholderSource = prop
}
Prop("autoplay") { (view: GifView, prop: Bool) in
view.autoplay = prop
}
AsyncFunction("toggleAsync") { (view: GifView) in
view.toggle()
}
AsyncFunction("playAsync") { (view: GifView) in
view.play()
}
AsyncFunction("pauseAsync") { (view: GifView) in
view.pause()
}
@@ -16,14 +16,14 @@ public class GifView: ExpoView, AVPlayerViewControllerDelegate {
)
private var isPlaying = true
private var isLoaded = false
// Requests
private var webpOperation: SDWebImageCombinedOperation?
private var placeholderOperation: SDWebImageCombinedOperation?
// Props
var source: String? = nil
var placeholderSource: String? = nil
var source: String?
var placeholderSource: String?
var autoplay = true {
didSet {
if !autoplay {
@@ -78,8 +78,7 @@ public class GifView: ExpoView, AVPlayerViewControllerDelegate {
// See:
// https://github.com/SDWebImage/SDWebImage/blob/master/Docs/HowToUse.md#using-asynchronous-image-caching-independently
if !SDImageCache.shared.diskImageDataExists(withKey: source),
let url = URL(string: placeholderSource)
{
let url = URL(string: placeholderSource) {
self.placeholderOperation = imageManager.loadImage(
with: url,
options: [.retryFailed],
@@ -132,8 +131,7 @@ public class GifView: ExpoView, AVPlayerViewControllerDelegate {
if let placeholderSource = self.placeholderSource,
imageUrl?.absoluteString == placeholderSource,
self.imageView.image == nil,
let image = image
{
let image = image {
self.setImage(image)
return
}
@@ -142,8 +140,7 @@ public class GifView: ExpoView, AVPlayerViewControllerDelegate {
imageUrl?.absoluteString == source,
// UIImage perf suckssss if the image is animated
let data = data,
let animatedImage = SDAnimatedImage(data: data)
{
let animatedImage = SDAnimatedImage(data: data) {
self.placeholderOperation?.cancel()
self.isPlaying = self.autoplay
self.isLoaded = true
@@ -1,10 +0,0 @@
package expo.modules.blueskyswissarmy.deviceprefs
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
class ExpoBlueskyDevicePrefsModule : Module() {
override fun definition() = ModuleDefinition {
Name("ExpoBlueskyDevicePrefs")
}
}
@@ -0,0 +1,24 @@
package expo.modules.blueskyswissarmy.platforminfo
import android.provider.Settings
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
class ExpoPlatformInfoModule : Module() {
override fun definition() =
ModuleDefinition {
Name("ExpoPlatformInfo")
// See https://github.com/software-mansion/react-native-reanimated/blob/7df5fd57d608fe25724608835461cd925ff5151d/packages/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/nativeProxy/NativeProxyCommon.java#L242
Function("getIsReducedMotionEnabled") {
val resolver = appContext.reactContext?.contentResolver ?: return@Function false
val scale = Settings.Global.getString(resolver, Settings.Global.TRANSITION_ANIMATION_SCALE) ?: return@Function false
try {
return@Function scale.toFloat() == 0f
} catch (_: Error) {
return@Function false
}
}
}
}
@@ -1,54 +1,103 @@
package expo.modules.blueskyswissarmy.referrer
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.util.Log
import com.android.installreferrer.api.InstallReferrerClient
import com.android.installreferrer.api.InstallReferrerStateListener
import expo.modules.kotlin.Promise
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import expo.modules.kotlin.Promise
class ExpoBlueskyReferrerModule : Module() {
override fun definition() = ModuleDefinition {
Name("ExpoBlueskyReferrer")
private var intent: Intent? = null
private var activityReferrer: Uri? = null
AsyncFunction("getGooglePlayReferrerInfoAsync") { promise: Promise ->
val referrerClient = InstallReferrerClient.newBuilder(appContext.reactContext).build()
referrerClient.startConnection(object : InstallReferrerStateListener {
override fun onInstallReferrerSetupFinished(responseCode: Int) {
if (responseCode == InstallReferrerClient.InstallReferrerResponse.OK) {
Log.d("ExpoGooglePlayReferrer", "Successfully retrieved referrer info.")
override fun definition() =
ModuleDefinition {
Name("ExpoBlueskyReferrer")
val response = referrerClient.installReferrer
Log.d("ExpoGooglePlayReferrer", "Install referrer: ${response.installReferrer}")
OnNewIntent {
intent = it
activityReferrer = appContext.currentActivity?.referrer
}
promise.resolve(
mapOf(
"installReferrer" to response.installReferrer,
"clickTimestamp" to response.referrerClickTimestampSeconds,
"installTimestamp" to response.installBeginTimestampSeconds
)
)
Function("getReferrerInfo") {
val intentReferrer =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent?.getParcelableExtra(Intent.EXTRA_REFERRER, Uri::class.java)
} else {
Log.d("ExpoGooglePlayReferrer", "Failed to get referrer info. Unknown error.")
promise.reject(
"ERR_GOOGLE_PLAY_REFERRER_UNKNOWN",
"Failed to get referrer info",
Exception("Failed to get referrer info")
)
intent?.getParcelableExtra(Intent.EXTRA_REFERRER)
}
referrerClient.endConnection()
// Some apps explicitly set a referrer, like Chrome. In these cases, we prefer this since
// it's the actual website that the user came from rather than the app.
if (intentReferrer is Uri) {
val res =
mapOf(
"referrer" to intentReferrer.toString(),
"hostname" to intentReferrer.host,
)
intent = null
return@Function res
}
override fun onInstallReferrerServiceDisconnected() {
Log.d("ExpoGooglePlayReferrer", "Failed to get referrer info. Service disconnected.")
referrerClient.endConnection()
promise.reject(
"ERR_GOOGLE_PLAY_REFERRER_DISCONNECTED",
"Failed to get referrer info",
Exception("Failed to get referrer info")
)
// In all other cases, we'll just record the app that sent the intent.
if (activityReferrer != null) {
// referrer could become null here. `.toString()` though can be called on null
val res =
mapOf(
"referrer" to activityReferrer.toString(),
"hostname" to (activityReferrer?.host ?: ""),
)
activityReferrer = null
return@Function res
}
})
return@Function null
}
AsyncFunction("getGooglePlayReferrerInfoAsync") { promise: Promise ->
val referrerClient = InstallReferrerClient.newBuilder(appContext.reactContext).build()
referrerClient.startConnection(
object : InstallReferrerStateListener {
override fun onInstallReferrerSetupFinished(responseCode: Int) {
if (responseCode == InstallReferrerClient.InstallReferrerResponse.OK) {
Log.d("ExpoGooglePlayReferrer", "Successfully retrieved referrer info.")
val response = referrerClient.installReferrer
Log.d("ExpoGooglePlayReferrer", "Install referrer: ${response.installReferrer}")
promise.resolve(
mapOf(
"installReferrer" to response.installReferrer,
"clickTimestamp" to response.referrerClickTimestampSeconds,
"installTimestamp" to response.installBeginTimestampSeconds,
),
)
} else {
Log.d("ExpoGooglePlayReferrer", "Failed to get referrer info. Unknown error.")
promise.reject(
"ERR_GOOGLE_PLAY_REFERRER_UNKNOWN",
"Failed to get referrer info",
Exception("Failed to get referrer info"),
)
}
referrerClient.endConnection()
}
override fun onInstallReferrerServiceDisconnected() {
Log.d("ExpoGooglePlayReferrer", "Failed to get referrer info. Service disconnected.")
referrerClient.endConnection()
promise.reject(
"ERR_GOOGLE_PLAY_REFERRER_DISCONNECTED",
"Failed to get referrer info",
Exception("Failed to get referrer info"),
)
}
},
)
}
}
}
}
}
@@ -0,0 +1,69 @@
package expo.modules.blueskyswissarmy.sharedprefs
import android.content.Context
import android.util.Log
import expo.modules.kotlin.jni.JavaScriptValue
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
class ExpoBlueskySharedPrefsModule : Module() {
private fun getContext(): Context {
val context = appContext.reactContext ?: throw Error("Context is null")
return context
}
override fun definition() =
ModuleDefinition {
Name("ExpoBlueskySharedPrefs")
Function("setString") { key: String, value: String ->
return@Function SharedPrefs(getContext()).setValue(key, value)
}
Function("setValue") { key: String, value: JavaScriptValue ->
val context = getContext()
Log.d("ExpoBlueskySharedPrefs", "Setting value for key: $key")
try {
if (value.isNumber()) {
SharedPrefs(context).setValue(key, value.getFloat())
} else if (value.isBool()) {
SharedPrefs(context).setValue(key, value.getBool())
} else if (value.isNull() || value.isUndefined()) {
SharedPrefs(context).removeValue(key)
} else {
Log.d(NAME, "Unsupported type: ${value.kind()}")
}
} catch (e: Error) {
Log.d(NAME, "Error setting value: $e")
}
}
Function("removeValue") { key: String ->
return@Function SharedPrefs(getContext()).removeValue(key)
}
Function("getString") { key: String ->
return@Function SharedPrefs(getContext()).getString(key)
}
Function("getNumber") { key: String ->
return@Function SharedPrefs(getContext()).getFloat(key)
}
Function("getBool") { key: String ->
return@Function SharedPrefs(getContext()).getBoolean(key)
}
Function("addToSet") { key: String, value: String ->
return@Function SharedPrefs(getContext()).addToSet(key, value)
}
Function("removeFromSet") { key: String, value: String ->
return@Function SharedPrefs(getContext()).removeFromSet(key, value)
}
Function("setContains") { key: String, value: String ->
return@Function SharedPrefs(getContext()).setContains(key, value)
}
}
}
@@ -0,0 +1,241 @@
package expo.modules.blueskyswissarmy.sharedprefs
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
val DEFAULTS =
mapOf<String, Any>(
"playSoundChat" to true,
"playSoundFollow" to false,
"playSoundLike" to false,
"playSoundMention" to false,
"playSoundQuote" to false,
"playSoundReply" to false,
"playSoundRepost" to false,
"badgeCount" to 0,
)
const val NAME = "SharedPrefs"
class SharedPrefs(
private val context: Context,
) {
companion object {
private var hasInitialized = false
private var instance: SharedPreferences? = null
fun getInstance(
context: Context,
info: String? = "(no info)",
): SharedPreferences {
if (instance == null) {
Log.d(NAME, "No preferences instance found, creating one.")
instance = context.getSharedPreferences("xyz.blueskyweb.app", Context.MODE_PRIVATE)
}
val safeInstance = instance ?: throw Error("Preferences is null: $info")
if (!hasInitialized) {
Log.d(NAME, "Preferences instance has not been initialized yet.")
initialize(safeInstance)
hasInitialized = true
Log.d(NAME, "Preferences instance has been initialized.")
}
return safeInstance
}
private fun initialize(instance: SharedPreferences) {
instance
.edit()
.apply {
DEFAULTS.forEach { (key, value) ->
if (instance.contains(key)) {
return@forEach
}
when (value) {
is Boolean -> {
putBoolean(key, value)
}
is String -> {
putString(key, value)
}
is Array<*> -> {
putStringSet(key, value.map { it.toString() }.toSet())
}
is Map<*, *> -> {
putStringSet(key, value.map { it.toString() }.toSet())
}
}
}
}.apply()
}
}
fun setValue(
key: String,
value: String,
) {
val safeInstance = getInstance(context)
safeInstance
.edit()
.apply {
putString(key, value)
}.apply()
}
fun setValue(
key: String,
value: Float,
) {
val safeInstance = getInstance(context)
safeInstance
.edit()
.apply {
putFloat(key, value)
}.apply()
}
fun setValue(
key: String,
value: Boolean,
) {
val safeInstance = getInstance(context)
safeInstance
.edit()
.apply {
putBoolean(key, value)
}.apply()
}
fun setValue(
key: String,
value: Set<String>,
) {
val safeInstance = getInstance(context)
safeInstance
.edit()
.apply {
putStringSet(key, value)
}.apply()
}
fun removeValue(key: String) {
val safeInstance = getInstance(context)
safeInstance
.edit()
.apply {
remove(key)
}.apply()
}
fun getString(key: String): String? {
val safeInstance = getInstance(context)
return safeInstance.getString(key, null)
}
fun getFloat(key: String): Float? {
val safeInstance = getInstance(context)
if (!safeInstance.contains(key)) {
return null
}
return safeInstance.getFloat(key, 0.0f)
}
@Suppress("ktlint:standard:function-naming")
fun _setAnyValue(
key: String,
value: Any,
) {
val safeInstance = getInstance(context)
safeInstance
.edit()
.apply {
when (value) {
is String -> putString(key, value)
is Float -> putFloat(key, value)
is Boolean -> putBoolean(key, value)
is Set<*> -> putStringSet(key, value.map { it.toString() }.toSet())
else -> throw Error("Unsupported type: ${value::class.java}")
}
}.apply()
}
fun getBoolean(key: String): Boolean? {
val safeInstance = getInstance(context)
if (!safeInstance.contains(key)) {
return null
}
Log.d(NAME, "Getting boolean for key: $key")
val res = safeInstance.getBoolean(key, false)
Log.d(NAME, "Got boolean for key: $key, value: $res")
return res
}
fun addToSet(
key: String,
value: String,
) {
val safeInstance = getInstance(context)
val set = safeInstance.getStringSet(key, setOf()) ?: setOf()
val newSet =
set.toMutableSet().apply {
add(value)
}
safeInstance
.edit()
.apply {
putStringSet(key, newSet)
}.apply()
}
fun removeFromSet(
key: String,
value: String,
) {
val safeInstance = getInstance(context)
val set = safeInstance.getStringSet(key, setOf()) ?: setOf()
val newSet =
set.toMutableSet().apply {
remove(value)
}
safeInstance
.edit()
.apply {
putStringSet(key, newSet)
}.apply()
}
fun setContains(
key: String,
value: String,
): Boolean {
val safeInstance = getInstance(context)
val set = safeInstance.getStringSet(key, setOf()) ?: setOf()
return set.contains(value)
}
fun hasValue(key: String): Boolean {
val safeInstance = getInstance(context)
return safeInstance.contains(key)
}
fun getValues(keys: Set<String>): Map<String, Any?> {
val safeInstance = getInstance(context)
return keys.associateWith { key ->
when (val value = safeInstance.all[key]) {
is String -> value
is Float -> value
is Boolean -> value
is Set<*> -> value
else -> null
}
}
}
}
@@ -0,0 +1,23 @@
package expo.modules.blueskyswissarmy.visibilityview
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
class ExpoBlueskyVisibilityViewModule : Module() {
override fun definition() =
ModuleDefinition {
Name("ExpoBlueskyVisibilityView")
AsyncFunction("updateActiveViewAsync") {
VisibilityViewManager.updateActiveView()
}
View(VisibilityView::class) {
Events(arrayOf("onChangeStatus"))
Prop("enabled") { view: VisibilityView, prop: Boolean ->
view.isViewEnabled = prop
}
}
}
}
@@ -0,0 +1,63 @@
package expo.modules.blueskyswissarmy.visibilityview
import android.content.Context
import android.graphics.Rect
import expo.modules.kotlin.AppContext
import expo.modules.kotlin.viewevent.EventDispatcher
import expo.modules.kotlin.views.ExpoView
class VisibilityView(
context: Context,
appContext: AppContext,
) : ExpoView(context, appContext) {
var isViewEnabled: Boolean = false
private val onChangeStatus by EventDispatcher()
private var isCurrentlyActive = false
override fun onAttachedToWindow() {
super.onAttachedToWindow()
VisibilityViewManager.addView(this)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
VisibilityViewManager.removeView(this)
}
fun setIsCurrentlyActive(isActive: Boolean) {
if (isCurrentlyActive == isActive) {
return
}
this.isCurrentlyActive = isActive
this.onChangeStatus(
mapOf(
"isActive" to isActive,
),
)
}
fun getPositionOnScreen(): Rect? {
if (!this.isShown) {
return null
}
val screenPosition = intArrayOf(0, 0)
this.getLocationInWindow(screenPosition)
return Rect(
screenPosition[0],
screenPosition[1],
screenPosition[0] + this.width,
screenPosition[1] + this.height,
)
}
fun isViewableEnough(): Boolean {
val positionOnScreen = this.getPositionOnScreen() ?: return false
val visibleArea = positionOnScreen.width() * positionOnScreen.height()
val totalArea = this.width * this.height
return visibleArea >= 0.5 * totalArea
}
}
@@ -0,0 +1,82 @@
package expo.modules.blueskyswissarmy.visibilityview
import android.graphics.Rect
class VisibilityViewManager {
companion object {
private val views = HashMap<Int, VisibilityView>()
private var currentlyActiveView: VisibilityView? = null
private var prevCount = 0
fun addView(view: VisibilityView) {
this.views[view.id] = view
if (this.prevCount == 0) {
this.updateActiveView()
}
this.prevCount = this.views.count()
}
fun removeView(view: VisibilityView) {
this.views.remove(view.id)
this.prevCount = this.views.count()
}
fun updateActiveView() {
var activeView: VisibilityView? = null
val count = this.views.count()
if (count == 1) {
val view = this.views.values.first()
if (view.isViewableEnough()) {
activeView = view
}
} else if (count > 1) {
val views = this.views.values
var mostVisibleView: VisibilityView? = null
var mostVisiblePosition: Rect? = null
views.forEach { view ->
if (!view.isViewableEnough()) {
return
}
val position = view.getPositionOnScreen() ?: return@forEach
val topY = position.centerY() - (position.height() / 2)
if (topY >= 150) {
if (mostVisiblePosition == null) {
mostVisiblePosition = position
}
if (position.centerY() <= mostVisiblePosition!!.centerY()) {
mostVisibleView = view
mostVisiblePosition = position
}
}
}
activeView = mostVisibleView
}
if (activeView == this.currentlyActiveView) {
return
}
this.clearActiveView()
if (activeView != null) {
this.setActiveView(activeView)
}
}
private fun clearActiveView() {
this.currentlyActiveView?.setIsCurrentlyActive(false)
this.currentlyActiveView = null
}
private fun setActiveView(view: VisibilityView) {
view.setIsCurrentlyActive(true)
this.currentlyActiveView = view
}
}
}
@@ -1,12 +1,19 @@
{
"platforms": ["ios", "tvos", "android", "web"],
"ios": {
"modules": ["ExpoBlueskyDevicePrefsModule", "ExpoBlueskyReferrerModule"]
"modules": [
"ExpoBlueskySharedPrefsModule",
"ExpoBlueskyReferrerModule",
"ExpoBlueskyVisibilityViewModule",
"ExpoPlatformInfoModule"
]
},
"android": {
"modules": [
"expo.modules.blueskyswissarmy.deviceprefs.ExpoBlueskyDevicePrefsModule",
"expo.modules.blueskyswissarmy.referrer.ExpoBlueskyReferrerModule"
"expo.modules.blueskyswissarmy.sharedprefs.ExpoBlueskySharedPrefsModule",
"expo.modules.blueskyswissarmy.referrer.ExpoBlueskyReferrerModule",
"expo.modules.blueskyswissarmy.visibilityview.ExpoBlueskyVisibilityViewModule",
"expo.modules.blueskyswissarmy.platforminfo.ExpoPlatformInfoModule"
]
}
}
+5 -2
View File
@@ -1,4 +1,7 @@
import * as DevicePrefs from './src/DevicePrefs'
import * as PlatformInfo from './src/PlatformInfo'
import {AudioCategory} from './src/PlatformInfo/types'
import * as Referrer from './src/Referrer'
import * as SharedPrefs from './src/SharedPrefs'
import VisibilityView from './src/VisibilityView'
export {DevicePrefs, Referrer}
export {AudioCategory, PlatformInfo, Referrer, SharedPrefs, VisibilityView}
@@ -1,23 +0,0 @@
import ExpoModulesCore
public class ExpoBlueskyDevicePrefsModule: Module {
func getDefaults(_ useAppGroup: Bool) -> UserDefaults? {
if useAppGroup {
return UserDefaults(suiteName: "group.app.bsky")
} else {
return UserDefaults.standard
}
}
public func definition() -> ModuleDefinition {
Name("ExpoBlueskyDevicePrefs")
AsyncFunction("getStringValueAsync") { (key: String, useAppGroup: Bool) in
return self.getDefaults(useAppGroup)?.string(forKey: key)
}
AsyncFunction("setStringValueAsync") { (key: String, value: String?, useAppGroup: Bool) in
self.getDefaults(useAppGroup)?.setValue(value, forKey: key)
}
}
}
@@ -0,0 +1,46 @@
import ExpoModulesCore
public class ExpoPlatformInfoModule: Module {
private var prevAudioActive: Bool?
private var prevAudioCategory: AVAudioSession.Category?
public func definition() -> ModuleDefinition {
Name("ExpoPlatformInfo")
Function("getIsReducedMotionEnabled") {
return UIAccessibility.isReduceMotionEnabled
}
Function("setAudioCategory") { (audioCategoryString: String) in
let audioCategory = AVAudioSession.Category(rawValue: audioCategoryString)
if audioCategory == self.prevAudioCategory {
return
}
self.prevAudioCategory = audioCategory
DispatchQueue.global(qos: .background).async {
try? AVAudioSession.sharedInstance().setCategory(audioCategory)
}
}
Function("setAudioActive") { (active: Bool) in
if active == self.prevAudioActive {
return
}
self.prevAudioActive = active
if active {
DispatchQueue.global(qos: .background).async {
try? AVAudioSession.sharedInstance().setActive(true)
}
} else {
DispatchQueue.global(qos: .background).async {
try? AVAudioSession
.sharedInstance()
.setActive(
false,
options: [.notifyOthersOnDeactivation]
)
}
}
}
}
}
@@ -0,0 +1,62 @@
import Foundation
import ExpoModulesCore
public class ExpoBlueskySharedPrefsModule: Module {
let defaults = UserDefaults(suiteName: "group.app.bsky")
func getDefaults(_ info: String = "(no info)") -> UserDefaults? {
guard let defaults = self.defaults else {
NSLog("Failed to get defaults for app group: \(info)")
return nil
}
return defaults
}
public func definition() -> ModuleDefinition {
Name("ExpoBlueskySharedPrefs")
// JavaScripValue causes a crash when trying to check `isString()`. Let's
// explicitly define setString instead.
Function("setString") { (key: String, value: String?) in
SharedPrefs.shared.setValue(key, value)
}
Function("setValue") { (key: String, value: JavaScriptValue) in
if value.isNumber() {
SharedPrefs.shared.setValue(key, value.getDouble())
} else if value.isBool() {
SharedPrefs.shared.setValue(key, value.getBool())
} else if value.isNull() || value.isUndefined() {
SharedPrefs.shared.removeValue(key)
}
}
Function("removeValue") { (key: String) in
SharedPrefs.shared.removeValue(key)
}
Function("getString") { (key: String) in
return SharedPrefs.shared.getString(key)
}
Function("getBool") { (key: String) in
return SharedPrefs.shared.getBool(key)
}
Function("getNumber") { (key: String) in
return SharedPrefs.shared.getNumber(key)
}
Function("addToSet") { (key: String, value: String) in
SharedPrefs.shared.addToSet(key, value)
}
Function("removeFromSet") { (key: String, value: String) in
SharedPrefs.shared.removeFromSet(key, value)
}
Function("setContains") { (key: String, value: String) in
return SharedPrefs.shared.setContains(key, value)
}
}
}
@@ -0,0 +1,89 @@
import Foundation
public class SharedPrefs {
public static let shared = SharedPrefs()
private let defaults = UserDefaults(suiteName: "group.app.bsky")
init() {
if defaults == nil {
NSLog("Failed to get user defaults for app group.")
}
}
private func getDefaults(_ info: String = "(no info)") -> UserDefaults? {
guard let defaults = self.defaults else {
NSLog("Failed to get defaults for app group: \(info)")
return nil
}
return defaults
}
public func setValue(_ key: String, _ value: String?) {
getDefaults(key)?.setValue(value, forKey: key)
}
public func setValue(_ key: String, _ value: Double?) {
getDefaults(key)?.setValue(value, forKey: key)
}
public func setValue(_ key: String, _ value: Bool?) {
getDefaults(key)?.setValue(value, forKey: key)
}
public func _setAnyValue(_ key: String, _ value: Any?) {
getDefaults(key)?.setValue(value, forKey: key)
}
public func removeValue(_ key: String) {
getDefaults(key)?.removeObject(forKey: key)
}
public func getString(_ key: String) -> String? {
return getDefaults(key)?.string(forKey: key)
}
public func getNumber(_ key: String) -> Double? {
return getDefaults(key)?.double(forKey: key)
}
public func getBool(_ key: String) -> Bool? {
return getDefaults(key)?.bool(forKey: key)
}
public func addToSet(_ key: String, _ value: String) {
var dict: [String: Bool]?
if var currDict = getDefaults(key)?.dictionary(forKey: key) as? [String: Bool] {
currDict[value] = true
dict = currDict
} else {
dict = [
value: true
]
}
getDefaults(key)?.setValue(dict, forKey: key)
}
public func removeFromSet(_ key: String, _ value: String) {
guard var dict = getDefaults(key)?.dictionary(forKey: key) as? [String: Bool] else {
return
}
dict.removeValue(forKey: value)
getDefaults(key)?.setValue(dict, forKey: key)
}
public func setContains(_ key: String, _ value: String) -> Bool {
guard let dict = getDefaults(key)?.dictionary(forKey: key) as? [String: Bool] else {
return false
}
return dict[value] == true
}
public func hasValue(_ key: String) -> Bool {
return getDefaults(key)?.value(forKey: key) != nil
}
public func getValues(_ keys: [String]) -> [String: Any?]? {
return getDefaults("keys:\(keys)")?.dictionaryWithValues(forKeys: keys)
}
}
@@ -0,0 +1,21 @@
import ExpoModulesCore
public class ExpoBlueskyVisibilityViewModule: Module {
public func definition() -> ModuleDefinition {
Name("ExpoBlueskyVisibilityView")
AsyncFunction("updateActiveViewAsync") {
VisibilityViewManager.shared.updateActiveView()
}
View(VisibilityView.self) {
Events([
"onChangeStatus"
])
Prop("enabled") { (view: VisibilityView, prop: Bool) in
view.enabled = prop
}
}
}
}
@@ -0,0 +1,86 @@
import Foundation
class VisibilityViewManager {
static let shared = VisibilityViewManager()
private let views = NSHashTable<VisibilityView>(options: .weakMemory)
private var currentlyActiveView: VisibilityView?
private var screenHeight: CGFloat = UIScreen.main.bounds.height
private var prevCount = 0
func addView(_ view: VisibilityView) {
self.views.add(view)
if self.prevCount == 0 {
self.updateActiveView()
}
self.prevCount = self.views.count
}
func removeView(_ view: VisibilityView) {
self.views.remove(view)
self.prevCount = self.views.count
}
func updateActiveView() {
DispatchQueue.main.async {
var activeView: VisibilityView?
if self.views.count == 1 {
let view = self.views.allObjects[0]
if view.isViewableEnough() {
activeView = view
}
} else if self.views.count > 1 {
let views = self.views.allObjects
var mostVisibleView: VisibilityView?
var mostVisiblePosition: CGRect?
views.forEach { view in
if !view.isViewableEnough() {
return
}
guard let position = view.getPositionOnScreen() else {
return
}
if position.minY >= 150 {
if mostVisiblePosition == nil {
mostVisiblePosition = position
}
if let unwrapped = mostVisiblePosition,
position.minY <= unwrapped.minY {
mostVisibleView = view
mostVisiblePosition = position
}
}
}
activeView = mostVisibleView
}
if activeView == self.currentlyActiveView {
return
}
self.clearActiveView()
if let view = activeView {
self.setActiveView(view)
}
}
}
private func clearActiveView() {
if let currentlyActiveView = self.currentlyActiveView {
currentlyActiveView.setIsCurrentlyActive(isActive: false)
self.currentlyActiveView = nil
}
}
private func setActiveView(_ view: VisibilityView) {
view.setIsCurrentlyActive(isActive: true)
self.currentlyActiveView = view
}
}
@@ -0,0 +1,69 @@
import ExpoModulesCore
class VisibilityView: ExpoView {
var enabled = false {
didSet {
if enabled {
VisibilityViewManager.shared.removeView(self)
}
}
}
private let onChangeStatus = EventDispatcher()
private var isCurrentlyActiveView = false
required init(appContext: AppContext? = nil) {
super.init(appContext: appContext)
}
public override func willMove(toWindow newWindow: UIWindow?) {
super.willMove(toWindow: newWindow)
if !self.enabled {
return
}
if newWindow == nil {
VisibilityViewManager.shared.removeView(self)
} else {
VisibilityViewManager.shared.addView(self)
}
}
func setIsCurrentlyActive(isActive: Bool) {
if isCurrentlyActiveView == isActive {
return
}
self.isCurrentlyActiveView = isActive
self.onChangeStatus([
"isActive": isActive
])
}
}
// 🚨 DANGER 🚨
// These functions need to be called from the main thread. Xcode will warn you if you call one of them
// off the main thread, so pay attention!
extension UIView {
func getPositionOnScreen() -> CGRect? {
if let window = self.window {
return self.convert(self.bounds, to: window)
}
return nil
}
func isViewableEnough() -> Bool {
guard let window = self.window else {
return false
}
let viewFrameOnScreen = self.convert(self.bounds, to: window)
let screenBounds = window.bounds
let intersection = viewFrameOnScreen.intersection(screenBounds)
let viewHeight = viewFrameOnScreen.height
let intersectionHeight = intersection.height
return intersectionHeight >= 0.5 * viewHeight
}
}
@@ -1,18 +0,0 @@
import {requireNativeModule} from 'expo-modules-core'
const NativeModule = requireNativeModule('ExpoBlueskyDevicePrefs')
export function getStringValueAsync(
key: string,
useAppGroup?: boolean,
): Promise<string | null> {
return NativeModule.getStringValueAsync(key, useAppGroup)
}
export function setStringValueAsync(
key: string,
value: string | null,
useAppGroup?: boolean,
): Promise<void> {
return NativeModule.setStringValueAsync(key, value, useAppGroup)
}
@@ -1,16 +0,0 @@
import {NotImplementedError} from '../NotImplemented'
export function getStringValueAsync(
key: string,
useAppGroup?: boolean,
): Promise<string | null> {
throw new NotImplementedError({key, useAppGroup})
}
export function setStringValueAsync(
key: string,
value: string | null,
useAppGroup?: boolean,
): Promise<string | null> {
throw new NotImplementedError({key, value, useAppGroup})
}
@@ -0,0 +1,20 @@
import {Platform} from 'react-native'
import {requireNativeModule} from 'expo-modules-core'
import {AudioCategory} from './types'
const NativeModule = requireNativeModule('ExpoPlatformInfo')
export function getIsReducedMotionEnabled(): boolean {
return NativeModule.getIsReducedMotionEnabled()
}
export function setAudioActive(active: boolean): void {
if (Platform.OS !== 'ios') return
NativeModule.setAudioActive(active)
}
export function setAudioCategory(audioCategory: AudioCategory): void {
if (Platform.OS !== 'ios') return
NativeModule.setAudioCategory(audioCategory)
}
@@ -0,0 +1,25 @@
import {NotImplementedError} from '../NotImplemented'
import {AudioCategory} from './types'
export function getIsReducedMotionEnabled(): boolean {
throw new NotImplementedError()
}
/**
* Set whether the app's audio should mix with other apps' audio. Will also resume background music playback when `false`
* if it was previously playing.
* @param mixWithOthers
* @see https://developer.apple.com/documentation/avfaudio/avaudiosession/setactiveoptions/1616603-notifyothersondeactivation
*/
export function setAudioActive(active: boolean): void {
throw new NotImplementedError({active})
}
/**
* Set the audio category for the app.
* @param audioCategory
* @platform ios
*/
export function setAudioCategory(audioCategory: AudioCategory): void {
throw new NotImplementedError({audioCategory})
}
@@ -0,0 +1,17 @@
import {NotImplementedError} from '../NotImplemented'
import {AudioCategory} from './types'
export function getIsReducedMotionEnabled(): boolean {
if (typeof window === 'undefined') {
return false
}
return window.matchMedia('(prefers-reduced-motion: reduce)').matches
}
export function setAudioActive(active: boolean): void {
throw new NotImplementedError({active})
}
export function setAudioCategory(audioCategory: AudioCategory): void {
throw new NotImplementedError({audioCategory})
}
@@ -0,0 +1,15 @@
/**
* Sets the audio session category on iOS. In general, we should only need to use this for the `playback` and `ambient`
* categories. This enum however includes other categories that are available in the native API for clarity and
* potential future use.
* @see https://developer.apple.com/documentation/avfoundation/avaudiosession/category
* @platform ios
*/
export enum AudioCategory {
Ambient = 'AVAudioSessionCategoryAmbient',
Playback = 'AVAudioSessionCategoryPlayback',
_SoloAmbient = 'AVAudioSessionCategorySoloAmbient',
_Record = 'AVAudioSessionCategoryRecord',
_PlayAndRecord = 'AVAudioSessionCategoryPlayAndRecord',
_MultiRoute = 'AVAudioSessionCategoryMultiRoute',
}
@@ -1,9 +1,13 @@
import {requireNativeModule} from 'expo'
import {GooglePlayReferrerInfo} from './types'
import {GooglePlayReferrerInfo, ReferrerInfo} from './types'
export const NativeModule = requireNativeModule('ExpoBlueskyReferrer')
export function getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo> {
export function getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo | null> {
return NativeModule.getGooglePlayReferrerInfoAsync()
}
export function getReferrerInfo(): Promise<ReferrerInfo | null> {
return NativeModule.getReferrerInfo()
}
@@ -0,0 +1,37 @@
import {SharedPrefs} from '../../index'
import {NotImplementedError} from '../NotImplemented'
import {GooglePlayReferrerInfo, ReferrerInfo} from './types'
export function getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo> {
throw new NotImplementedError()
}
export function getReferrerInfo(): ReferrerInfo | null {
const referrer = SharedPrefs.getString('referrer')
if (referrer) {
SharedPrefs.removeValue('referrer')
try {
const url = new URL(referrer)
return {
referrer,
hostname: url.hostname,
}
} catch (e) {
return {
referrer,
hostname: referrer,
}
}
}
const referrerApp = SharedPrefs.getString('referrerApp')
if (referrerApp) {
SharedPrefs.removeValue('referrerApp')
return {
referrer: referrerApp,
hostname: referrerApp,
}
}
return null
}
@@ -1,7 +1,10 @@
import {NotImplementedError} from '../NotImplemented'
import {GooglePlayReferrerInfo} from './types'
import {GooglePlayReferrerInfo, ReferrerInfo} from './types'
// @ts-ignore throws
export function getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo> {
throw new NotImplementedError()
}
export function getReferrerInfo(): ReferrerInfo | null {
throw new NotImplementedError()
}
@@ -0,0 +1,33 @@
import {Platform} from 'react-native'
import {NotImplementedError} from '../NotImplemented'
import {GooglePlayReferrerInfo, ReferrerInfo} from './types'
export function getGooglePlayReferrerInfoAsync(): Promise<GooglePlayReferrerInfo> {
throw new NotImplementedError()
}
export function getReferrerInfo(): ReferrerInfo | null {
if (
Platform.OS === 'web' &&
// for ssr
typeof document !== 'undefined' &&
document != null &&
document.referrer
) {
try {
const url = new URL(document.referrer)
if (url.hostname !== 'bsky.app') {
return {
referrer: url.href,
hostname: url.hostname,
}
}
} catch {
// If something happens to the URL parsing, we don't want to actually cause any problems for the user. Just
// log the error so we might catch it
console.error('Failed to parse referrer URL')
}
}
return null
}
@@ -1,7 +1,10 @@
export type GooglePlayReferrerInfo =
| {
installReferrer?: string
clickTimestamp?: number
installTimestamp?: number
}
| undefined
export type GooglePlayReferrerInfo = {
installReferrer?: string
clickTimestamp?: number
installTimestamp?: number
}
export type ReferrerInfo = {
referrer: string
hostname: string
}
@@ -0,0 +1,51 @@
import {requireNativeModule} from 'expo-modules-core'
const NativeModule = requireNativeModule('ExpoBlueskySharedPrefs')
export function setValue(
key: string,
value: string | number | boolean | null | undefined,
): void {
// A bug on Android causes `JavaScripValue.isString()` to cause a crash on some occasions, seemingly because of a
// memory violation. Instead, we will use a specific function to set strings on this platform.
if (typeof value === 'string') {
return NativeModule.setString(key, value)
}
return NativeModule.setValue(key, value)
}
export function removeValue(key: string): void {
return NativeModule.removeValue(key)
}
export function getString(key: string): string | undefined {
return nullToUndefined(NativeModule.getString(key))
}
export function getNumber(key: string): number | undefined {
return nullToUndefined(NativeModule.getNumber(key))
}
export function getBool(key: string): boolean | undefined {
return nullToUndefined(NativeModule.getBool(key))
}
export function addToSet(key: string, value: string): void {
return NativeModule.addToSet(key, value)
}
export function removeFromSet(key: string, value: string): void {
return NativeModule.removeFromSet(key, value)
}
export function setContains(key: string, value: string): boolean {
return NativeModule.setContains(key, value)
}
// iOS returns `null` if a value does not exist, and Android returns `undefined. Normalize these here for JS types
function nullToUndefined(value: any) {
if (value == null) {
return undefined
}
return value
}
@@ -0,0 +1,36 @@
import {NotImplementedError} from '../NotImplemented'
export function setValue(
key: string,
value: string | number | boolean | null | undefined,
): void {
throw new NotImplementedError({key, value})
}
export function removeValue(key: string): void {
throw new NotImplementedError({key})
}
export function getString(key: string): string | null {
throw new NotImplementedError({key})
}
export function getNumber(key: string): number | null {
throw new NotImplementedError({key})
}
export function getBool(key: string): boolean | null {
throw new NotImplementedError({key})
}
export function addToSet(key: string, value: string): void {
throw new NotImplementedError({key, value})
}
export function removeFromSet(key: string, value: string): void {
throw new NotImplementedError({key, value})
}
export function setContains(key: string, value: string): boolean {
throw new NotImplementedError({key, value})
}

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