Compare commits

...

109 Commits

Author SHA1 Message Date
Eric Bailey 7637bc3f75 Update comment 2024-06-18 10:13:48 -05:00
Eric Bailey 86ebc38505 Or equal to 2024-06-18 10:08:30 -05:00
Eric Bailey 953b246571 Delete old code 2024-06-18 09:56:31 -05:00
Eric Bailey 946a770f30 Migrate to hooks 2024-06-18 09:56:22 -05:00
Eric Bailey abb01c7d80 Add hook 2024-06-18 09:48:30 -05:00
Eric Bailey 58adc0ef08 Add long format 2024-06-18 09:24:04 -05:00
Eric Bailey 8b436be50f Remove old test 2024-06-17 18:36:08 -05:00
Eric Bailey 5544e804e1 Split up tests, use date-fns as helpers 2024-06-17 18:35:48 -05:00
Jan Aagaard 69a9b2ecc5 Create a testable version of ago() and re-enable the disabled test (#4364)
* Enable the test of ago()

* Use test cases

This puts the input and the expected values next to each other.

* Create dateDiff function

This is a copy of ago(), but with the ability to specify the second date instead of using Date.now().

* Let ago() use dateDiff()

* Move constants close to usage

* Test dateDiff instead of ago

This makes it possible to test the dates without being forced to rely on what the current date is.

The commented out tests do not yet pass. This is fixed in later commits.

* Update dateDiff and enable the remaining tests
2024-06-17 17:35:37 -05:00
Eric Bailey 077da08309 Explore feed cards (#4521)
* Replace FeedSourceCard on Explore page

(cherry picked from commit e7e9787bfaa9368bfaeaaa4ca144ab77b438219c)

* Replace FeedSourceCard on Search page

(cherry picked from commit ac47aade7622d359eee9509763cda666d964d8a3)
2024-06-17 13:56:11 -05:00
Eric Bailey 7e88d0d7a6 Onboarding fixes (#4508)
* Add extra padding to bottom of profile step

* Make profile pic gen higher res
2024-06-17 13:21:35 -05:00
Eric Bailey 332524b7de Add native_pwi_disabled feature gate experiment (#4507)
* Add native_pwi_disabled feature gate experiment

* Use const
2024-06-17 13:21:09 -05:00
Eric Bailey f5f3bd8130 Select, don't mutate (#4541) 2024-06-17 19:22:39 +03:00
Hailey ba2fadb661 Don't show "Pin/Add" button on feed card w/ no session (#4539)
* pt 1

* tweak
2024-06-17 11:05:02 -05:00
Hailey 89be5a442c Add patch for RCTBaseTextInput fixing selectTextOnFocus prop (#4533)
* create patch

* remove js fix in `SearchScreen`
2024-06-17 10:56:02 +03:00
Joel 10c6035f31 Make lint-staged work in more envs (#4532)
When setting up the environment on a Windows machine with Git Bash, the pre-commit hook errored out:

> /usr/bin/bash: line 1: C:UsersNameFoldersocial-appnode_modules.binlint-staged: command not found

Seemingly, `yarn lint-staged` is not able to correctly resolve the path.

This commit changes the pre-commit hook to use `npx` instead of `yarn`, for 2 reasons:
1. It works on my machine (and is supposedly a more universal solution)
2. It is how it is done in the documentation: https://github.com/lint-staged/lint-staged?tab=readme-ov-file#examples

Co-authored-by: Joel <joel.garplind+github@gmail.com>
2024-06-16 13:42:15 +03:00
Hailey d0bb97e8a9 fix native post meta (#4530) 2024-06-16 12:23:48 +03:00
Eric Bailey eaef1446f2 Use URI as key (#4522) 2024-06-14 14:28:25 -05:00
Eric Bailey 5751014117 Feed source card (#4512)
* Pass event through click handlers

* Add FeedCard, use in Feeds screen

* Tweak space

* Don't contrain rt height

* Tweak space

* Fix type errors, don't pass event to fns that don't expect it

* Show unresolved RT prior to facet resolution
2024-06-14 14:24:04 -05:00
Minseo Lee 51a3e60132 Fix kawaii logo (#4505) 2024-06-14 10:39:08 -07:00
Eric Bailey 36e976fe5c Redo explore page (#4491)
* Redo explore page, wip

* Remove circle icons

* Load more styling

* Lower limit

* Some styling tweaks

* Abstract

* Add tab, query, factor out

* Revert unneeded change

* Revert unneeded change v2

* Update copy

* Load more styling

* Header styles

* The thin blue line

* Make sure it's hairline

* Update query keys

* Border

* Expand avis

* Very load more copy
2024-06-14 12:32:57 -05:00
dan 94c1f4968d Resolve patch-package warnings (#4520) 2024-06-14 18:01:31 +01:00
Eric Bailey f8c58a68a9 Fix count again (#4516) 2024-06-14 17:59:53 +01:00
Hailey 641a36c21d version bump (#4519) 2024-06-14 17:59:02 +01:00
Eric Bailey fe3f872d49 Add known followers to shadow cache (#4517) 2024-06-14 11:56:43 -05:00
Eric Bailey 4c0f037880 Reuse overfetching for popular feeds, add in existing filtering (#4501) 2024-06-14 11:56:29 -05:00
Samuel Newman bdeac28d74 Try/catch URL parsing of referrer (#4511) 2024-06-13 15:06:22 -07:00
Hailey d9066a6beb add document.referrer to statsig custom (#4509)
* add referrer to statsig custom

dont include referrer if hostname is bsky.app

save

add `document.referrer` to statsig custom

* add a hostname field

* account for ssr

* account for ssr
2024-06-13 23:31:19 +02:00
Samuel Newman d20c59e19d Calculate correct keyboard offset in composer (#4500)
* calculate correct keyboard offset

* give viewHeight a default value

* much simpler approach
2024-06-13 15:58:56 +01:00
dan 498e46ae4e Hide bio and social proof for blocked users (#4504) 2024-06-13 11:03:31 +02:00
Minseo Lee 7faa1d9131 KnownFollowers localization (#4494)
* Update KnownFollowers.tsx

* Update KnownFollowers.tsx

* Update KnownFollowers.tsx
2024-06-13 04:58:25 +02:00
Eric Bailey 28ba99917a Fix profile hover card blocked state (#4480)
* Fix: mini profile on hover allows following a blocker/blocked user (#4423) (#4440)

* Tweaks

---------

Co-authored-by: Michał Gołda <michal.golda@hotmail.com>
2024-06-13 04:53:32 +02:00
Eric Bailey d989128e5b Set profile hover prefetch stale time to 30s (#4417)
* Set prefetch stale time to 30s

* Run prefetch on mouseOver

* Only prefetch once on mousemove
2024-06-13 04:44:06 +02:00
Mary 247af5aee9 Prevent rich-formatting paste (#4327)
* fix: prevent rich-formatting paste

* fix: return true instead of preventDefault
2024-06-13 04:37:49 +02:00
dan ebd4f93b9c Show social proof in hovercards (#4502)
* Add social proof to hovercards

* Close it more reliably
2024-06-13 04:23:37 +02:00
Eric Bailey a55f924639 Just use server count (#4499)
* Just use server count

* Fix count
2024-06-12 16:52:00 -05:00
Hailey 99078bbff2 Don't show warning when sharing your own post in PWI opt-out mode (#4495) 2024-06-12 08:51:08 -07:00
Kuwa Lee 4e4a7bf18f Update Chinese localization (#4410)
* TW: Update and clean

* CN:run intl:extract

* 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: unify "登入"

* Update messages.po

* Update messages.po

* BOTH: fix "Post"

msgctxt "description"
msgid "Post"

* CN: Update translates

* CN: Remove superseded strings

* TW: Update and clean

* CN: fix msgid "This feed is empty."

---------

Co-authored-by: Frudrax Cheng <i@cynosura.one>
Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>
2024-06-11 17:17:47 -07:00
Paul Frazee 6736384ff4 Release 1.86 prep (#4490)
* Test updates

* Run intl extract
2024-06-11 16:20:26 -07:00
Eric Bailey bb0a6a4b6c Add KnownFollowers component to standard profile header (#4420)
* Add KnownFollowers component to standard profile header

* Prep for known followers screen

* Add known followers screen

* Tighten space

* Add pressed state

* Edit title

* Vertically center

* Don't show if no known followers

* Bump sdk

* Use actual followers.length to show

* Updates to show logic, space

* Prevent fresh data from applying to cached screens

* Tighten space

* Better label

* Oxford comma

* Fix count logic

* Add bskyweb route

* Useless ternary

* Minor spacing tweak

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2024-06-11 15:42:28 -07:00
Marco Maroni 7011ac8f72 Update italian localization (#4374)
* Update messages.po

Update italian localization

* Fix translation

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

Fix by @surfdude29

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

* Added some translation

---------

Co-authored-by: Marco Maroni <marcomaroni@gmail.com>
Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
2024-06-11 15:29:38 -07:00
Minseo Lee 815ab79309 Update Korean localization (#4387)
* 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-06-11 15:28:36 -07:00
surfdude29 43f6b68aa2 Update French localization (#4437)
* Update French localization

* use `point médian`

* Update messages.po

* add three new strings

* translate new string

* Apply suggestions from code review

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

* update revision date

---------

Co-authored-by: Stanislas Signoud <signez@stanisoft.net>
2024-06-11 15:27:34 -07:00
Kevin Scannell 0640364e0f Irish back to 100% (#4454) 2024-06-11 15:26:08 -07:00
Paul Frazee e1dcd2e434 Fix to thread load-more bug (#4488) 2024-06-11 15:01:13 -07:00
Eric Bailey a4ca4db35c Stringify path error (#4379) 2024-06-11 14:54:47 -07:00
Samuel Newman 3d4b390a8a Only enable keyboard controller when necessary (#4483)
* Only enable keyboard controller when necessary

* make it screen only

* rm keyboard padding

* rm keyboardpadding file

* revert using keyboard controller in composer

* remove styles.outer (unnecessary for revert)

* continue to use keyboard padding in the report dialog for dms

---------

Co-authored-by: Hailey <me@haileyok.com>
2024-06-11 12:50:56 -07:00
Paul Frazee aca0fa23ec Update feeds header to just have edit link (#4484) 2024-06-11 12:34:03 -07:00
Paul Frazee 46e12c6d34 Improve thread loading (#4402)
* Increase the number of posts loaded when a self-thread is present

* Increase depth to 10, detect cutoffs on self-threads and show continue link

* Stacky the avis
2024-06-11 11:30:38 -07:00
Eric Bailey 4b6609d48b Move feeds screen into common navigator, handle usages (#4365)
* Move feeds screen into common navigator, handle usages

* Add link to Feeds from home screen (#4366)

* Add link to feeds to home screen header

* Center logo

* Replace icons

* Tweak spacing

* Tweak spacing

* Swap icon, sizing

* Buttonize, size

* Make menu same alignment on all screens

* Remove FeedsTab support, enable drawer swipe on MessagesTab

* Add note

* Vertically align header

* Swap in Pin

* Use hashtag icon

* Remove png

* Fix reference

* Ensure alignment with home and other screens
2024-06-11 11:08:06 -07:00
Samuel Newman 3573f7ea40 rip out native translate (#4482) 2024-06-11 10:51:03 -07:00
Hailey fac5f6cdac register the push token after request has been approved on android (#4481)
* register the push token after request has been approved on android

* use a `0` string for badge count

* Revert "use a `0` string for badge count"

This reverts commit efac398618.

* temporary fix
2024-06-11 10:28:48 -07:00
Hailey 808dd3569d patch expo-notifications to clear badge (#4475) 2024-06-10 20:19:19 -07:00
Samuel Newman d85c8a0976 Revert to old modal on android (#4458)
* revert to old modal on android

* close alf dialogs before closing composer

* Try to fix white area

* Use hook

* Fix Back button

* oops

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2024-06-11 05:10:57 +02:00
Eric Bailey 14cddb7ec0 Sort profile feedgens by likes (#4380)
* Sort profile feedgens by likes

* Sort entire set as we get more pages

* Nah

This reverts commit 3b8714fd97.

* Update profile-feedgens.ts

---------

Co-authored-by: dan <dan.abramov@gmail.com>
2024-06-11 03:55:44 +02:00
Eric Bailey 848151c4f9 Revert "Fix undefined block (#4471)" (#4472)
This reverts commit 7cac413f3b.
2024-06-11 03:49:28 +02:00
Eric Bailey 7cac413f3b Fix undefined block (#4471)
* Fix undefined block (#4378)

* Fix undefined block

* Changing text and handling all blocks.

* Tweaks

* Hide follow button in hover card if user blocked

---------

Co-authored-by: Josh <hi@oracularhades.com>
2024-06-10 20:45:23 -05:00
Eric Bailey 212f5750e3 Open convo menu when tapping on chat with deleted account (#4470)
* Open menu on click

* Remove useless util

* Improve label

* s/press/click

* Tweak message

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2024-06-11 03:36:42 +02:00
Hailey c73ad43a80 stop using addPushTokenListener (#4467)
* stop using push token listener

* cleanup the hack

* add a comment

* remove test code

* Fix patch

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2024-06-11 03:16:21 +02:00
Samuel Newman 4e9a65200e add hitslop to repost button (#4469) 2024-06-10 16:43:04 -07:00
Paul Frazee 86e81650d3 Add a loading indicator to notifications when loading latest (#4468)
* Add a loading indicator to notifications when loading latest

* Adjust size and alignment
2024-06-11 01:17:34 +02:00
Paul Frazee 4efd576f6a Visually improve the empty state of feeds (#4466) 2024-06-10 13:44:21 -07:00
Paul Frazee c2d7d23423 Give a meaningful error in the composer when replying to a deleted post (#4464) 2024-06-10 22:02:00 +02:00
Paul Frazee 0404111f03 Fix: show known feed error UI more reliably (#4463) 2024-06-10 22:01:14 +02:00
Eric Bailey 7356763e49 Only validate persisted data for existing users (#4465) 2024-06-10 22:00:45 +02:00
Hailey 5dd195bcb7 Check canAskAgain for notification permissions (#4460) 2024-06-10 12:14:00 -07:00
Paul Frazee 90ec22a674 Add support for new-tab clicks on feeds (#4462) 2024-06-10 11:44:13 -07:00
Paul Frazee 59f49bef68 Override legacy language codes for indonesian, hebrew, and yiddish (#4461)
* Manually override incorrect 'in' to 'id' lang code

* Fix additional legacy language code issues
2024-06-10 20:37:14 +02:00
dan 620ab88713 Ungate mobile web notification badge (#4459) 2024-06-10 18:17:26 +02:00
Francesco Lodovici b688da8d58 Fix (#4430): Use separate hooks for shell mode animated styles (#4451)
* Fix (#4430): Use separate hooks for shell mode animated styles

* Consolidate in one file

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2024-06-10 17:19:28 +02:00
Samuel Newman fd03ea3fe1 Throttle instead of debounce (#4456)
* throttle instead of debounce

* trailing: true

* Fix throttle call

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2024-06-10 17:02:57 +02:00
Samuel Newman 1317d881ed debounce refetching listconvos (#4455) 2024-06-10 16:08:13 +02:00
dan 01b7a94a7e Patch to work around a crash in RN internals (#4426)
Co-authored-by: Hailey <me@haileyok.com>
2024-06-08 01:55:57 -07:00
dan a2d1cf68b9 Reliably focus keyboard on Android (#4427) 2024-06-07 22:27:16 -07:00
Hailey 5f63b8d40f Bump expo-notifications to fix NPE (#4425)
* bump library

* rename patch file
2024-06-08 03:17:00 +01:00
Hailey 480a40862f Use the proper logic on iOS to increment the badge (#4233) 2024-06-07 15:15:33 -07:00
Hailey c58aedf050 Bump 1.86.0 (#4411) 2024-06-07 20:00:50 +01:00
Samuel Newman 29a4a5f90c Composer - backup android focus (#4415)
* backup android focus

* bump to 300ms just to make sure it catches all of them
2024-06-07 13:32:38 +01:00
Hailey fefae27396 scope enable/disable of keyboard controller in composer to only android (#4412) 2024-06-06 22:29:34 -07:00
Minseo Lee 2540616f43 Fix horizontal padding of tab within profile header (#3924)
Co-authored-by: Hailey <me@haileyok.com>
2024-06-06 21:41:03 -07:00
Paul Frazee bd1e6fced9 1.85.1 2024-06-06 12:18:08 -07:00
Hailey 885ad2c756 disable keyboard controller in the composer screen (#4399)
* disable keyboard controller in the composer screen

* add comment

* oops

* destructure

* rename to keyboard context for clarity

* move destructure

* cleanup
2024-06-06 12:15:36 -07:00
Paul Frazee fef16e060f Fix notifications pagination on web (#4400)
* Fix notifications pagination on web

* Preserve larger threshold on native
2024-06-06 12:10:50 -07:00
dan ac25e43bef Patch Expo Modules for Android crash (#4398)
* Patch Expo Modules for Android crash

* rm intermediaries, add a readme

---------

Co-authored-by: Hailey <me@haileyok.com>
2024-06-06 11:21:38 -07:00
Hailey 094c08ea60 Revert "Change Android's MainActivity launchMode to singleTop" (#4397)
* Revert "Change Android's MainActivity launchMode to singleTop (#4255)"

This reverts commit 65ad16e394.

* Update bundle-deploy-eas-update.yml

---------

Co-authored-by: dan <dan.abramov@gmail.com>
2024-06-06 19:08:20 +01:00
dan 559764156d Fix tall image lightbox on Android (#4393) 2024-06-06 15:13:53 +01:00
dan 3801932902 Fix prettier (#4392) 2024-06-06 15:00:05 +01:00
Hailey 72f46ed734 cancel animations before updating value for min-shell animation (#4386)
* cancel animations before updating value

* comment
2024-06-06 14:28:02 +01:00
Samuel Newman 85e676257e Composer - make sure android keyboard opens (#4390)
* keep trying to open keyboard until it's open

* limit number of retries

* keep the original 50ms one as well

* Proper fix!

* disable autoFocus if not visible

* Reset derived state

* Revert "Reset derived state"

This reverts commit 71f57391ae.

* Use derived state pattern

* Rename for clarity

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2024-06-06 14:21:22 +01:00
dan 48796449ea Switch to withSpring to work around Reanimated regression (#4391) 2024-06-06 14:17:26 +01:00
dan 1f954c1065 Split ShareViaChatDialog (#4383) 2024-06-06 01:10:21 +01:00
dan 5d2119ffe5 Pin resolution of Statsig Expo dependencies (#4382)
* Force resolution of expo-constants

* Pin expo-device
2024-06-06 00:40:28 +01:00
Hailey 492c271a06 add legacy to picker for Android (#4377)
* add `legacy` to picker

* add for the other callsite
2024-06-05 16:27:57 -07:00
Hailey 42477d8bbb add notification count badge mobile web (#4375)
* add notification count badge mobile web

* rename var

* add gate to types

* implement gate

* nit

* Fix condition order, narrow types

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2024-06-05 18:13:42 +01:00
Hailey aefbab3850 Remove expo-constants (#4371) 2024-06-05 09:05:25 -07:00
Hailey af2aaa1f39 add types for desktopFixedHeight to List (#4356)
* add types for `desktopFixedHeight`

* nit

* accept both `number` and `boolean`
2024-06-05 09:03:40 -07:00
Paul Frazee 22858192dc Release 1.85 (#4372)
* Update tests

* Run intl extract
2024-06-04 20:00:21 -07:00
Minseo Lee 618a3c1ebe Update Korean localization (#4318)
* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Merge remote-tracking branch 'upstream/main' into update-korean-localization
2024-06-04 19:55:05 -07:00
Kevin Scannell 5e0452fc6b Update Irish translations back to 100% (#4306) 2024-06-04 19:54:38 -07:00
Stanislas Signoud 47033e5270 Update French localizations (#4223)
* Update French localizations

* Fix typos in French and misusage of "Ignorer"

Thanks @surfdude29 for the review!

Fixes #4296.

* Bump strings and remove unused one in French

* Translate three new missing strings in French

* Bump strings and trim unused old in French again

* Translate five new missing strings in French again
2024-06-04 19:53:59 -07:00
Takayuki KUSANO bb9afa9ce8 Upated Japanese translation (#4311)
* Delete obsoleted translated messages

* Translate new messages, update some messages

* Update Japanese translations
2024-06-04 19:52:58 -07:00
Frudrax Cheng 504bd28e80 Update Chinese Localization (#4323)
* Improve zh-TW translation

* improve and fix

* Update messages.po

msgid "Nevermind, create a handle for me"

* revert L3813

* 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: Fix and clean

msgid "Copy {0}"

msgid "Feeds are custom algorithms that users build with a little coding expertise. <0/> for more information."

* CN: Unify emoji

* TW: Update and unify some translation

* TW: unify "色情

* TW: update

* TW: typo

* CN:translated latest commit

* CN:removed superseded strings

* CN:fixed string 'left to go'

* CN:unified translation

* CN:translated latest commit

* CN:fix typo

---------

Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>
Co-authored-by: Kuwa Lee <kuwalee1069@gmail.com>
2024-06-04 19:52:08 -07:00
Hailey afbcac3ff3 use a timeout to focus the composer input (#4370)
* use a timeout to focus the composer input

* scope to just android

* scope useEffect to just android as well

* oops

* cleanup
2024-06-05 03:44:43 +01:00
Eric Bailey deea2f3839 Clarify some things in OTA docs (#4367)
* Clarify some things

* add github outputs script

* add slack notify android

* test slack android

* fix indent

* fix test

* sigh...

* Revert "fix test"

This reverts commit c99764464f.

* Revert "fix indent"

This reverts commit 4cce508d28.

* Revert "test slack android"

This reverts commit b02419b247.

* test ios workflow

* remove testing

* add slack info to docs

* use correct output for android

---------

Co-authored-by: Hailey <me@haileyok.com>
2024-06-04 18:55:18 -07:00
Paul Frazee 7613cdb89b Fix: visually indicate when quoted content is labeled (#4369) 2024-06-05 02:19:26 +01:00
Eric Bailey 3ece21cb45 [🙅] Integrate deactivate (#4308)
* Update types

(cherry picked from commit 27deac1f367825771ba76fa098ec1b0a62dcf64a)

* Integrate into deactivate dialog

(cherry picked from commit 84f299a447259cc1fbfc7be607e28197779e4ec1)

* Integrate into Deactivated screen

(cherry picked from commit 29193f34822ecdf11e2a407197fa230285dfe846)

* Bump api sdk

(cherry picked from commit 738c622d3e5a23bfbb0d3bdce3a6bdf01e54ca60)

* Update permalink

(cherry picked from commit c10bf5c071d76c3054bc4ce9d313c10b1820f038)

* Bump sdk pkg

* Update types to match backend

* Loosen types for forwards compat

* Hydrate status from persisted data

* Refresh session when re-activating, clear query cache

* Show app password error

* Refactor dialog to clear state when closed

* Add app password error to Deactivated screen
2024-06-05 02:02:22 +01:00
Eric Bailey e64b7cf698 Fix descenders cutoff in new chat dialog (#4359) 2024-06-04 20:51:38 +01:00
Eric Bailey a2d5343a87 Report persisted schema validation failures (#4358)
* Report persisted schema validation failures

* Make it safer
2024-06-04 20:51:28 +01:00
dan b5ac450442 Don't show profile labels until loaded (#4357) 2024-06-04 19:54:30 +01:00
185 changed files with 25044 additions and 20506 deletions
+9 -2
View File
@@ -34,6 +34,9 @@ jobs:
node-version-file: .nvmrc
cache: yarn
- name: 🪛 Setup jq
uses: dcarbone/install-jq-action@v2
- name: 🔨 Setup EAS
uses: expo/expo-github-action@v8
with:
@@ -96,13 +99,17 @@ jobs:
name: build-${{ steps.timestamp.outputs.time }}.apk
path: build.apk
- name: 📚 Get version from package.json
id: get-build-info
run: bash scripts/setGitHubOutput.sh
- name: 🔔 Notify Slack of Production Build
if: ${{ inputs.profile == 'production' }}
uses: slackapi/slack-github-action@v1.25.0
with:
payload: |
{
"text": "Android build is ready for submission. This is a production build! Download the artifact here: ${{ steps.upload-artifact-production.outputs.artifact-url }}"
"text": "Android production build for Google Play Store submission is ready!\n```Artifact: ${{ steps.upload-artifact-production.outputs.artifact-url }}\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}```"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
@@ -140,7 +147,7 @@ jobs:
with:
payload: |
{
"text": "Android production APK build is ready for download. This is a production build, and you should add it to the GitHub release! Download the artifact here: ${{ steps.upload-artifact-production-apk.outputs.artifact-url }}"
"text": "Android production build for GitHub/Obtanium is ready!\n```Artifact: ${{ steps.upload-artifact-production-apk.outputs.artifact-url }}\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }}```"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
+19
View File
@@ -34,6 +34,9 @@ jobs:
node-version-file: .nvmrc
cache: yarn
- name: 🪛 Setup jq
uses: dcarbone/install-jq-action@v2
- name: 🔨 Setup EAS
uses: expo/expo-github-action@v8
with:
@@ -81,6 +84,22 @@ jobs:
- name: 🚀 Deploy
run: eas submit -p ios --non-interactive --path build.ipa
- name: 📚 Get version from package.json
id: get-build-info
run: bash scripts/setGitHubOutput.sh
- name: 🔔 Notify Slack of Production Build
if: ${{ inputs.profile == 'production' }}
uses: slackapi/slack-github-action@v1.25.0
with:
payload: |
{
"text": "iOS production build for App Store submission is ready!\n```Artifact: Check TestFlight to know when it is available\nVersion Number: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }}\nBuild Number: ${{ steps.get-build-info.outputs.BSKY_IOS_BUILD_NUMBER }}```"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
- name: ⬇️ Restore Cache
id: get-base-commit
uses: actions/cache@v4
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
yarn lint-staged
npx lint-staged
+15 -4
View File
@@ -36,12 +36,15 @@ appId: xyz.blueskyweb.app
id: "viewHeaderDrawerBtn"
- tapOn:
id: "menuItemButton-Feeds"
- tapOn: "Edit Saved Feeds"
- tapOn:
id: "editFeedsBtn"
- tapOn:
label: "Tap on down arrow"
point: "79%,23%"
- tapOn:
id: "bottomBarHomeBtn"
id: "viewHeaderDrawerBtn"
- tapOn:
id: "viewHeaderDrawerBtn"
- assertVisible:
id: "homeScreenFeedTabs-selector-0"
text: "alice-favs"
@@ -54,11 +57,15 @@ appId: xyz.blueskyweb.app
id: "viewHeaderDrawerBtn"
- tapOn:
id: "menuItemButton-Feeds"
- tapOn:
id: "editFeedsBtn"
- tapOn:
label: "Tap on down arrow"
point: "79%,23%"
- tapOn:
id: "bottomBarHomeBtn"
id: "viewHeaderDrawerBtn"
- tapOn:
id: "viewHeaderDrawerBtn"
- assertVisible:
id: "homeScreenFeedTabs-selector-0"
text: "Following"
@@ -71,11 +78,15 @@ appId: xyz.blueskyweb.app
id: "viewHeaderDrawerBtn"
- tapOn:
id: "menuItemButton-Feeds"
- tapOn:
id: "editFeedsBtn"
- tapOn:
label: "Tap on unpin"
point: "91%,23%"
- tapOn:
id: "bottomBarHomeBtn"
id: "viewHeaderDrawerBtn"
- tapOn:
id: "viewHeaderDrawerBtn"
- assertVisible:
id: "homeScreenFeedTabs-selector-0"
text: "alice-favs"
+3 -1
View File
@@ -28,6 +28,8 @@ appId: xyz.blueskyweb.app
- tapOn: "Pin to Home"
- tapOn:
id: "bottomBarHomeBtn"
- tapOn:
id: "viewHeaderDrawerBtn"
- assertNotVisible: "Feeds ✨"
- tapOn:
@@ -50,7 +52,7 @@ appId: xyz.blueskyweb.app
text: "1"
- tapOn:
id: "repostBtn"
- tapOn: "Undo repost"
- tapOn: "Remove repost"
- assertNotVisible:
id: "repostCount"
-31
View File
@@ -1,31 +0,0 @@
# Remove this test when the old onboarding is deprecated
appId: xyz.blueskyweb.app
---
- runScript:
file: ../setupServer.js
env:
SERVER_PATH: "?users"
- runFlow:
file: ../setupApp.yml
- tapOn:
id: "e2eSignInAlice"
- tapOn:
id: "e2eStartLongboarding"
- tapOn: "Continue to next step"
- tapOn: "Continue to the next step without following any accounts"
- tapOn: Show replies in Following feed
- tapOn: Show quote-posts in Following feed
- tapOn: Show re-posts in Following feed
- tapOn: Show replies in Following feed
- waitForAnimationToEnd
- tapOn: Continue to next step
- waitForAnimationToEnd
- tapOn: "Continue to the next step"
- waitForAnimationToEnd
- tapOn: Continue to next step
- waitForAnimationToEnd
- tapOn: Continue to next step
- waitForAnimationToEnd
- tapOn: "Complete onboarding and start using your account"
- waitForAnimationToEnd
- assertVisible: Following
+2 -2
View File
@@ -58,7 +58,7 @@ appId: xyz.blueskyweb.app
id: "repostBtn"
childOf:
id: "postThreadItem-by-bob.test"
- tapOn: "Undo repost"
- tapOn: "Remove repost"
- assertNotVisible:
id: "repostCount-expanded"
@@ -77,7 +77,7 @@ appId: xyz.blueskyweb.app
id: "repostBtn"
childOf:
id: "postThreadItem-by-carla.test"
- tapOn: "Undo repost"
- tapOn: "Remove repost"
- assertNotVisible:
id: "repostCount"
childOf:
-74
View File
@@ -6,7 +6,6 @@ import {createFullHandle, makeValidHandle} from '../../src/lib/strings/handles'
import {enforceLen} from '../../src/lib/strings/helpers'
import {detectLinkables} from '../../src/lib/strings/rich-text-detection'
import {shortenLinks} from '../../src/lib/strings/rich-text-manip'
import {ago} from '../../src/lib/strings/time'
import {
makeRecordUri,
toNiceDomain,
@@ -142,79 +141,6 @@ describe('makeRecordUri', () => {
})
})
// FIXME: Reenable after fixing non-deterministic test.
describe.skip('ago', () => {
const oneYearDate = new Date(
new Date().setMonth(new Date().getMonth() - 11),
).setDate(new Date().getDate() - 28)
const inputs = [
1671461038,
'04 Dec 1995 00:12:00 GMT',
new Date(),
new Date().setSeconds(new Date().getSeconds() - 10),
new Date().setMinutes(new Date().getMinutes() - 10),
new Date().setHours(new Date().getHours() - 1),
new Date().setDate(new Date().getDate() - 1),
new Date().setDate(new Date().getDate() - 20),
new Date().setDate(new Date().getDate() - 25),
new Date().setDate(new Date().getDate() - 28),
new Date().setDate(new Date().getDate() - 29),
new Date().setDate(new Date().getDate() - 30),
new Date().setMonth(new Date().getMonth() - 1),
new Date(new Date().setMonth(new Date().getMonth() - 1)).setDate(
new Date().getDate() - 20,
),
new Date(new Date().setMonth(new Date().getMonth() - 1)).setDate(
new Date().getDate() - 25,
),
new Date(new Date().setMonth(new Date().getMonth() - 1)).setDate(
new Date().getDate() - 28,
),
new Date(new Date().setMonth(new Date().getMonth() - 1)).setDate(
new Date().getDate() - 29,
),
new Date().setMonth(new Date().getMonth() - 11),
new Date(new Date().setMonth(new Date().getMonth() - 11)).setDate(
new Date().getDate() - 20,
),
new Date(new Date().setMonth(new Date().getMonth() - 11)).setDate(
new Date().getDate() - 25,
),
oneYearDate,
]
const outputs = [
new Date(1671461038).toLocaleDateString(),
new Date('04 Dec 1995 00:12:00 GMT').toLocaleDateString(),
'now',
'10s',
'10m',
'1h',
'1d',
'20d',
'25d',
'28d',
'29d',
'1mo',
'1mo',
'1mo',
'1mo',
'2mo',
'2mo',
'11mo',
'11mo',
'11mo',
new Date(oneYearDate).toLocaleDateString(),
]
it('correctly calculates how much time passed, in a string', () => {
for (let i = 0; i < inputs.length; i++) {
const result = ago(inputs[i])
expect(result).toEqual(outputs[i])
}
})
})
describe('makeValidHandle', () => {
const inputs = [
'test-handle-123',
-1
View File
@@ -204,7 +204,6 @@ module.exports = function (config) {
],
'./plugins/withAndroidManifestPlugin.js',
'./plugins/withAndroidManifestFCMIconPlugin.js',
'./plugins/withAndroidManifestLaunchModePlugin.js',
'./plugins/withAndroidStylesWindowBackgroundPlugin.js',
'./plugins/withAndroidStylesAccentColorPlugin.js',
'./plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js',
@@ -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 21a1 1 0 0 1-.707-.293l-6-6a1 1 0 1 1 1.414-1.414L11 17.586V4a1 1 0 1 1 2 0v13.586l4.293-4.293a1 1 0 0 1 1.414 1.414l-6 6A1 1 0 0 1 12 21Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 285 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M4 5a1 1 0 0 0 0 2h16a1 1 0 1 0 0-2H4Zm0 12a1 1 0 1 0 0 2h3a1 1 0 1 0 0-2H4Zm-1-5a1 1 0 0 1 1-1h5a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1Zm14-3c.552 0 1 .41 1 .917V13.5h3.583c.507 0 .917.448.917 1s-.41 1-.917 1H18v3.583c0 .507-.448.917-1 .917s-1-.41-1-.917V15.5h-3.583c-.507 0-.917-.448-.917-1s.41-1 .917-1H16V9.917C16 9.41 16.448 9 17 9Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 471 B

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

After

Width:  |  Height:  |  Size: 299 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.5 3a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v3.997a6.25 6.25 0 0 0 1.83 4.42l.377.376A1 1 0 0 1 20 12.5V15a1 1 0 0 1-1 1h-6v5a1 1 0 1 1-2 0v-5H5a1 1 0 0 1-1-1v-2.5a1 1 0 0 1 .293-.707l.376-.377A6.25 6.25 0 0 0 6.5 6.996V3.001Zm2 1v2.997a8.25 8.25 0 0 1-2.416 5.834L6 12.914V14h12v-1.086l-.084-.083A8.25 8.25 0 0 1 15.5 6.997V4h-7Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 465 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M7 3a1 1 0 0 1 1 1v2h2a1 1 0 1 1 0 2H8v2a1 1 0 1 1-2 0V8H4a1 1 0 0 1 0-2h2V4a1 1 0 0 1 1-1Zm6 4a4 4 0 1 1 8 0 4 4 0 0 1-8 0Zm4-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4ZM3 14a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-6Zm2 1v4h4v-4H5Zm9.171-.829a1 1 0 0 1 1.415 0L17 15.585l1.414-1.414a1 1 0 1 1 1.414 1.414L18.414 17l1.414 1.414a1 1 0 0 1-1.414 1.414L17 18.414l-1.415 1.414a1 1 0 0 1-1.414-1.414l1.415-1.415-1.415-1.414a1 1 0 0 1 0-1.414Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 590 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M12.902 1.568a1 1 0 0 0-1.804 0L8.455 7.085l-6.085.799a1 1 0 0 0-.557 1.718l4.45 4.207-1.117 6.008a1 1 0 0 0 1.458 1.063L12 17.962l5.396 2.918a1 1 0 0 0 1.459-1.063l-1.117-6.008 4.45-4.207a1 1 0 0 0-.558-1.718l-6.085-.8-2.643-5.516Z"/></svg>

After

Width:  |  Height:  |  Size: 335 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 1a1 1 0 0 1 .902.568l2.643 5.517 6.085.799a1 1 0 0 1 .557 1.718l-4.45 4.207 1.118 6.008a1 1 0 0 1-1.46 1.063L12 17.962 6.604 20.88a1 1 0 0 1-1.459-1.063l1.117-6.008-4.45-4.207a1 1 0 0 1 .558-1.718l6.085-.8 2.643-5.516A1 1 0 0 1 12 1Zm0 3.315-1.975 4.123a1 1 0 0 1-.772.56l-4.538.595 3.317 3.137a1 1 0 0 1 .296.91l-.834 4.485 4.03-2.179a1 1 0 0 1 .952 0l4.03 2.179-.834-4.485a1 1 0 0 1 .296-.91l3.317-3.137-4.538-.596a1 1 0 0 1-.772-.56L12 4.316Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 592 B

+1
View File
@@ -207,6 +207,7 @@ func serve(cctx *cli.Context) error {
e.GET("/profile/:handleOrDID", server.WebProfile)
e.GET("/profile/:handleOrDID/follows", server.WebGeneric)
e.GET("/profile/:handleOrDID/followers", server.WebGeneric)
e.GET("/profile/:handleOrDID/known-followers", server.WebGeneric)
e.GET("/profile/:handleOrDID/lists/:rkey", server.WebGeneric)
e.GET("/profile/:handleOrDID/feed/:rkey", server.WebGeneric)
e.GET("/profile/:handleOrDID/feed/:rkey/liked-by", server.WebGeneric)
+22 -8
View File
@@ -14,15 +14,20 @@ diff results in incompatible native changes, a new client build will automatical
### Prerequisites
- Remove any internal client from your device and download the client from the App Store/Google Play. This will help for
testing as well as retrieving the build number.
- You should have signed in to EAS locally through npx eas login. You will need to modify the build number in a
subsequent step.
- Identify the build number of the production app you want to deploy an update for. iOS and Android build numbers are
divergent, so you will need to find both
- Find the latest production build number for both iOS and Android in Slack. These are listed in #client-builds
- Production builds always send the Version Number and Build Number in the Slack message. Search for the latest
production version number, and you should find the correct information.
![slack-build-info](./img/slack-build-info.png)
- It may also be useful to check the current production clients for these values. This will also help for testing. Note
that you will need to _fully_ remove the existing internal client build from your device, otherwise the given values in
the app may differ from the actual production values.
![app-build-number](./img/app-build-number.png)
- You should have signed in to EAS locally through npx eas login. You will need to modify the build number in a
subsequent step.
- Ensure that the commit the initial client was cut from is properly tagged in git. The tag should be in the format of 1.X.0
- Note: If the commit is not properly tagged, then the OTA deployment will simply fail since the GitHub Action will
not be able to find a commit to fingerprint and diff against.
@@ -38,14 +43,17 @@ to create your branch from, this should be properly set.
### Deployment
- Update the build number through EAS
- Update the build number through EAS to match the build numbers of the
production iOS/Android apps
- Note: This isnt strictly necessary, but having a step that takes you off of GitHub and into the terminal provides
a little “friction” to avoid fat fingering a release. Since there are legitimate reasons to just “click and deploy”
for internal builds, I felt it useful to make sure it doesnt accidentally become a prod deployment.
- Set the build number to the appropriate build number found in the prerequisite steps. Again, this should be the
- Set the build numbers to the values found in the prerequisite steps. Again, this should be the
build number for the current production release you want to deploy for.
- `npx eas build:version:set -p ios`
- `npx eas build:version:set -p android`
- These steps should spit out what the current build number is, save those values
for later too
- Run the deployment
- Navigate to https://github.com/bluesky-social/social-app/actions/workflows/bundle-deploy-eas-update.yml
- Select the “Run Workflow” dropdown
@@ -79,3 +87,9 @@ In about five minutes, the new deployment should be available for download. To t
- Launch the app once and wait approximately 15 seconds
- Relaunch the app
- Check the Settings page and scroll to the bottom. The commit hash should now be the latest commit on your deployed branch.
### Post Deployment
- Reset both platforms build numbers to what they were before the OTA
deployment. These values should have been logged by the EAS CLI when you
reset them to the production values prior to OTA.
Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

+7 -1
View File
@@ -1,4 +1,5 @@
import UserNotifications
import UIKit
let APP_GROUP = "group.app.bsky"
@@ -31,7 +32,12 @@ class NotificationService: UNNotificationServiceExtension {
}
func mutateWithBadge(_ content: UNMutableNotificationContent) {
content.badge = 1
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) {
@@ -66,5 +66,9 @@ class ExpoBackgroundNotificationHandlerModule : Module() {
AsyncFunction("removeManyFromStringArrayAsync") { forKey: String, strings: Array<String> ->
NotificationPrefs(appContext.reactContext).removeManyFromStringArray(forKey, strings)
}
AsyncFunction("setBadgeCountAsync") { _: Int ->
// This does nothing on Android
}
}
}
@@ -10,7 +10,8 @@ let DEFAULTS: [String:Any] = [
"playSoundQuote": false,
"playSoundReply": false,
"playSoundRepost": false,
"mutedThreads": [:] as! [String:[String]]
"mutedThreads": [:] as! [String:[String]],
"badgeCount": 0,
]
/*
@@ -112,5 +113,9 @@ public class ExpoBackgroundNotificationHandlerModule: Module {
userDefaults?.setValue(curr, forKey: forKey)
}
}
AsyncFunction("setBadgeCountAsync") { (count: Int) in
userDefaults?.setValue(count, forKey: "badgeCount")
}
}
}
@@ -31,6 +31,7 @@ export type ExpoBackgroundNotificationHandlerModule = {
forKey: keyof BackgroundNotificationHandlerPreferences,
value: string[],
) => Promise<void>
setBadgeCountAsync: (count: number) => Promise<void>
}
// TODO there are more preferences in the native code, however they have not been added here yet.
@@ -24,4 +24,5 @@ export const BackgroundNotificationHandler = {
removeFromStringArrayAsync: async (_: string, __: string) => {},
addManyToStringArrayAsync: async (_: string, __: string[]) => {},
removeManyFromStringArrayAsync: async (_: string, __: string[]) => {},
setBadgeCountAsync: async (_: number) => {},
} as ExpoBackgroundNotificationHandlerModule
@@ -1,6 +0,0 @@
{
"platforms": ["ios"],
"ios": {
"modules": ["ExpoBlueskyTranslateModule"]
}
}
-6
View File
@@ -1,6 +0,0 @@
export {
isAvailable,
isLanguageSupported,
NativeTranslationModule,
NativeTranslationView,
} from './src/ExpoBlueskyTranslateView'
@@ -1,20 +0,0 @@
import ExpoModulesCore
import SwiftUI
// Thanks to Andrew Levy for this code snippet
// https://github.com/andrew-levy/swiftui-react-native/blob/d3fbb2abf07601ff0d4b83055e7717bb980910d6/ios/Common/ExpoView%2BUIHostingController.swift
extension ExpoView {
func setupHostingController(_ hostingController: UIHostingController<some View>) {
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
hostingController.view.backgroundColor = .clear
addSubview(hostingController.view)
NSLayoutConstraint.activate([
hostingController.view.topAnchor.constraint(equalTo: self.topAnchor),
hostingController.view.bottomAnchor.constraint(equalTo: self.bottomAnchor),
hostingController.view.leftAnchor.constraint(equalTo: self.leftAnchor),
hostingController.view.rightAnchor.constraint(equalTo: self.rightAnchor),
])
}
}
@@ -1,21 +0,0 @@
Pod::Spec.new do |s|
s.name = 'ExpoBlueskyTranslate'
s.version = '1.0.0'
s.summary = 'Uses SwiftUI translation to translate text.'
s.description = 'Uses SwiftUI translation to translate text.'
s.author = ''
s.homepage = 'https://docs.expo.dev/modules/'
s.platforms = { :ios => '13.4' }
s.source = { git: '' }
s.static_framework = true
s.dependency 'ExpoModulesCore'
# Swift/Objective-C compatibility
s.pod_target_xcconfig = {
'DEFINES_MODULE' => 'YES',
'SWIFT_COMPILATION_MODE' => 'wholemodule'
}
s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
end
@@ -1,18 +0,0 @@
import ExpoModulesCore
import Foundation
import SwiftUI
public class ExpoBlueskyTranslateModule: Module {
public func definition() -> ModuleDefinition {
Name("ExpoBlueskyTranslate")
AsyncFunction("presentAsync") { (text: String) in
DispatchQueue.main.async { [weak state = TranslateViewState.shared] in
state?.isPresented = true
state?.text = text
}
}
View(ExpoBlueskyTranslateView.self) {}
}
}
@@ -1,22 +0,0 @@
import ExpoModulesCore
import Foundation
import SwiftUI
class TranslateViewState: ObservableObject {
static var shared = TranslateViewState()
@Published var isPresented = false
@Published var text = ""
}
class ExpoBlueskyTranslateView: ExpoView {
required init(appContext: AppContext? = nil) {
if #available(iOS 14.0, *) {
let hostingController = UIHostingController(rootView: TranslateView())
super.init(appContext: appContext)
setupHostingController(hostingController)
} else {
super.init(appContext: appContext)
}
}
}
@@ -1,31 +0,0 @@
import SwiftUI
// conditionally import the Translation module
#if canImport(Translation)
import Translation
#endif
struct TranslateView: View {
@ObservedObject var state = TranslateViewState.shared
var body: some View {
if #available(iOS 17.4, *) {
VStack {
UIViewRepresentableWrapper(view: UIView(frame: .zero))
}
.translationPresentation(
isPresented: $state.isPresented,
text: state.text
)
}
}
}
struct UIViewRepresentableWrapper: UIViewRepresentable {
let view: UIView
func makeUIView(context: Context) -> UIView {
return view
}
func updateUIView(_ uiView: UIView, context: Context) {}
}
@@ -1,3 +0,0 @@
export type ExpoBlueskyTranslateModule = {
presentAsync: (text: string) => Promise<void>
}
@@ -1,51 +0,0 @@
import React from 'react'
import {Platform} from 'react-native'
import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core'
import {ExpoBlueskyTranslateModule} from './ExpoBlueskyTranslate.types'
export const NativeTranslationModule =
requireNativeModule<ExpoBlueskyTranslateModule>('ExpoBlueskyTranslate')
const NativeView: React.ComponentType = requireNativeViewManager(
'ExpoBlueskyTranslate',
)
export function NativeTranslationView() {
return <NativeView />
}
// can be something like "17.5.1", so just take the first two parts
const version = String(Platform.Version).split('.').slice(0, 2).join('.')
export const isAvailable = Number(version) >= 17.4
// https://en.wikipedia.org/wiki/Translate_(Apple)#Languages
const SUPPORTED_LANGUAGES = [
'ar',
'zh',
'zh',
'nl',
'en',
'en',
'fr',
'de',
'id',
'it',
'ja',
'ko',
'pl',
'pt',
'ru',
'es',
'th',
'tr',
'uk',
'vi',
]
export function isLanguageSupported(lang?: string) {
// If the language is not provided, we assume it is supported
if (!lang) return true
return SUPPORTED_LANGUAGES.includes(lang)
}
@@ -1,13 +0,0 @@
export const NativeTranslationModule = {
presentAsync: async (_: string) => {},
}
export function NativeTranslationView() {
return null
}
export const isAvailable = false
export function isLanguageSupported(_lang?: string) {
return false
}
+5 -4
View File
@@ -1,6 +1,6 @@
{
"name": "bsky.app",
"version": "1.85.0",
"version": "1.87.0",
"private": true,
"engines": {
"node": ">=18"
@@ -49,7 +49,7 @@
"open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web"
},
"dependencies": {
"@atproto/api": "^0.12.14",
"@atproto/api": "^0.12.18",
"@bam.tech/react-native-image-resizer": "^3.0.4",
"@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
@@ -116,7 +116,6 @@
"expo-build-properties": "^0.12.1",
"expo-camera": "~15.0.9",
"expo-clipboard": "^6.0.3",
"expo-constants": "~16.0.1",
"expo-dev-client": "^4.0.14",
"expo-device": "~6.0.2",
"expo-file-system": "^17.0.1",
@@ -129,7 +128,7 @@
"expo-localization": "~15.0.3",
"expo-media-library": "~16.0.3",
"expo-navigation-bar": "~3.0.4",
"expo-notifications": "~0.28.3",
"expo-notifications": "~0.28.7",
"expo-sharing": "^12.0.1",
"expo-splash-screen": "~0.27.4",
"expo-status-bar": "~1.12.1",
@@ -272,6 +271,8 @@
"resolutions": {
"@types/react": "^18",
"**/zeed-dom": "0.10.9",
"**/expo-constants": "16.0.1",
"**/expo-device": "6.0.2",
"@react-native/babel-preset": "0.74.1"
},
"jest": {
@@ -1,9 +1,9 @@
diff --git a/node_modules/expo-haptics/android/src/main/java/expo/modules/haptics/HapticsModule.kt b/node_modules/expo-haptics/android/src/main/java/expo/modules/haptics/HapticsModule.kt
index 26c52af..b949a4c 100644
index 1520465..6ea988a 100644
--- a/node_modules/expo-haptics/android/src/main/java/expo/modules/haptics/HapticsModule.kt
+++ b/node_modules/expo-haptics/android/src/main/java/expo/modules/haptics/HapticsModule.kt
@@ -42,7 +42,7 @@ class HapticsModule : Module() {
private fun vibrate(type: HapticsVibrationType) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- vibrator.vibrate(VibrationEffect.createWaveform(type.timings, type.amplitudes, -1))
+5
View File
@@ -0,0 +1,5 @@
## expo-modules-core Patch
This patch fixes crashes seen in some Android clients when using intents to open the app. See https://github.com/expo/expo/pull/29513.
Do not remove this patch until that PR lands in Expo and is released.
+14
View File
@@ -0,0 +1,14 @@
diff --git a/node_modules/expo-modules-core/android/src/main/java/expo/modules/adapters/react/NativeModulesProxy.java b/node_modules/expo-modules-core/android/src/main/java/expo/modules/adapters/react/NativeModulesProxy.java
index bb74e80..0aa0202 100644
--- a/node_modules/expo-modules-core/android/src/main/java/expo/modules/adapters/react/NativeModulesProxy.java
+++ b/node_modules/expo-modules-core/android/src/main/java/expo/modules/adapters/react/NativeModulesProxy.java
@@ -90,8 +90,8 @@ public class NativeModulesProxy extends ReactContextBaseJavaModule {
mModuleRegistry.ensureIsInitialized();
KotlinInteropModuleRegistry kotlinModuleRegistry = getKotlinInteropModuleRegistry();
- kotlinModuleRegistry.emitOnCreate();
kotlinModuleRegistry.installJSIInterop();
+ kotlinModuleRegistry.emitOnCreate();
Map<String, Object> constants = new HashMap<>(3);
constants.put(MODULES_CONSTANTS_KEY, new HashMap<>());
@@ -1,29 +1,54 @@
diff --git a/node_modules/expo-notifications/android/build.gradle b/node_modules/expo-notifications/android/build.gradle
index d233e1f..cc2f856 100644
index b863077..8b5209e 100644
--- a/node_modules/expo-notifications/android/build.gradle
+++ b/node_modules/expo-notifications/android/build.gradle
@@ -32,6 +32,7 @@ dependencies {
api 'com.google.firebase:firebase-messaging:22.0.0'
api 'me.leolin:ShortcutBadger:1.1.22@aar'
+ implementation project(':expo-background-notification-handler')
if (project.findProject(':expo-modules-test-core')) {
testImplementation project(':expo-modules-test-core')
diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/badge/BadgeHelper.kt b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/badge/BadgeHelper.kt
index 63a46c5..f911834 100644
--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/badge/BadgeHelper.kt
+++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/badge/BadgeHelper.kt
@@ -1,5 +1,6 @@
package expo.modules.notifications.badge
+import android.app.NotificationManager
import android.content.Context
import android.util.Log
import me.leolin.shortcutbadger.ShortcutBadgeException
@@ -12,7 +13,12 @@ object BadgeHelper {
fun setBadgeCount(context: Context, badgeCount: Int): Boolean {
return try {
- ShortcutBadger.applyCountOrThrow(context.applicationContext, badgeCount)
+ if (badgeCount == 0) {
+ val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.cancelAll()
+ } else {
+ ShortcutBadger.applyCountOrThrow(context.applicationContext, badgeCount)
+ }
BadgeHelper.badgeCount = badgeCount
true
} catch (e: ShortcutBadgeException) {
diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/JSONNotificationContentBuilder.java b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/JSONNotificationContentBuilder.java
index 0af7fe0..8f2c8d8 100644
--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/JSONNotificationContentBuilder.java
+++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/JSONNotificationContentBuilder.java
@@ -14,6 +14,7 @@ import expo.modules.notifications.notifications.enums.NotificationPriority;
import expo.modules.notifications.notifications.model.NotificationContent;
public class JSONNotificationContentBuilder extends NotificationContent.Builder {
+ private static final String CHANNEL_ID_KEY = "channelId";
private static final String TITLE_KEY = "title";
private static final String TEXT_KEY = "message";
private static final String SUBTITLE_KEY = "subtitle";
@@ -36,6 +37,7 @@ public class JSONNotificationContentBuilder extends NotificationContent.Builder
public NotificationContent.Builder setPayload(JSONObject payload) {
this.setTitle(getTitle(payload))
+ .setChannelId(getChannelId(payload))
@@ -33,7 +58,7 @@ index 0af7fe0..8f2c8d8 100644
@@ -60,6 +62,14 @@ public class JSONNotificationContentBuilder extends NotificationContent.Builder
return this;
}
+ protected String getChannelId(JSONObject payload) {
+ try {
+ return payload.getString(CHANNEL_ID_KEY);
@@ -46,7 +71,7 @@ index 0af7fe0..8f2c8d8 100644
try {
return payload.getString(TITLE_KEY);
diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java
index f1fed19..80afe9e 100644
index f1fed19..012757b 100644
--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java
+++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java
@@ -20,6 +20,7 @@ import expo.modules.notifications.notifications.enums.NotificationPriority;
@@ -60,10 +85,10 @@ index f1fed19..80afe9e 100644
@@ -50,6 +51,11 @@ public class NotificationContent implements Parcelable, Serializable {
}
};
+ @Nullable
+ public String getChannelId() {
+ return mTitle;
+ return mChannelId;
+ }
+
@Nullable
@@ -71,14 +96,14 @@ index f1fed19..80afe9e 100644
return mTitle;
@@ -121,6 +127,7 @@ public class NotificationContent implements Parcelable, Serializable {
}
protected NotificationContent(Parcel in) {
+ mChannelId = in.readString();
mTitle = in.readString();
mText = in.readString();
mSubtitle = in.readString();
@@ -146,6 +153,7 @@ public class NotificationContent implements Parcelable, Serializable {
@Override
public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mChannelId);
@@ -87,7 +112,7 @@ index f1fed19..80afe9e 100644
dest.writeString(mSubtitle);
@@ -166,6 +174,7 @@ public class NotificationContent implements Parcelable, Serializable {
private static final long serialVersionUID = 397666843266836802L;
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
+ out.writeObject(mChannelId);
out.writeObject(mTitle);
@@ -95,7 +120,7 @@ index f1fed19..80afe9e 100644
out.writeObject(mSubtitle);
@@ -190,6 +199,7 @@ public class NotificationContent implements Parcelable, Serializable {
}
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
+ mChannelId = (String) in.readObject();
mTitle = (String) in.readObject();
@@ -103,7 +128,7 @@ index f1fed19..80afe9e 100644
mSubtitle = (String) in.readObject();
@@ -240,6 +250,7 @@ public class NotificationContent implements Parcelable, Serializable {
}
public static class Builder {
+ private String mChannelId;
private String mTitle;
@@ -112,7 +137,7 @@ index f1fed19..80afe9e 100644
@@ -260,6 +271,11 @@ public class NotificationContent implements Parcelable, Serializable {
useDefaultVibrationPattern();
}
+ public Builder setChannelId(String channelId) {
+ mChannelId = channelId;
+ return this;
@@ -122,7 +147,7 @@ index f1fed19..80afe9e 100644
mTitle = title;
return this;
@@ -336,6 +352,7 @@ public class NotificationContent implements Parcelable, Serializable {
public NotificationContent build() {
NotificationContent content = new NotificationContent();
+ content.mChannelId = mChannelId;
@@ -134,16 +159,16 @@ index 6bd9928..ee93d70 100644
--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.java
+++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.java
@@ -48,6 +48,10 @@ public class ExpoNotificationBuilder extends ChannelAwareNotificationBuilder {
NotificationContent content = getNotificationContent();
+ if (content.getChannelId() != null) {
+ builder.setChannelId(content.getChannelId());
+ }
+
builder.setAutoCancel(content.isAutoDismiss());
builder.setOngoing(content.isSticky());
diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt
index 55b3a8d..1b99d5b 100644
--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt
@@ -158,7 +183,7 @@ index 55b3a8d..1b99d5b 100644
import org.json.JSONObject
import java.lang.ref.WeakReference
import java.util.*
-open class FirebaseMessagingDelegate(protected val context: Context) : FirebaseMessagingDelegate {
+open class FirebaseMessagingDelegate(protected val context: Context) : FirebaseMessagingDelegate, BackgroundNotificationHandlerInterface {
companion object {
@@ -166,7 +191,7 @@ index 55b3a8d..1b99d5b 100644
// than by static properties. Fortunately, using weak references we can
@@ -89,12 +92,21 @@ open class FirebaseMessagingDelegate(protected val context: Context) : FirebaseM
fun getBackgroundTasks() = sBackgroundTaskConsumerReferences.values.mapNotNull { it.get() }
override fun onMessageReceived(remoteMessage: RemoteMessage) {
- NotificationsService.receive(context, createNotification(remoteMessage))
- getBackgroundTasks().forEach {
@@ -181,7 +206,7 @@ index 55b3a8d..1b99d5b 100644
+ }
}
}
+ override fun showMessage(remoteMessage: RemoteMessage) {
+ NotificationsService.receive(context, createNotification(remoteMessage))
+ }
@@ -7,3 +7,8 @@ in `onMessageReceived` are sent to the module for handling.
It also allows us to set the Android notification channel ID from the notification `data`, rather
than the `notification` object in the payload.
### `setBadgeCountAsync` fix on Android
`ShortcutBadger`'s `setCount` doesn't work for clearing the badge on Android for some reason. Instead, let's use the
Android API for clearing the badge.
@@ -1,11 +1,11 @@
diff --git a/node_modules/expo-updates/ios/EXUpdates/Update/ExpoUpdatesUpdate.swift b/node_modules/expo-updates/ios/EXUpdates/Update/ExpoUpdatesUpdate.swift
index b85291e..07a5d3c 100644
index b85291e..546709d 100644
--- a/node_modules/expo-updates/ios/EXUpdates/Update/ExpoUpdatesUpdate.swift
+++ b/node_modules/expo-updates/ios/EXUpdates/Update/ExpoUpdatesUpdate.swift
@@ -78,13 +78,20 @@ public final class ExpoUpdatesUpdate: Update {
status = UpdateStatus.StatusPending
}
+ // Instead of relying on various hacks to get the correct format for the specific
+ // platform on the backend, we can just add this little patch..
+ let dateFormatter = DateFormatter()
+50 -8
View File
@@ -1,3 +1,29 @@
diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm
index b0d71dc..9974932 100644
--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm
+++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm
@@ -377,10 +377,6 @@ - (void)textInputDidBeginEditing
self.backedTextInputView.attributedText = [NSAttributedString new];
}
- if (_selectTextOnFocus) {
- [self.backedTextInputView selectAll:nil];
- }
-
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeFocus
reactTag:self.reactTag
text:[self.backedTextInputView.attributedText.string copy]
@@ -611,6 +607,10 @@ - (UIView *)reactAccessibilityElement
- (void)reactFocus
{
[self.backedTextInputView reactFocus];
+
+ if (_selectTextOnFocus) {
+ [self.backedTextInputView selectAll:nil];
+ }
}
- (void)reactBlur
diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h
index e9b330f..1ecdf0a 100644
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h
@@ -5,7 +31,7 @@ index e9b330f..1ecdf0a 100644
@@ -16,4 +16,6 @@
@property (nonatomic, copy) RCTDirectEventBlock onRefresh;
@property (nonatomic, weak) UIScrollView *scrollView;
+- (void)forwarderBeginRefreshing;
+
@end
@@ -16,7 +42,7 @@ index b09e653..4c32b31 100644
@@ -198,9 +198,53 @@ - (void)refreshControlValueChanged
[self setCurrentRefreshingState:super.refreshing];
_refreshingProgrammatically = NO;
+ if (@available(iOS 17.4, *)) {
+ if (_currentRefreshingState) {
+ UIImpactFeedbackGenerator *feedbackGenerator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight];
@@ -29,7 +55,7 @@ index b09e653..4c32b31 100644
_onRefresh(nil);
}
}
+/*
+ This method is used by Bluesky's ExpoScrollForwarder. This allows other React Native
+ libraries to perform a refresh of a scrollview and access the refresh control's onRefresh
@@ -38,15 +64,15 @@ index b09e653..4c32b31 100644
+- (void)forwarderBeginRefreshing
+{
+ _refreshingProgrammatically = NO;
+
+
+ [self sizeToFit];
+
+
+ if (!self.scrollView) {
+ return;
+ }
+
+
+ UIScrollView *scrollView = (UIScrollView *)self.scrollView;
+
+
+ [UIView animateWithDuration:0.3
+ delay:0
+ options:UIViewAnimationOptionBeginFromCurrentState
@@ -58,7 +84,7 @@ index b09e653..4c32b31 100644
+ completion:^(__unused BOOL finished) {
+ [super beginRefreshing];
+ [self setCurrentRefreshingState:super.refreshing];
+
+
+ if (self->_onRefresh) {
+ self->_onRefresh(nil);
+ }
@@ -67,3 +93,19 @@ index b09e653..4c32b31 100644
+}
+
@end
diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java
index 5f5e1ab..aac00b6 100644
--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java
+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java
@@ -99,8 +99,9 @@ public class JavaTimerManager {
}
// If the JS thread is busy for multiple frames we cancel any other pending runnable.
- if (mCurrentIdleCallbackRunnable != null) {
- mCurrentIdleCallbackRunnable.cancel();
+ IdleCallbackRunnable currentRunnable = mCurrentIdleCallbackRunnable;
+ if (currentRunnable != null) {
+ currentRunnable.cancel();
}
mCurrentIdleCallbackRunnable = new IdleCallbackRunnable(frameTimeNanos);
+7
View File
@@ -11,3 +11,10 @@ in the RN repo: https://github.com/facebook/react-native/issues/43388
Patching `RCTRefreshControl.m` and `RCTRefreshControl.h` to add a new `forwarderBeginRefreshing` method to the class.
This method is used by `ExpoScrollForwarder` to initiate a refresh of the underlying `UIScrollView` from inside that
module.
## TextInput Patch - `selectTextOnFocus` fix
Patching `RCTBaseTextInputView.m` to fix an issue where `selectTextOnFocus` does not work as expected on iOS 17. This
patch _only_ fixes the Paper version of `TextInput`. If we migrate to Fabric and the fix has not been made upstream,
we can apply the same fix. See https://github.com/facebook/react-native/pull/44307.
@@ -1,17 +0,0 @@
const {withAndroidManifest} = require('expo/config-plugins')
module.exports = function withAndroidManifestLaunchModePlugin(appConfig) {
return withAndroidManifest(appConfig, function (decoratedAppConfig) {
try {
const mainApplication =
decoratedAppConfig.modResults.manifest.application[0]
const mainActivity = mainApplication.activity.find(
elem => elem.$['android:name'] === '.MainActivity',
)
mainActivity.$['android:launchMode'] = 'singleTop'
} catch (e) {
console.error(`withAndroidManifestLaunchModePlugin failed`, e)
}
return decoratedAppConfig
})
}
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
outputIos=$(eas build:version:get -p ios)
outputAndroid=$(eas build:version:get -p android)
BSKY_IOS_BUILD_NUMBER=${outputIos#*buildNumber - }
BSKY_ANDROID_VERSION_CODE=${outputAndroid#*versionCode - }
echo PACKAGE_VERSION="$(jq -r '.version' package.json)" > "$GITHUB_OUTPUT"
echo BSKY_IOS_BUILD_NUMBER=$BSKY_IOS_BUILD_NUMBER >> "$GITHUB_OUTPUT"
echo BSKY_ANDROID_VERSION_CODE=$BSKY_ANDROID_VERSION_CODE >> "$GITHUB_OUTPUT"
+9 -2
View File
@@ -14,7 +14,11 @@ import * as SplashScreen from 'expo-splash-screen'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
import {
initialize,
Provider as StatsigProvider,
tryFetchGates,
} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {MessagesProvider} from '#/state/messages'
import {init as initPersistedState} from '#/state/persisted'
@@ -69,6 +73,9 @@ function InnerApp() {
try {
if (account) {
await resumeSession(account)
} else {
await initialize()
await tryFetchGates(undefined, 'prefer-fresh-gates')
}
} catch (e) {
logger.error(`session: resume failed`, {message: e})
@@ -143,7 +150,7 @@ function App() {
* that is set up in the InnerApp component above.
*/
return (
<KeyboardProvider enabled={true} statusBarTranslucent={true}>
<KeyboardProvider enabled={false} statusBarTranslucent={true}>
<SessionProvider>
<ShellStateProvider>
<PrefsStateProvider>
+9 -26
View File
@@ -22,7 +22,6 @@ import {buildStateObject} from 'lib/routes/helpers'
import {
AllNavigatorParams,
BottomTabNavigatorParams,
FeedsTabNavigatorParams,
FlatNavigatorParams,
HomeTabNavigatorParams,
MessagesTabNavigatorParams,
@@ -42,6 +41,7 @@ import {PreferencesThreads} from 'view/screens/PreferencesThreads'
import {SavedFeeds} from 'view/screens/SavedFeeds'
import HashtagScreen from '#/screens/Hashtag'
import {ModerationScreen} from '#/screens/Moderation'
import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers'
import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
import {init as initAnalytics} from './lib/analytics/analytics'
import {useWebScrollRestoration} from './lib/hooks/useWebScrollRestoration'
@@ -91,7 +91,6 @@ const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
const HomeTab = createNativeStackNavigatorWithAuth<HomeTabNavigatorParams>()
const SearchTab = createNativeStackNavigatorWithAuth<SearchTabNavigatorParams>()
const FeedsTab = createNativeStackNavigatorWithAuth<FeedsTabNavigatorParams>()
const NotificationsTab =
createNativeStackNavigatorWithAuth<NotificationsTabNavigatorParams>()
const MyProfileTab =
@@ -171,6 +170,13 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
title: title(msg`People followed by @${route.params.name}`),
})}
/>
<Stack.Screen
name="ProfileKnownFollowers"
getComponent={() => ProfileKnownFollowersScreen}
options={({route}) => ({
title: title(msg`Followers of @${route.params.name} that you know`),
})}
/>
<Stack.Screen
name="ProfileList"
getComponent={() => ProfileListScreen}
@@ -306,6 +312,7 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
getComponent={() => MessagesSettingsScreen}
options={{title: title(msg`Chat settings`), requireAuth: true}}
/>
<Stack.Screen name="Feeds" getComponent={() => FeedsScreen} />
</>
)
}
@@ -330,7 +337,6 @@ function TabsNavigator() {
tabBar={tabBar}>
<Tab.Screen name="HomeTab" getComponent={() => HomeTabNavigator} />
<Tab.Screen name="SearchTab" getComponent={() => SearchTabNavigator} />
<Tab.Screen name="FeedsTab" getComponent={() => FeedsTabNavigator} />
<Tab.Screen
name="NotificationsTab"
getComponent={() => NotificationsTabNavigator}
@@ -384,24 +390,6 @@ function SearchTabNavigator() {
)
}
function FeedsTabNavigator() {
const pal = usePalette('default')
return (
<FeedsTab.Navigator
screenOptions={{
animation: isAndroid ? 'ios' : undefined,
animationDuration: 285,
gestureEnabled: true,
fullScreenGestureEnabled: true,
headerShown: false,
contentStyle: pal.view,
}}>
<FeedsTab.Screen name="Feeds" getComponent={() => FeedsScreen} />
{commonScreens(FeedsTab as typeof HomeTab)}
</FeedsTab.Navigator>
)
}
function NotificationsTabNavigator() {
const pal = usePalette('default')
return (
@@ -505,11 +493,6 @@ const FlatNavigator = () => {
getComponent={() => SearchScreen}
options={{title: title(msg`Search`)}}
/>
<Flat.Screen
name="Feeds"
getComponent={() => FeedsScreen}
options={{title: title(msg`Feeds`)}}
/>
<Flat.Screen
name="Notifications"
getComponent={() => NotificationsScreen}
+205
View File
@@ -0,0 +1,205 @@
import React from 'react'
import {GestureResponderEvent, View} from 'react-native'
import {AppBskyActorDefs, AppBskyFeedDefs, AtUri} from '@atproto/api'
import {msg, plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logger} from '#/logger'
import {
useAddSavedFeedsMutation,
usePreferencesQuery,
useRemoveFeedMutation,
} from '#/state/queries/preferences'
import {sanitizeHandle} from 'lib/strings/handles'
import {useSession} from 'state/session'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import * as Toast from 'view/com/util/Toast'
import {useTheme} from '#/alf'
import {atoms as a} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import {useRichText} from '#/components/hooks/useRichText'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
import {Link as InternalLink} from '#/components/Link'
import {Loader} from '#/components/Loader'
import * as Prompt from '#/components/Prompt'
import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography'
export function Default({feed}: {feed: AppBskyFeedDefs.GeneratorView}) {
return (
<Link feed={feed}>
<Outer>
<Header>
<Avatar src={feed.avatar} />
<TitleAndByline title={feed.displayName} creator={feed.creator} />
<Action uri={feed.uri} pin />
</Header>
<Description description={feed.description} />
<Likes count={feed.likeCount || 0} />
</Outer>
</Link>
)
}
export function Link({
children,
feed,
}: {
children: React.ReactElement
feed: AppBskyFeedDefs.GeneratorView
}) {
const href = React.useMemo(() => {
const urip = new AtUri(feed.uri)
const handleOrDid = feed.creator.handle || feed.creator.did
return `/profile/${handleOrDid}/feed/${urip.rkey}`
}, [feed])
return <InternalLink to={href}>{children}</InternalLink>
}
export function Outer({children}: {children: React.ReactNode}) {
return <View style={[a.flex_1, a.gap_md]}>{children}</View>
}
export function Header({children}: {children: React.ReactNode}) {
return <View style={[a.flex_row, a.align_center, a.gap_md]}>{children}</View>
}
export function Avatar({src}: {src: string | undefined}) {
return <UserAvatar type="algo" size={40} avatar={src} />
}
export function TitleAndByline({
title,
creator,
}: {
title: string
creator: AppBskyActorDefs.ProfileViewBasic
}) {
const t = useTheme()
return (
<View style={[a.flex_1]}>
<Text
style={[a.text_md, a.font_bold, a.flex_1, a.leading_snug]}
numberOfLines={1}>
{title}
</Text>
<Text
style={[a.flex_1, a.leading_snug, t.atoms.text_contrast_medium]}
numberOfLines={1}>
<Trans>Feed by {sanitizeHandle(creator.handle, '@')}</Trans>
</Text>
</View>
)
}
export function Description({description}: {description?: string}) {
const [rt, isResolving] = useRichText(description || '')
if (!description) return null
return isResolving ? (
<RichText value={description} style={[a.leading_snug]} />
) : (
<RichText value={rt} style={[a.leading_snug]} />
)
}
export function Likes({count}: {count: number}) {
const t = useTheme()
return (
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
{plural(count || 0, {
one: 'Liked by # user',
other: 'Liked by # users',
})}
</Text>
)
}
export function Action({uri, pin}: {uri: string; pin?: boolean}) {
const {hasSession} = useSession()
if (!hasSession) return null
return <ActionInner uri={uri} pin={pin} />
}
function ActionInner({uri, pin}: {uri: string; pin?: boolean}) {
const {_} = useLingui()
const {data: preferences} = usePreferencesQuery()
const {isPending: isAddSavedFeedPending, mutateAsync: saveFeeds} =
useAddSavedFeedsMutation()
const {isPending: isRemovePending, mutateAsync: removeFeed} =
useRemoveFeedMutation()
const savedFeedConfig = React.useMemo(() => {
return preferences?.savedFeeds?.find(
feed => feed.type === 'feed' && feed.value === uri,
)
}, [preferences?.savedFeeds, uri])
const removePromptControl = Prompt.usePromptControl()
const isPending = isAddSavedFeedPending || isRemovePending
const toggleSave = React.useCallback(
async (e: GestureResponderEvent) => {
e.preventDefault()
e.stopPropagation()
try {
if (savedFeedConfig) {
await removeFeed(savedFeedConfig)
} else {
await saveFeeds([
{
type: 'feed',
value: uri,
pinned: pin || false,
},
])
}
Toast.show(_(msg`Feeds updated!`))
} catch (e: any) {
logger.error(e, {context: `FeedCard: failed to update feeds`, pin})
Toast.show(_(msg`Failed to update feeds`))
}
},
[_, pin, saveFeeds, removeFeed, uri, savedFeedConfig],
)
const onPrompRemoveFeed = React.useCallback(
async (e: GestureResponderEvent) => {
e.preventDefault()
e.stopPropagation()
removePromptControl.open()
},
[removePromptControl],
)
return (
<>
<Button
disabled={isPending}
label={_(msg`Add this feed to your feeds`)}
size="small"
variant="ghost"
color="secondary"
shape="square"
onPress={savedFeedConfig ? onPrompRemoveFeed : toggleSave}>
{savedFeedConfig ? (
<ButtonIcon size="md" icon={isPending ? Loader : Trash} />
) : (
<ButtonIcon size="md" icon={isPending ? Loader : Plus} />
)}
</Button>
<Prompt.Basic
control={removePromptControl}
title={_(msg`Remove from my feeds?`)}
description={_(
msg`Are you sure you want to remove this from your feeds?`,
)}
onConfirm={toggleSave}
confirmButtonCta={_(msg`Remove`)}
confirmButtonColor="negative"
/>
</>
)
}
@@ -5,7 +5,7 @@ import Animated, {
useSharedValue,
} from 'react-native-reanimated'
export function KeyboardPadding({maxHeight}: {maxHeight?: number}) {
export function KeyboardControllerPadding({maxHeight}: {maxHeight?: number}) {
const keyboardHeight = useSharedValue(0)
useKeyboardHandler(
@@ -0,0 +1,7 @@
export function KeyboardControllerPadding({
maxHeight: _,
}: {
maxHeight?: number
}) {
return null
}
-3
View File
@@ -1,3 +0,0 @@
export function KeyboardPadding({maxHeight: _}: {maxHeight?: number}) {
return null
}
+204
View File
@@ -0,0 +1,204 @@
import React from 'react'
import {View} from 'react-native'
import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
import {msg, Plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme} from '#/alf'
import {Link, LinkProps} from '#/components/Link'
import {Text} from '#/components/Typography'
const AVI_SIZE = 30
const AVI_BORDER = 1
/**
* Shared logic to determine if `KnownFollowers` should be shown.
*
* Checks the # of actual returned users instead of the `count` value, because
* `count` includes blocked users and `followers` does not.
*/
export function shouldShowKnownFollowers(
knownFollowers?: AppBskyActorDefs.KnownFollowers,
) {
return knownFollowers && knownFollowers.followers.length > 0
}
export function KnownFollowers({
profile,
moderationOpts,
onLinkPress,
}: {
profile: AppBskyActorDefs.ProfileViewDetailed
moderationOpts: ModerationOpts
onLinkPress?: LinkProps['onPress']
}) {
const cache = React.useRef<Map<string, AppBskyActorDefs.KnownFollowers>>(
new Map(),
)
/*
* Results for `knownFollowers` are not sorted consistently, so when
* revalidating we can see a flash of this data updating. This cache prevents
* this happening for screens that remain in memory. When pushing a new
* screen, or once this one is popped, this cache is empty, so new data is
* displayed.
*/
if (profile.viewer?.knownFollowers && !cache.current.has(profile.did)) {
cache.current.set(profile.did, profile.viewer.knownFollowers)
}
const cachedKnownFollowers = cache.current.get(profile.did)
if (cachedKnownFollowers && shouldShowKnownFollowers(cachedKnownFollowers)) {
return (
<KnownFollowersInner
profile={profile}
cachedKnownFollowers={cachedKnownFollowers}
moderationOpts={moderationOpts}
onLinkPress={onLinkPress}
/>
)
}
return null
}
function KnownFollowersInner({
profile,
moderationOpts,
cachedKnownFollowers,
onLinkPress,
}: {
profile: AppBskyActorDefs.ProfileViewDetailed
moderationOpts: ModerationOpts
cachedKnownFollowers: AppBskyActorDefs.KnownFollowers
onLinkPress?: LinkProps['onPress']
}) {
const t = useTheme()
const {_} = useLingui()
const textStyle = [
a.flex_1,
a.text_sm,
a.leading_snug,
t.atoms.text_contrast_medium,
]
const slice = cachedKnownFollowers.followers.slice(0, 3).map(f => {
const moderation = moderateProfile(f, moderationOpts)
return {
profile: {
...f,
displayName: sanitizeDisplayName(
f.displayName || f.handle,
moderation.ui('displayName'),
),
},
moderation,
}
})
const count = cachedKnownFollowers.count
return (
<Link
label={_(
msg`Press to view followers of this account that you also follow`,
)}
onPress={onLinkPress}
to={makeProfileLink(profile, 'known-followers')}
style={[
a.flex_1,
a.flex_row,
a.gap_md,
a.align_center,
{marginLeft: -AVI_BORDER},
]}>
{({hovered, pressed}) => (
<>
<View
style={[
{
height: AVI_SIZE,
width: AVI_SIZE + (slice.length - 1) * a.gap_md.gap,
},
pressed && {
opacity: 0.5,
},
]}>
{slice.map(({profile: prof, moderation}, i) => (
<View
key={prof.did}
style={[
a.absolute,
a.rounded_full,
{
borderWidth: AVI_BORDER,
borderColor: t.atoms.bg.backgroundColor,
width: AVI_SIZE + AVI_BORDER * 2,
height: AVI_SIZE + AVI_BORDER * 2,
left: i * a.gap_md.gap,
zIndex: AVI_BORDER - i,
},
]}>
<UserAvatar
size={AVI_SIZE}
avatar={prof.avatar}
moderation={moderation.ui('avatar')}
/>
</View>
))}
</View>
<Text
style={[
textStyle,
hovered && {
textDecorationLine: 'underline',
textDecorationColor: t.atoms.text_contrast_medium.color,
},
pressed && {
opacity: 0.5,
},
]}
numberOfLines={2}>
{count > 2 ? (
<Trans>
Followed by{' '}
<Text key={slice[0].profile.did} style={textStyle}>
{slice[0].profile.displayName}
</Text>
,{' '}
<Text key={slice[1].profile.did} style={textStyle}>
{slice[1].profile.displayName}
</Text>
, and{' '}
<Plural value={count - 2} one="# other" other="# others" />
</Trans>
) : count === 2 ? (
<Trans>
Followed by{' '}
<Text key={slice[0].profile.did} style={textStyle}>
{slice[0].profile.displayName}
</Text>{' '}
and{' '}
<Text key={slice[1].profile.did} style={textStyle}>
{slice[1].profile.displayName}
</Text>
</Trans>
) : (
<Trans>
Followed by{' '}
<Text key={slice[0].profile.did} style={textStyle}>
{slice[0].profile.displayName}
</Text>
</Trans>
)}
</Text>
</>
)}
</Link>
)
}
+3 -11
View File
@@ -12,7 +12,8 @@ import {
isExternalUrl,
linkRequiresWarning,
} from '#/lib/strings/url-helpers'
import {isNative, isWeb} from '#/platform/detection'
import {isNative} from '#/platform/detection'
import {shouldClickOpenNewTab} from '#/platform/urls'
import {useModalControls} from '#/state/modals'
import {useOpenLink} from '#/state/preferences/in-app-browser'
import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped'
@@ -116,16 +117,7 @@ export function useLink({
if (isExternal) {
openLink(href)
} else {
/**
* A `GestureResponderEvent`, but cast to `any` to avoid using a bunch
* of @ts-ignore below.
*/
const event = e as any
const isMiddleClick = isWeb && event.button === 1
const isMetaKey =
isWeb &&
(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey)
const shouldOpenInNewTab = isMetaKey || isMiddleClick
const shouldOpenInNewTab = shouldClickOpenNewTab(e)
if (isBskyDownloadUrl(href)) {
shareUrl(BSKY_DOWNLOAD_URL)
+83 -26
View File
@@ -5,6 +5,7 @@ import {flip, offset, shift, size, useFloating} from '@floating-ui/react-dom'
import {msg, plural} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {getModerationCauseKey} from '#/lib/moderation'
import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
@@ -22,11 +23,16 @@ import {useFollowMethods} from '#/components/hooks/useFollowMethods'
import {useRichText} from '#/components/hooks/useRichText'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {
KnownFollowers,
shouldShowKnownFollowers,
} from '#/components/KnownFollowers'
import {InlineLinkText, Link} from '#/components/Link'
import {Loader} from '#/components/Loader'
import {Portal} from '#/components/Portal'
import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography'
import {ProfileLabel} from '../moderation/ProfileHeaderAlerts'
import {ProfileHoverCardProps} from './types'
const floatingMiddlewares = [
@@ -45,10 +51,23 @@ const floatingMiddlewares = [
]
export function ProfileHoverCard(props: ProfileHoverCardProps) {
const prefetchProfileQuery = usePrefetchProfileQuery()
const prefetchedProfile = React.useRef(false)
const onPointerMove = () => {
if (!prefetchedProfile.current) {
prefetchedProfile.current = true
prefetchProfileQuery(props.did)
}
}
if (props.disable || isTouchDevice) {
return props.children
} else {
return <ProfileHoverCardInner {...props} />
return (
<View onPointerMove={onPointerMove}>
<ProfileHoverCardInner {...props} />
</View>
)
}
}
@@ -370,7 +389,10 @@ function Inner({
profile: profileShadow,
logContext: 'ProfileHoverCard',
})
const blockHide = profile.viewer?.blocking || profile.viewer?.blockedBy
const isBlockedUser =
profile.viewer?.blocking ||
profile.viewer?.blockedBy ||
profile.viewer?.blockingByList
const following = formatCount(profile.followsCount || 0)
const followers = formatCount(profile.followersCount || 0)
const pluralizedFollowers = plural(profile.followersCount || 0, {
@@ -401,29 +423,41 @@ function Inner({
/>
</Link>
{!isMe && (
<Button
size="small"
color={profileShadow.viewer?.following ? 'secondary' : 'primary'}
variant="solid"
label={
profileShadow.viewer?.following
? _(msg`Following`)
: _(msg`Follow`)
}
style={[a.rounded_full]}
onPress={profileShadow.viewer?.following ? unfollow : follow}>
<ButtonIcon
position="left"
icon={profileShadow.viewer?.following ? Check : Plus}
/>
<ButtonText>
{profileShadow.viewer?.following
? _(msg`Following`)
: _(msg`Follow`)}
</ButtonText>
</Button>
)}
{!isMe &&
(isBlockedUser ? (
<Link
to={profileURL}
label={_(msg`View blocked user's profile`)}
onPress={hide}
size="small"
color="secondary"
variant="solid"
style={[a.rounded_full]}>
<ButtonText>{_(msg`View profile`)}</ButtonText>
</Link>
) : (
<Button
size="small"
color={profileShadow.viewer?.following ? 'secondary' : 'primary'}
variant="solid"
label={
profileShadow.viewer?.following
? _(msg`Following`)
: _(msg`Follow`)
}
style={[a.rounded_full]}
onPress={profileShadow.viewer?.following ? unfollow : follow}>
<ButtonIcon
position="left"
icon={profileShadow.viewer?.following ? Check : Plus}
/>
<ButtonText>
{profileShadow.viewer?.following
? _(msg`Following`)
: _(msg`Follow`)}
</ButtonText>
</Button>
))}
</View>
<Link to={profileURL} label={_(msg`View profile`)} onPress={hide}>
@@ -439,7 +473,19 @@ function Inner({
</View>
</Link>
{!blockHide && (
{isBlockedUser && (
<View style={[a.flex_row, a.flex_wrap, a.gap_xs]}>
{moderation.ui('profileView').alerts.map(cause => (
<ProfileLabel
key={getModerationCauseKey(cause)}
cause={cause}
disableDetailsDialog
/>
))}
</View>
)}
{!isBlockedUser && (
<>
<View style={[a.flex_row, a.flex_wrap, a.gap_md, a.pt_xs]}>
<InlineLinkText
@@ -473,6 +519,17 @@ function Inner({
/>
</View>
) : undefined}
{!isMe &&
shouldShowKnownFollowers(profile.viewer?.knownFollowers) && (
<View style={[a.flex_row, a.align_center, a.gap_sm, a.pt_md]}>
<KnownFollowers
profile={profile}
moderationOpts={moderationOpts}
onLinkPress={hide}
/>
</View>
)}
</>
)}
</View>
+10 -7
View File
@@ -1,10 +1,10 @@
import React from 'react'
import {View} from 'react-native'
import {GestureResponderEvent, View} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonColor, ButtonText} from '#/components/Button'
import {Button, ButtonColor, ButtonProps, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {Text} from '#/components/Typography'
@@ -136,7 +136,7 @@ export function Action({
* Note: The dialog will close automatically when the action is pressed, you
* should NOT close the dialog as a side effect of this method.
*/
onPress: () => void
onPress: ButtonProps['onPress']
color?: ButtonColor
/**
* Optional i18n string. If undefined, it will default to "Confirm".
@@ -147,9 +147,12 @@ export function Action({
const {_} = useLingui()
const {gtMobile} = useBreakpoints()
const {close} = Dialog.useDialogContext()
const handleOnPress = React.useCallback(() => {
close(onPress)
}, [close, onPress])
const handleOnPress = React.useCallback(
(e: GestureResponderEvent) => {
close(() => onPress?.(e))
},
[close, onPress],
)
return (
<Button
@@ -186,7 +189,7 @@ export function Basic({
* Note: The dialog will close automatically when the action is pressed, you
* should NOT close the dialog as a side effect of this method.
*/
onConfirm: () => void
onConfirm: ButtonProps['onPress']
confirmButtonColor?: ButtonColor
showCancel?: boolean
}>) {
@@ -15,7 +15,6 @@ import * as Dialog from '#/components/Dialog'
import * as Toggle from '#/components/forms/Toggle'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
import {KeyboardPadding} from '#/components/KeyboardPadding'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import {ReportDialogProps} from './types'
@@ -222,7 +221,6 @@ export function SubmitView({
{submitting && <ButtonIcon icon={Loader} />}
</Button>
</View>
<KeyboardPadding />
</View>
)
}
-2
View File
@@ -28,7 +28,6 @@ import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Has
import {PageText_Stroke2_Corner0_Rounded as PageText} from '#/components/icons/PageText'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {KeyboardPadding} from '#/components/KeyboardPadding'
import {Loader} from '#/components/Loader'
import * as Prompt from '#/components/Prompt'
import {Text} from '#/components/Typography'
@@ -257,7 +256,6 @@ function MutedWordsInner() {
</View>
<Dialog.Close />
<KeyboardPadding maxHeight={100} />
</Dialog.ScrollableInner>
)
}
+1 -1
View File
@@ -49,7 +49,7 @@ export function LeaveConvoPrompt({
)}
confirmButtonCta={_(msg`Leave`)}
confirmButtonColor="negative"
onConfirm={leaveConvo}
onConfirm={() => leaveConvo()}
/>
)
}
+2
View File
@@ -16,6 +16,7 @@ import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {KeyboardControllerPadding} from '#/components/KeyboardControllerPadding'
import {Button, ButtonIcon, ButtonText} from '../Button'
import {Divider} from '../Divider'
import {ChevronLeft_Stroke2_Corner0_Rounded as Chevron} from '../icons/Chevron'
@@ -47,6 +48,7 @@ let ReportDialog = ({
<Dialog.ScrollableInner label={_(msg`Report this message`)}>
<DialogInner params={params} />
<Dialog.Close />
<KeyboardControllerPadding />
</Dialog.ScrollableInner>
</Dialog.Outer>
)
@@ -395,11 +395,13 @@ function ProfileCard({
/>
<View style={[a.flex_1, a.gap_2xs]}>
<Text
style={[t.atoms.text, a.font_bold, a.leading_snug]}
style={[t.atoms.text, a.font_bold, a.leading_tight]}
numberOfLines={1}>
{displayName}
</Text>
<Text style={t.atoms.text_contrast_high} numberOfLines={2}>
<Text
style={[a.leading_tight, t.atoms.text_contrast_high]}
numberOfLines={2}>
{!enabled ? <Trans>{handle} can't be messaged</Trans> : handle}
</Text>
</View>
@@ -15,8 +15,24 @@ export function SendViaChatDialog({
control: Dialog.DialogControlProps
onSelectChat: (chatId: string) => void
}) {
const {_} = useLingui()
return (
<Dialog.Outer
control={control}
testID="sendViaChatChatDialog"
nativeOptions={{sheet: {snapPoints: ['100%']}}}>
<SendViaChatDialogInner control={control} onSelectChat={onSelectChat} />
</Dialog.Outer>
)
}
function SendViaChatDialogInner({
control,
onSelectChat,
}: {
control: Dialog.DialogControlProps
onSelectChat: (chatId: string) => void
}) {
const {_} = useLingui()
const {mutate: createChat} = useGetConvoForMembers({
onSuccess: data => {
onSelectChat(data.convo.id)
@@ -39,15 +55,10 @@ export function SendViaChatDialog({
)
return (
<Dialog.Outer
control={control}
testID="sendViaChatChatDialog"
nativeOptions={{sheet: {snapPoints: ['100%']}}}>
<SearchablePeopleList
title={_(msg`Send post to...`)}
onSelectChat={onCreateChat}
showRecentConvos
/>
</Dialog.Outer>
<SearchablePeopleList
title={_(msg`Send post to...`)}
onSelectChat={onCreateChat}
showRecentConvos
/>
)
}
+4
View File
@@ -7,3 +7,7 @@ export const ArrowTopRight_Stroke2_Corner0_Rounded = createSinglePathSVG({
export const ArrowLeft_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M3 12a1 1 0 0 1 .293-.707l6-6a1 1 0 0 1 1.414 1.414L6.414 11H20a1 1 0 1 1 0 2H6.414l4.293 4.293a1 1 0 0 1-1.414 1.414l-6-6A1 1 0 0 1 3 12Z',
})
export const ArrowBottom_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M12 21a1 1 0 0 1-.707-.293l-6-6a1 1 0 1 1 1.414-1.414L11 17.586V4a1 1 0 1 1 2 0v13.586l4.293-4.293a1 1 0 0 1 1.414 1.414l-6 6A1 1 0 0 1 12 21Z',
})
+9
View File
@@ -0,0 +1,9 @@
import {createSinglePathSVG} from './TEMPLATE'
/*
* This icon is off-menu, not part of the icon set
*/
export const ListPlus_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M4 5a1 1 0 0 0 0 2h16a1 1 0 1 0 0-2H4Zm0 12a1 1 0 1 0 0 2h3a1 1 0 1 0 0-2H4Zm-1-5a1 1 0 0 1 1-1h5a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1Zm14-3c.552 0 1 .41 1 .917V13.5h3.583c.507 0 .917.448.917 1s-.41 1-.917 1H18v3.583c0 .507-.448.917-1 .917s-1-.41-1-.917V15.5h-3.583c-.507 0-.917-.448-.917-1s.41-1 .917-1H16V9.917C16 9.41 16.448 9 17 9Z',
})
+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Menu_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M2 6a1 1 0 0 1 1-1h18a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1Zm0 6a1 1 0 0 1 1-1h18a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1Zm0 6a1 1 0 0 1 1-1h18a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1Z',
})
+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Pin_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M6.5 3a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v3.997a6.25 6.25 0 0 0 1.83 4.42l.377.376A1 1 0 0 1 20 12.5V15a1 1 0 0 1-1 1h-6v5a1 1 0 1 1-2 0v-5H5a1 1 0 0 1-1-1v-2.5a1 1 0 0 1 .293-.707l.376-.377A6.25 6.25 0 0 0 6.5 6.996V3.001Zm2 1v2.997a8.25 8.25 0 0 1-2.416 5.834L6 12.914V14h12v-1.086l-.084-.083A8.25 8.25 0 0 1 15.5 6.997V4h-7Z',
})
+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Shapes_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M7 3a1 1 0 0 1 1 1v2h2a1 1 0 1 1 0 2H8v2a1 1 0 1 1-2 0V8H4a1 1 0 0 1 0-2h2V4a1 1 0 0 1 1-1Zm6 4a4 4 0 1 1 8 0 4 4 0 0 1-8 0Zm4-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4ZM3 14a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-6Zm2 1v4h4v-4H5Zm9.171-.829a1 1 0 0 1 1.415 0L17 15.585l1.414-1.414a1 1 0 1 1 1.414 1.414L18.414 17l1.414 1.414a1 1 0 0 1-1.414 1.414L17 18.414l-1.415 1.414a1 1 0 0 1-1.414-1.414l1.415-1.415-1.415-1.414a1 1 0 0 1 0-1.414Z',
})
+9
View File
@@ -0,0 +1,9 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Star_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M12 1a1 1 0 0 1 .902.568l2.643 5.517 6.085.799a1 1 0 0 1 .557 1.718l-4.45 4.207 1.118 6.008a1 1 0 0 1-1.46 1.063L12 17.962 6.604 20.88a1 1 0 0 1-1.459-1.063l1.117-6.008-4.45-4.207a1 1 0 0 1 .558-1.718l6.085-.8 2.643-5.516A1 1 0 0 1 12 1Zm0 3.315-1.975 4.123a1 1 0 0 1-.772.56l-4.538.595 3.317 3.137a1 1 0 0 1 .296.91l-.834 4.485 4.03-2.179a1 1 0 0 1 .952 0l4.03 2.179-.834-4.485a1 1 0 0 1 .296-.91l3.317-3.137-4.538-.596a1 1 0 0 1-.772-.56L12 4.316Z',
})
export const Star_Filled_Corner0_Rounded = createSinglePathSVG({
path: 'M12.902 1.568a1 1 0 0 0-1.804 0L8.455 7.085l-6.085.799a1 1 0 0 0-.557 1.718l4.45 4.207-1.117 6.008a1 1 0 0 0 1.458 1.063L12 17.962l5.396 2.918a1 1 0 0 0 1.459-1.063l-1.117-6.008 4.45-4.207a1 1 0 0 0-.558-1.718l-6.085-.8-2.643-5.516Z',
})
@@ -14,7 +14,6 @@ import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {KeyboardPadding} from '#/components/KeyboardPadding'
import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography'
import {Divider} from '../Divider'
@@ -110,7 +109,6 @@ function LabelsOnMeDialogInner(props: LabelsOnMeDialogProps) {
</>
)}
<Dialog.Close />
<KeyboardPadding />
</Dialog.ScrollableInner>
)
}
@@ -43,7 +43,13 @@ export function ProfileHeaderAlerts({
)
}
function ProfileLabel({cause}: {cause: ModerationCause}) {
export function ProfileLabel({
cause,
disableDetailsDialog,
}: {
cause: ModerationCause
disableDetailsDialog?: boolean
}) {
const t = useTheme()
const control = useModerationDetailsDialogControl()
const desc = useModerationCauseDescription(cause)
@@ -51,6 +57,7 @@ function ProfileLabel({cause}: {cause: ModerationCause}) {
return (
<>
<Button
disabled={disableDetailsDialog}
label={desc.name}
onPress={() => {
control.open()
@@ -87,7 +94,9 @@ function ProfileLabel({cause}: {cause: ModerationCause}) {
)}
</Button>
<ModerationDetailsDialog control={control} modcause={cause} />
{!disableDetailsDialog && (
<ModerationDetailsDialog control={control} modcause={cause} />
)}
</>
)
}
+102
View File
@@ -0,0 +1,102 @@
import {describe, expect, it} from '@jest/globals'
import {MessageDescriptor} from '@lingui/core'
import {addDays, subDays, subHours, subMinutes, subSeconds} from 'date-fns'
import {dateDiff} from '../useTimeAgo'
const lingui: any = (obj: MessageDescriptor) => obj.message
const base = new Date('2024-06-17T00:00:00Z')
describe('dateDiff', () => {
it(`works with numbers`, () => {
expect(dateDiff(subDays(base, 3), Number(base), {lingui})).toEqual('3d')
})
it(`works with strings`, () => {
expect(dateDiff(subDays(base, 3), base.toString(), {lingui})).toEqual('3d')
})
it(`works with dates`, () => {
expect(dateDiff(subDays(base, 3), base, {lingui})).toEqual('3d')
})
it(`equal values return now`, () => {
expect(dateDiff(base, base, {lingui})).toEqual('now')
})
it(`future dates return now`, () => {
expect(dateDiff(addDays(base, 3), base, {lingui})).toEqual('now')
})
it(`values < 5 seconds ago return now`, () => {
const then = subSeconds(base, 4)
expect(dateDiff(then, base, {lingui})).toEqual('now')
})
it(`values >= 5 seconds ago return seconds`, () => {
const then = subSeconds(base, 5)
expect(dateDiff(then, base, {lingui})).toEqual('5s')
})
it(`values < 1 min return seconds`, () => {
const then = subSeconds(base, 59)
expect(dateDiff(then, base, {lingui})).toEqual('59s')
})
it(`values >= 1 min return minutes`, () => {
const then = subSeconds(base, 60)
expect(dateDiff(then, base, {lingui})).toEqual('1m')
})
it(`minutes round down`, () => {
const then = subSeconds(base, 119)
expect(dateDiff(then, base, {lingui})).toEqual('1m')
})
it(`values < 1 hour return minutes`, () => {
const then = subMinutes(base, 59)
expect(dateDiff(then, base, {lingui})).toEqual('59m')
})
it(`values >= 1 hour return hours`, () => {
const then = subMinutes(base, 60)
expect(dateDiff(then, base, {lingui})).toEqual('1h')
})
it(`hours round down`, () => {
const then = subMinutes(base, 119)
expect(dateDiff(then, base, {lingui})).toEqual('1h')
})
it(`values < 1 day return hours`, () => {
const then = subHours(base, 23)
expect(dateDiff(then, base, {lingui})).toEqual('23h')
})
it(`values >= 1 day return days`, () => {
const then = subHours(base, 24)
expect(dateDiff(then, base, {lingui})).toEqual('1d')
})
it(`days round down`, () => {
const then = subHours(base, 47)
expect(dateDiff(then, base, {lingui})).toEqual('1d')
})
it(`values < 30 days return days`, () => {
const then = subDays(base, 29)
expect(dateDiff(then, base, {lingui})).toEqual('29d')
})
it(`values >= 30 days return months`, () => {
const then = subDays(base, 30)
expect(dateDiff(then, base, {lingui})).toEqual('1mo')
})
it(`months round down`, () => {
const then = subDays(base, 59)
expect(dateDiff(then, base, {lingui})).toEqual('1mo')
})
it(`values are rounded by increments of 30`, () => {
const then = subDays(base, 61)
expect(dateDiff(then, base, {lingui})).toEqual('2mo')
})
it(`values < 360 days return months`, () => {
const then = subDays(base, 359)
expect(dateDiff(then, base, {lingui})).toEqual('11mo')
})
it(`values >= 360 days return the earlier value`, () => {
const then = subDays(base, 360)
expect(dateDiff(then, base, {lingui})).toEqual(then.toLocaleDateString())
})
})
@@ -1,23 +1,15 @@
import {interpolate, useAnimatedStyle} from 'react-native-reanimated'
import {useMinimalShellMode as useMinimalShellModeState} from '#/state/shell/minimal-mode'
import {useMinimalShellMode} from '#/state/shell/minimal-mode'
import {useShellLayout} from '#/state/shell/shell-layout'
export function useMinimalShellMode() {
const mode = useMinimalShellModeState()
const {footerHeight, headerHeight} = useShellLayout()
// Keep these separated so that we only pay for useAnimatedStyle that gets used.
const footerMinimalShellTransform = useAnimatedStyle(() => {
return {
pointerEvents: mode.value === 0 ? 'auto' : 'none',
opacity: Math.pow(1 - mode.value, 2),
transform: [
{
translateY: interpolate(mode.value, [0, 1], [0, footerHeight.value]),
},
],
}
})
const headerMinimalShellTransform = useAnimatedStyle(() => {
export function useMinimalShellHeaderTransform() {
const mode = useMinimalShellMode()
const {headerHeight} = useShellLayout()
const headerTransform = useAnimatedStyle(() => {
return {
pointerEvents: mode.value === 0 ? 'auto' : 'none',
opacity: Math.pow(1 - mode.value, 2),
@@ -28,7 +20,32 @@ export function useMinimalShellMode() {
],
}
})
const fabMinimalShellTransform = useAnimatedStyle(() => {
return headerTransform
}
export function useMinimalShellFooterTransform() {
const mode = useMinimalShellMode()
const {footerHeight} = useShellLayout()
const footerTransform = useAnimatedStyle(() => {
return {
pointerEvents: mode.value === 0 ? 'auto' : 'none',
opacity: Math.pow(1 - mode.value, 2),
transform: [
{
translateY: interpolate(mode.value, [0, 1], [0, footerHeight.value]),
},
],
}
})
return footerTransform
}
export function useMinimalShellFabTransform() {
const mode = useMinimalShellMode()
const fabTransform = useAnimatedStyle(() => {
return {
transform: [
{
@@ -37,9 +54,5 @@ export function useMinimalShellMode() {
],
}
})
return {
footerMinimalShellTransform,
headerMinimalShellTransform,
fabMinimalShellTransform,
}
return fabTransform
}
+1
View File
@@ -7,6 +7,7 @@ export function useNavigationTabState() {
const res = {
isAtHome: getTabState(state, 'Home') !== TabState.Outside,
isAtSearch: getTabState(state, 'Search') !== TabState.Outside,
// FeedsTab no longer exists, but this check works for `Feeds` screen as well
isAtFeeds: getTabState(state, 'Feeds') !== TabState.Outside,
isAtNotifications:
getTabState(state, 'Notifications') !== TabState.Outside,
+95
View File
@@ -0,0 +1,95 @@
import {useCallback, useMemo} from 'react'
import {msg, plural} from '@lingui/macro'
import {I18nContext, useLingui} from '@lingui/react'
import {differenceInSeconds} from 'date-fns'
export type TimeAgoOptions = {
lingui: I18nContext['_']
format?: 'long' | 'short'
}
export function useGetTimeAgo() {
const {_} = useLingui()
return useCallback(
(
date: number | string | Date,
options?: Omit<TimeAgoOptions, 'lingui'>,
) => {
return dateDiff(date, Date.now(), {lingui: _, format: options?.format})
},
[_],
)
}
export function useTimeAgo(
date: number | string | Date,
options?: Omit<TimeAgoOptions, 'lingui'>,
): string {
const timeAgo = useGetTimeAgo()
return useMemo(() => {
return timeAgo(date, {...options})
}, [date, options, timeAgo])
}
const NOW = 5
const MINUTE = 60
const HOUR = MINUTE * 60
const DAY = HOUR * 24
const MONTH_30 = DAY * 30
/**
* Returns the difference between `earlier` and `later` dates, formatted as a
* natural language string.
*
* - All month are considered exactly 30 days.
* - Dates assume `earlier` <= `later`, and will otherwise return 'now'.
* - Differences >= 360 days are returned as the "M/D/YYYY" string
* - All values round down
*/
export function dateDiff(
earlier: number | string | Date,
later: number | string | Date,
options: TimeAgoOptions,
): string {
const _ = options.lingui
const format = options?.format || 'short'
const long = format === 'long'
const diffSeconds = differenceInSeconds(new Date(later), new Date(earlier))
if (diffSeconds < NOW) {
return _(msg`now`)
} else if (diffSeconds < MINUTE) {
return `${diffSeconds}${
long ? ` ${plural(diffSeconds, {one: 'second', other: 'seconds'})}` : 's'
}`
} else if (diffSeconds < HOUR) {
const diff = Math.floor(diffSeconds / MINUTE)
return `${diff}${
long ? ` ${plural(diff, {one: 'minute', other: 'minutes'})}` : 'm'
}`
} else if (diffSeconds < DAY) {
const diff = Math.floor(diffSeconds / HOUR)
return `${diff}${
long ? ` ${plural(diff, {one: 'hour', other: 'hours'})}` : 'h'
}`
} else if (diffSeconds < MONTH_30) {
const diff = Math.floor(diffSeconds / DAY)
return `${diff}${
long ? ` ${plural(diff, {one: 'day', other: 'days'})}` : 'd'
}`
} else {
const diff = Math.floor(diffSeconds / MONTH_30)
if (diff < 12) {
return `${diff}${
long ? ` ${plural(diff, {one: 'month', other: 'months'})}` : 'mo'
}`
} else {
const str = new Date(earlier).toLocaleDateString()
if (long) {
return _(msg`on ${str}`)
}
return str
}
}
}
+3 -1
View File
@@ -3,8 +3,9 @@ import {
launchImageLibraryAsync,
MediaTypeOptions,
} from 'expo-image-picker'
import {getDataUriSize} from './util'
import * as Toast from 'view/com/util/Toast'
import {getDataUriSize} from './util'
export async function openPicker(opts?: ImagePickerOptions) {
const response = await launchImageLibraryAsync({
@@ -12,6 +13,7 @@ export async function openPicker(opts?: ImagePickerOptions) {
mediaTypes: MediaTypeOptions.Images,
quality: 1,
...opts,
legacy: true,
})
if (response.assets && response.assets.length > 4) {
+54 -17
View File
@@ -6,7 +6,8 @@ import {BskyAgent} from '@atproto/api'
import {logger} from '#/logger'
import {SessionAccount, useAgent, useSession} from '#/state/session'
import {logEvent, useGate} from 'lib/statsig/statsig'
import {devicePlatform, isNative} from 'platform/detection'
import {devicePlatform, isAndroid, isNative} from 'platform/detection'
import BackgroundNotificationHandler from '../../../modules/expo-background-notification-handler'
const SERVICE_DID = (serviceUrl?: string) =>
serviceUrl?.includes('staging')
@@ -42,7 +43,7 @@ async function getPushToken(skipPermissionCheck = false) {
const granted =
skipPermissionCheck || (await Notifications.getPermissionsAsync()).granted
if (granted) {
Notifications.getDevicePushTokenAsync()
return Notifications.getDevicePushTokenAsync()
}
}
@@ -55,7 +56,22 @@ export function useNotificationsRegistration() {
return
}
getPushToken()
// HACK - see https://github.com/bluesky-social/social-app/pull/4467
// An apparent regression in expo-notifications causes `addPushTokenListener` to not fire on Android whenever the
// token changes by calling `getPushToken()`. This is a workaround to ensure we register the token once it is
// generated on Android.
if (isAndroid) {
;(async () => {
const token = await getPushToken()
// Token will be undefined if we don't have notifications permission
if (token) {
registerPushToken(agent, currentAccount, token)
}
})()
} else {
getPushToken()
}
// According to the Expo docs, there is a chance that the token will change while the app is open in some rare
// cases. This will fire `registerPushToken` whenever that happens.
@@ -71,14 +87,18 @@ export function useNotificationsRegistration() {
export function useRequestNotificationsPermission() {
const gate = useGate()
const {currentAccount} = useSession()
const agent = useAgent()
return async (context: 'StartOnboarding' | 'AfterOnboarding' | 'Login') => {
return async (
context: 'StartOnboarding' | 'AfterOnboarding' | 'Login' | 'Home',
) => {
const permissions = await Notifications.getPermissionsAsync()
if (
!isNative ||
permissions?.status === 'granted' ||
permissions?.status === 'denied'
(permissions?.status === 'denied' && !permissions.canAskAgain)
) {
return
}
@@ -94,6 +114,9 @@ export function useRequestNotificationsPermission() {
) {
return
}
if (context === 'Home' && !currentAccount) {
return
}
const res = await Notifications.requestPermissionsAsync()
logEvent('notifications:request', {
@@ -103,24 +126,38 @@ export function useRequestNotificationsPermission() {
if (res.granted) {
// This will fire a pushTokenEvent, which will handle registration of the token
getPushToken(true)
const token = await getPushToken(true)
// Same hack as above. We cannot rely on the `addPushTokenListener` to fire on Android due to an Expo bug, so we
// will manually register it here. Note that this will occur only:
// 1. right after the user signs in, leading to no `currentAccount` account being available - this will be instead
// picked up from the useEffect above on `currentAccount` change
// 2. right after onboarding. In this case, we _need_ this registration, since `currentAccount` will not change
// and we need to ensure the token is registered right after permission is granted. `currentAccount` will already
// be available in this case, so the registration will succeed.
// We should remove this once expo-notifications (and possibly FCMv1) is fixed and the `addPushTokenListener` is
// working again. See https://github.com/expo/expo/issues/28656
if (isAndroid && currentAccount && token) {
registerPushToken(agent, currentAccount, token)
}
}
}
}
export async function decrementBadgeCount(by: number | 'reset' = 1) {
export async function decrementBadgeCount(by: number) {
if (!isNative) return
const currCount = await getBadgeCountAsync()
if (by === 'reset') {
await setBadgeCountAsync(0)
return
let count = await getBadgeCountAsync()
count -= by
if (count < 0) {
count = 0
}
let newCount = currCount - by
if (newCount < 0) {
newCount = 0
}
await setBadgeCountAsync(newCount)
await BackgroundNotificationHandler.setBadgeCountAsync(count)
await setBadgeCountAsync(count)
}
export async function resetBadgeCount() {
await BackgroundNotificationHandler.setBadgeCountAsync(0)
await setBadgeCountAsync(0)
}
+1 -1
View File
@@ -35,7 +35,7 @@ export function isStateAtTabRoot(state: State | undefined) {
return (
isTab(currentRoute.name, 'Home') ||
isTab(currentRoute.name, 'Search') ||
isTab(currentRoute.name, 'Feeds') ||
isTab(currentRoute.name, 'Messages') ||
isTab(currentRoute.name, 'Notifications') ||
isTab(currentRoute.name, 'MyProfile')
)
+2 -6
View File
@@ -15,6 +15,7 @@ export type CommonNavigatorParams = {
Profile: {name: string; hideBackButton?: boolean}
ProfileFollowers: {name: string}
ProfileFollows: {name: string}
ProfileKnownFollowers: {name: string}
ProfileList: {name: string; rkey: string}
PostThread: {name: string; rkey: string}
PostLikedBy: {name: string; rkey: string}
@@ -40,12 +41,12 @@ export type CommonNavigatorParams = {
Hashtag: {tag: string; author?: string}
MessagesConversation: {conversation: string; embed?: string}
MessagesSettings: undefined
Feeds: undefined
}
export type BottomTabNavigatorParams = CommonNavigatorParams & {
HomeTab: undefined
SearchTab: undefined
FeedsTab: undefined
NotificationsTab: undefined
MyProfileTab: undefined
MessagesTab: undefined
@@ -59,10 +60,6 @@ export type SearchTabNavigatorParams = CommonNavigatorParams & {
Search: {q?: string}
}
export type FeedsTabNavigatorParams = CommonNavigatorParams & {
Feeds: undefined
}
export type NotificationsTabNavigatorParams = CommonNavigatorParams & {
Notifications: undefined
}
@@ -89,7 +86,6 @@ export type AllNavigatorParams = CommonNavigatorParams & {
Home: undefined
SearchTab: undefined
Search: {q?: string}
FeedsTab: undefined
Feeds: undefined
NotificationsTab: undefined
Notifications: undefined
+1 -1
View File
@@ -17,7 +17,7 @@ export type LogEvents = {
}
'notifications:openApp': {}
'notifications:request': {
context: 'StartOnboarding' | 'AfterOnboarding' | 'Login'
context: 'StartOnboarding' | 'AfterOnboarding' | 'Login' | 'Home'
status: 'granted' | 'denied' | 'undetermined'
}
'state:background:sampled': {
+1
View File
@@ -1,5 +1,6 @@
export type Gate =
// Keep this alphabetic please.
| 'native_pwi_disabled'
| 'request_notifications_permission_after_onboarding_v2'
| 'show_avi_follow_button'
| 'show_follow_back_label_v2'
+29 -2
View File
@@ -14,6 +14,8 @@ import {useNonReactiveCallback} from '../hooks/useNonReactiveCallback'
import {LogEvents} from './events'
import {Gate} from './gates'
const SDK_KEY = 'client-SXJakO39w9vIhl3D44u8UupyzFl4oZ2qPIkjwcvuPsV'
type StatsigUser = {
userID: string | undefined
// TODO: Remove when enough users have custom.platform:
@@ -26,6 +28,8 @@ type StatsigUser = {
bundleDate: number
refSrc: string
refUrl: string
referrer: string
referrerHostname: string
appLanguage: string
contentLanguages: string[]
}
@@ -33,12 +37,29 @@ type StatsigUser = {
let refSrc = ''
let refUrl = ''
let referrer = ''
let referrerHostname = ''
if (isWeb && typeof window !== 'undefined') {
const params = new URLSearchParams(window.location.search)
refSrc = params.get('ref_src') ?? ''
refUrl = decodeURIComponent(params.get('ref_url') ?? '')
}
if (
isWeb &&
typeof document !== 'undefined' &&
document != null &&
document.referrer
) {
try {
const url = new URL(document.referrer)
if (url.hostname !== 'bsky.app') {
referrer = document.referrer
referrerHostname = url.hostname
}
} catch {}
}
export type {LogEvents}
function createStatsigOptions(prefetchUsers: StatsigUser[]) {
@@ -198,6 +219,8 @@ function toStatsigUser(did: string | undefined): StatsigUser {
custom: {
refSrc,
refUrl,
referrer,
referrerHostname,
platform: Platform.OS as 'ios' | 'android' | 'web',
bundleIdentifier: BUNDLE_IDENTIFIER,
bundleDate: BUNDLE_DATE,
@@ -230,7 +253,7 @@ AppState.addEventListener('change', (state: AppStateStatus) => {
})
export async function tryFetchGates(
did: string,
did: string | undefined,
strategy: 'prefer-low-latency' | 'prefer-fresh-gates',
) {
try {
@@ -254,6 +277,10 @@ export async function tryFetchGates(
}
}
export function initialize() {
return Statsig.initialize(SDK_KEY, null, createStatsigOptions([]))
}
export function Provider({children}: {children: React.ReactNode}) {
const {currentAccount, accounts} = useSession()
const did = currentAccount?.did
@@ -299,7 +326,7 @@ export function Provider({children}: {children: React.ReactNode}) {
<GateCache.Provider value={gateCache}>
<StatsigProvider
key={did}
sdkKey="client-SXJakO39w9vIhl3D44u8UupyzFl4oZ2qPIkjwcvuPsV"
sdkKey={SDK_KEY}
mountKey={currentStatsigUser.userID}
user={currentStatsigUser}
// This isn't really blocking due to short initTimeoutMs above.
-42
View File
@@ -1,45 +1,3 @@
const NOW = 5
const MINUTE = 60
const HOUR = MINUTE * 60
const DAY = HOUR * 24
const MONTH_30 = DAY * 30
const MONTH = DAY * 30.41675 // This results in 365.001 days in a year, which is close enough for nearly all cases
export function ago(date: number | string | Date): string {
let ts: number
if (typeof date === 'string') {
ts = Number(new Date(date))
} else if (date instanceof Date) {
ts = Number(date)
} else {
ts = date
}
const diffSeconds = Math.floor((Date.now() - ts) / 1e3)
if (diffSeconds < NOW) {
return `now`
} else if (diffSeconds < MINUTE) {
return `${diffSeconds}s`
} else if (diffSeconds < HOUR) {
return `${Math.floor(diffSeconds / MINUTE)}m`
} else if (diffSeconds < DAY) {
return `${Math.floor(diffSeconds / HOUR)}h`
} else if (diffSeconds < MONTH_30) {
return `${Math.round(diffSeconds / DAY)}d`
} else {
let months = diffSeconds / MONTH
if (months % 1 >= 0.9) {
months = Math.ceil(months)
} else {
months = Math.floor(months)
}
if (months < 12) {
return `${months}mo`
} else {
return new Date(ts).toLocaleDateString()
}
}
}
export function niceDate(date: number | string | Date) {
const d = new Date(date)
return `${d.toLocaleDateString('en-us', {
+18 -1
View File
@@ -116,7 +116,7 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
const langs = appLanguage.split(',').filter(Boolean)
for (const lang of langs) {
switch (lang) {
switch (fixLegacyLanguageCode(lang)) {
case 'en':
return AppLanguage.en
case 'ca':
@@ -157,3 +157,20 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
}
return AppLanguage.en
}
export function fixLegacyLanguageCode(code: string | null): string | null {
// handle some legacy code conversions, see https://xml.coverpages.org/iso639a.html
if (code === 'in') {
// indonesian
return 'id'
}
if (code === 'iw') {
// hebrew
return 'he'
}
if (code === 'ji') {
// yiddish
return 'yi'
}
return code
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -2,6 +2,7 @@ import {Platform} from 'react-native'
import {isReducedMotion} from 'react-native-reanimated'
import {getLocales} from 'expo-localization'
import {fixLegacyLanguageCode} from '#/locale/helpers'
import {dedupArray} from 'lib/functions'
export const isIOS = Platform.OS === 'ios'
@@ -17,7 +18,7 @@ export const isMobileWeb =
export const deviceLocales = dedupArray(
getLocales?.()
.map?.(locale => locale.languageCode)
.map?.(locale => fixLegacyLanguageCode(locale.languageCode))
.filter(code => typeof code === 'string'),
) as string[]

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