Compare commits

..

93 Commits

Author SHA1 Message Date
Hailey 1db9db52b3 one more tweak 2024-10-10 11:30:57 -07:00
Hailey ff6c9351d7 add timeout 2024-10-10 11:26:41 -07:00
Eric Bailey 3b44ea203c Invert non config filtering (#5674)
* Invert filtering logic

* Rename

* Rm comment
2024-10-10 10:04:23 -07:00
Eric Bailey 96cc31babc Include non-configurable in defs query (#5673) 2024-10-10 09:59:29 -07:00
Eric Bailey 509e3d9c85 Filter out non-configrable mod authorities from report dialog (#5670) 2024-10-10 09:47:12 -07:00
Hailey 676e6269e7 Show download button for Apple (#5671) 2024-10-10 09:15:24 -07:00
Hailey 2841944789 Ensure app clip works even with starter-pack (#5664) 2024-10-09 15:29:25 -07:00
Hailey 9f070cd9cd Cleanup unused props from dialogs (#5665) 2024-10-09 14:50:54 -07:00
Hailey 86b0d3b498 Add missing web component (for native view manager) (#5662) 2024-10-09 12:09:16 -07:00
Samuel Newman cca344a3d1 Allow nested sheets without boilerplate (#5660)
Co-authored-by: Hailey <me@haileyok.com>
2024-10-09 11:30:42 -07:00
Paul Frazee b3ade19bbe Update joined_this_week 2024-10-09 11:08:12 -07:00
Paul Frazee da28c5690a Release 1.92 (#5658)
* Fix tests

* intl extract
2024-10-08 19:46:24 -07:00
Hailey f52be63358 Fix layout bugs in appeals dialog (#5657) 2024-10-08 17:59:30 -07:00
Eric Bailey 857b20b2a8 Make 'em round (#5654) 2024-10-08 14:47:09 -07:00
Eric Bailey 8fe242728e Downgrade Radix packages for type compat (#5653) 2024-10-08 13:04:42 -07:00
Eric Bailey 2bb1f4abb8 Merge fontVariant defs (#5652) 2024-10-08 12:02:53 -07:00
Hailey 3da4f9b170 fix clear button in input (#5650)
Co-authored-by: Eric Bailey <git@esb.lol>
2024-10-08 11:45:04 -07:00
Hailey 2ab95124dd Bump 1.92.0 (#5641) 2024-10-08 11:38:05 -07:00
Eric Bailey 1db39ed1d1 Filter posts containing mute words from search and notifications (#5599)
* Filter mute words from search

* Filter mute words from notifications

* Do no filter search if using from filter
2024-10-08 13:22:37 -05:00
Samuel Newman fc82d2f6d5 Move Dialogs to Radix (#5648)
* Use Redix FocusTrap (#5638)

* Use Redix FocusTrap

* force resolutions on radix libs

* add focus guards

* use @radix-ui/dismissable-layer for escape handling

* fix banner menu keypress by using `Pressable`

* add menu in dialog example to storybook

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>

* use DismissableLayer/FocusScope for composer

* fix storybook dialog

* thread Portal through Prompt and avatar/banner

* fix dialog style regression

* remove tamagui

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2024-10-08 20:00:49 +03:00
dan efcf8a6ae5 Nicer error message for disabled quotes (#5644) 2024-10-08 14:46:15 +09:00
Eric Bailey db76452822 Update neue nux date (#5643) 2024-10-07 19:43:55 -05:00
Hailey ab9c4acc5b Make alt text scrollable on native (#5642) 2024-10-07 17:07:50 -07:00
dan e1ca3ae40e Move remaining composer state into reducer (#5623)
Co-authored-by: Mary <git@mary.my.id>
Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
Co-authored-by: Hailey <me@haileyok.com>
2024-10-07 17:07:40 -07:00
dan c06040cc20 Fetch link previews from RQ (#5608)
Co-authored-by: Mary <git@mary.my.id>
Co-authored-by: Hailey <me@haileyok.com>
2024-10-07 17:02:58 -07:00
dan dd8be2e939 Use composer state as source of truth for embeds/links on publish (#5606)
Co-authored-by: Mary <git@mary.my.id>
Co-authored-by: Hailey <me@haileyok.com>
2024-10-07 16:58:42 -07:00
Fathia e564fe9cc6 Update Indonesian translation (#5316)
Co-authored-by: Indonesian <github-actions@github.com>
2024-10-07 16:15:46 -07:00
Hailey 80e25b3d9f [Video] Add dimension info to share intent pt.2 (#5640) 2024-10-07 15:32:26 -07:00
Hailey 509b37fafa [Video] Prevent screen from dimming while in full screen (#5637) 2024-10-07 14:12:17 -07:00
Hailey ee25b89801 [Video] Add dimension info to share intent (#5639) 2024-10-07 14:09:47 -07:00
Samuel Newman 94e7bfbe40 [Video] Revert safari hackfix (#5367) 2024-10-07 12:55:03 -07:00
Hailey 58b1d9326d Swipeable to delete chat, custom swipeable (#5614)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2024-10-07 11:15:17 -07:00
Samuel Newman 8d80f1344d equal spacing on displayname/handle (#5636) 2024-10-07 21:12:05 +03:00
Takayuki KUSANO c365096200 Update Japanese translation (#5374)
* Update translation

* Fixed

* Update translation

* Update translation

* Update translation

* Update translation

* Suggested fix by Hima-Zinn

* Update translation

* Update translation

* Update translation

* Translate "pinned"

* Update translation

* Refactor video uploads (#5570)

* Remove unused video field

* Stop exposing video dispatch

* Move cancellation out of the reducer

* Make useUploadStatusQuery controlled by jobId

* Rename SetStatus to SetProcessing

This action only has one callsite and it's always passing "processing".

* Move jobId into video reducer state

* Make cancellation scoped

* Inline useCompressVideoMutation

* Move processVideo down the file

* Extract getErrorMessage

* useServiceAuthToken -> getServiceAuthToken

* useVideoAgent -> createVideoAgent

* useVideoUploadLimits -> getVideoUploadLimits

* useUploadVideoMutation -> uploadVideo

* Use async/await in processVideo

* Inline onVideoCompressed into processVideo

* Use async/await for uploadVideo

* Factor out error messages

* Guard dispatch with signal

This lets us remove the scattered signal checks around dispatch.

* Move job polling out of RQ

* Handle poll failures

* Remove unnecessary guards

* Slightly more accurate condition

* Move initVideoUri handling out of the hook

* Remove dead argument

It wasn't being used before either.

* Remove unused detailed status

This isn't being used because we're only respecting that state variable when isProcessing=true, but isProcessing is always false during video upload.

If we want to re-add this later, it should really just be derived from the reducer state.

* Harden the video reducer

* Tie all spawned work to a signal

* Preserve asset/media for nicer error state

* Rename actions to match states

* Inline useUploadVideo

This abstraction is getting in the way of some future work.

* Move MIME check to the only place that handles it

* Manage video reducer from composer reducer (#5573)

* Move video state into composer state

* Represent video as embed

This is slightly broken. In particular, we can't remove video yet because there's no action that results in video embed being removed.

* Properly represent video as embed

This aligns the video state lifetime with the embed lifetime. Video can now be properly added and removed.

* Disable Add Video when we have images

* Ignore empty image pick

* Rename some files and variables (#5587)

* Move composer reducers together

* videoUploadState -> videoState

* Inline videoDispatch

* 🪵📌 (#5594)

* [🐴] Reduce amount that message sent date is shown (#4228)

* Fix profile header buttons (#5558)

* Fix profile header buttons

* Adjust labeler buttons too

* Fix load state jumps

* Small tweak for web

* Remove log

* Update

* Revert "Update"

This reverts commit b073c04830.

* Revert "Fix profile header buttons (#5558)"

This reverts commit c00c5fa495.

* Revert "[🐴] Reduce amount that message sent date is shown (#4228)"

This reverts commit af8e3422a6.

* Revert "🪵📌 (#5594)"

This reverts commit 3ab5190aca.

* Revert "Rename some files and variables (#5587)"

This reverts commit 475708ea30.

* Revert "Manage video reducer from composer reducer (#5573)"

This reverts commit 59589e34a3.

* Revert "Refactor video uploads (#5570)"

This reverts commit f74f9bc1d9.

* Update translation

* Update translation

* Delete unused entries

* Restored the trailing dot by #5622

---------

Co-authored-by: dan <dan.abramov@gmail.com>
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
Co-authored-by: Eric Bailey <git@esb.lol>
2024-10-07 11:45:21 -05:00
Ivan Beà e523066588 Update catalana messages.po (#5380)
100% translated again, take a look please @jordimas @darccio @surfdude29 @rortan134
2024-10-07 11:42:48 -05:00
f e l p s bba4a40a47 Update and inconsistency fixes to pt-br translation (#5436)
* Update and inconsistency fixes to pt-br translation

* Update and inconsistency fixes to pt-br translation

Updated last translator

* More tr

* More translations updated
2024-10-07 11:35:26 -05:00
Eric Bailey f5094240e7 Tweak #5522 (#5635)
* Reduce post reason icon size & offset (#5522)

* Make x margin within pin icon the same

* Reduce post reason icon size and offset

* Use existing pin

---------

Co-authored-by: futur <me@futuristick.ca>
2024-10-07 11:28:54 -05:00
Minseo Lee c5fef1b9b5 Update Korean localization (#5401)
* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po
2024-10-07 11:27:11 -05:00
Frudrax Cheng 11bd2e8952 Update Chinese localization (#5433)
* CN: Update Translates

* CN: Remove superseded strings

* CN: Update Translates

* CN: Update Translates

* TW: Update and clean

* Both: Update Translates

* CN: Update Translates

* CN: Remove superseded strings

* 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>

* CN: Update Translates

* CN: Remove superseded strings

* CN: Remove extra spaces

* CN: Make some translations more localized

* CN: Make some translations more localized#2

* CN: Update Translates

* CN: Update Translates

* CN: Remove superseded strings

* CN: Update Translates

* TW: Run intl:extract

* TW: Update Translates

* TW: Remove superseded strings

* TW: Improve translation

* Both: Run intl:extract

* CN: Update Translates

* 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-CN/messages.po

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* Both: Run intl:extract

* CN: Update Translates

* TW: Update Translates

* Both: Fix typo

* Both: Run intl:extract

* CN: Update Translates

* TW: Update Translates

* Both: Run intl:extract

* Both: Update Translates

* Both: Update Translates

* Both: Run intl:extract

* Both: Update Translates

* Both: Correct some grammar of words

* 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-CN/messages.po

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

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

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

* Both: Update Translates

---------

Co-authored-by: Kuwa Lee <kuwalee1069@gmail.com>
Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>
2024-10-07 11:21:43 -05:00
Frudrax Cheng d28186ac62 Add Cantonese Localization (#5479)
* Add zh-HK localization

* Update dates.ts

* HK: Update Translates

* HK: Fix typo

* HK: Fix wrong language code

* HK: Update Translates

* HK: Update Translates

* rename 'Chinese (Hong Kong)' to 'Cantonese'

* HK: Update Translates

* Update languages.ts

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

* HK: Update Translates by reviewers

---------

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
2024-10-07 11:20:06 -05:00
surfdude29 ff70b8ff61 use tabular numbers in CharProgress (#5631) 2024-10-07 09:45:05 -05:00
surfdude29 f050c51f11 Remove line breaks in intro text in issue templates (#5632)
* remove line breaks in bug_report.yml

* remove line breaks in bug_report_fabric.yml

* remove line breaks in feature_request.yml
2024-10-07 09:39:19 -05:00
Takayuki KUSANO 140f720fac Restored the dot in the message displayed in the moderation details dialog (#5622) 2024-10-07 15:53:38 +03:00
Hailey 686dfa5f78 Fix bounce when expanding sheet on ios (#5627) 2024-10-06 12:03:06 -07:00
Hailey 0ebf636652 Fix padding in repost dialog (#5626) 2024-10-06 11:41:45 -07:00
Hailey b52f8ced56 Fix composer char progress layout (#5620)
* fix composer

* more fixes
2024-10-06 14:42:52 +09:00
Minseo Lee 25e3c93e1e Improve localization of {els} string on Create Account page (#3912)
Co-authored-by: Hailey <me@haileyok.com>
2024-10-05 16:22:00 -07:00
Takayuki KUSANO 1118fd6883 Enclose the entire message in Trans tags for translation (#5616) 2024-10-05 16:12:30 -07:00
Eric Bailey 76d63f9967 Add alt text limit to image dialog (#5611)
* Add alt text limit to image dialog

* GIF alt text too

* Fix

* tweaks, save alt on dialog dismiss

* simplify close behavior

* use state in gif alt

* state

---------

Co-authored-by: Hailey <me@haileyok.com>
2024-10-05 19:58:04 +09:00
Jaz 6dfd57e621 Merge pull request #5610 from bluesky-social/static_asset_cdn
Serve static assets from a CDN host if provided
2024-10-04 14:39:43 -07:00
Jaz Volpert 795e2c0629 New assets to static CDN 2024-10-04 14:31:37 -07:00
Jaz Volpert 5f3c4c60db Merge branch 'main' into static_asset_cdn 2024-10-04 14:28:52 -07:00
Eric Bailey 3fb14d184a Move font-face defs into HTML for CDN (#5612) 2024-10-04 16:28:05 -05:00
Jaz Volpert 0d99b8b055 Consistent spacing for variable handlebars 2024-10-04 13:49:59 -07:00
Jaz Volpert eb5a6c8911 Propagate env var and trim trailing slash 2024-10-04 13:43:15 -07:00
Jaz Volpert b0fe2c6c9a Serve static assets from a CDN host if provided 2024-10-04 13:40:45 -07:00
Eric Bailey 1cedd4a3c4 Post alignment (#5580)
Co-authored-by: Hailey <me@haileyok.com>
2024-10-04 13:33:17 -07:00
Eric Bailey d17da8474b Emoji handling nested (#5609) 2024-10-04 13:31:40 -07:00
Hailey 00486e9499 [Sheets] [Pt. 1] Root PR (#5557)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
Co-authored-by: Eric Bailey <git@esb.lol>
Co-authored-by: dan <dan.abramov@gmail.com>
Co-authored-by: Hailey <me@haileyok.com>
2024-10-04 13:24:12 -07:00
Hailey 9802ebe20d Properly patch React Native to fix iOS RefreshControl bug (#5605) 2024-10-04 13:17:57 -07:00
dan 09caf327d9 Make composer reducer source of truth for images/video when publishing (#5595)
* Move caption and altText state into video reducer

* Make composer state source of truth for images and video publish
2024-10-05 03:45:34 +09:00
dan 282db85c06 Track links and embeds in the composer reducer (#5593)
* Scaffold embed draft types

These don't map 1:1 to the record structure. Rather, we select data from the draft on posting.

* Prefill initial quote

* Implement the reducer
2024-10-05 03:39:33 +09:00
Eric Bailey 6382fe4535 Bump SDK (#5602) 2024-10-04 09:34:03 -05:00
Eric Bailey 54ca816536 Update built asset caching (#5601) 2024-10-04 09:33:32 -05:00
Hailey 48241cb2b6 Downgrade sentry to recommended 5.24.3 (#5604) 2024-10-04 01:28:26 -07:00
Eric Bailey 3f66531e83 Add option to search in any language (#5598) 2024-10-03 20:23:07 -05:00
Eric Bailey ce53b3a264 Font tweaks (#5597)
* Increase fontWeight on android

* Use atoms for a couple stray fontWeights

* Rm unused file
2024-10-03 20:22:54 -05:00
Eric Bailey eb3b01d0ad Fix profile header buttons (#5558)
* Fix profile header buttons

* Adjust labeler buttons too

* Fix load state jumps

* Small tweak for web

* Remove log
2024-10-03 10:45:01 -05:00
Samuel Newman 7e79c7f768 [🐴] Reduce amount that message sent date is shown (#4228) 2024-10-03 18:19:38 +03:00
Samuel Newman 523a9a48c2 🪵📌 (#5594) 2024-10-03 17:28:13 +03:00
dan 2aa365b5b6 Rename some files and variables (#5587)
* Move composer reducers together

* videoUploadState -> videoState

* Inline videoDispatch
2024-10-03 14:57:48 +09:00
dan 03704e2b48 Manage video reducer from composer reducer (#5573)
* Move video state into composer state

* Represent video as embed

This is slightly broken. In particular, we can't remove video yet because there's no action that results in video embed being removed.

* Properly represent video as embed

This aligns the video state lifetime with the embed lifetime. Video can now be properly added and removed.

* Disable Add Video when we have images

* Ignore empty image pick
2024-10-03 14:26:38 +09:00
dan d2392d2d64 Refactor video uploads (#5570)
* Remove unused video field

* Stop exposing video dispatch

* Move cancellation out of the reducer

* Make useUploadStatusQuery controlled by jobId

* Rename SetStatus to SetProcessing

This action only has one callsite and it's always passing "processing".

* Move jobId into video reducer state

* Make cancellation scoped

* Inline useCompressVideoMutation

* Move processVideo down the file

* Extract getErrorMessage

* useServiceAuthToken -> getServiceAuthToken

* useVideoAgent -> createVideoAgent

* useVideoUploadLimits -> getVideoUploadLimits

* useUploadVideoMutation -> uploadVideo

* Use async/await in processVideo

* Inline onVideoCompressed into processVideo

* Use async/await for uploadVideo

* Factor out error messages

* Guard dispatch with signal

This lets us remove the scattered signal checks around dispatch.

* Move job polling out of RQ

* Handle poll failures

* Remove unnecessary guards

* Slightly more accurate condition

* Move initVideoUri handling out of the hook

* Remove dead argument

It wasn't being used before either.

* Remove unused detailed status

This isn't being used because we're only respecting that state variable when isProcessing=true, but isProcessing is always false during video upload.

If we want to re-add this later, it should really just be derived from the reducer state.

* Harden the video reducer

* Tie all spawned work to a signal

* Preserve asset/media for nicer error state

* Rename actions to match states

* Inline useUploadVideo

This abstraction is getting in the way of some future work.

* Move MIME check to the only place that handles it
2024-10-03 11:41:23 +09:00
Samuel Newman c2dac855cc Delete messages NUX (#5574) 2024-10-02 22:23:11 +03:00
Samuel Newman 13c9c79aee move files around (#5576) 2024-10-02 22:21:59 +03:00
Samuel Newman 405966830c Make reply prompt more subtle on desktop (#5569)
* make reply prompt more subtle on desktop

* fix alignment

* Tweak transition timing

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2024-10-02 11:40:18 -05:00
Eric Bailey 3972d3d4c3 Ignore built files (#5572) 2024-10-02 11:18:15 -05:00
Hailey a444fc53a0 import order (#5571) 2024-10-02 11:09:54 -05:00
Eric Bailey 1a7885b921 Add suspense boundary in onboarding (#5556) 2024-10-02 10:55:06 -05:00
Eric Bailey 3569873123 Tweak verify email dialog (#5555)
* Add intent/verify-email to go server

* Tweak verify intent dialog
2024-10-02 10:54:51 -05:00
Eric Bailey f251a43dd6 Add intent/verify-email to go server (#5554) 2024-10-02 10:54:25 -05:00
Eric Bailey 5a98ea684d Include ghost hook to statically export fonts (#5553) 2024-10-01 15:57:49 -05:00
dan e33b2b1bee Handle /starter-pack-short/:code in Go (#5546) 2024-10-02 00:09:21 +09:00
dan d2fd5589dc Introduce a composer reducer and move image state there (#5547)
* Add composer reducer

* Support adding images

Co-authored-by: Mary <git@mary.my.id>

* Support updating and deleting images

Co-authored-by: Mary <git@mary.my.id>

* Derive images state from composer state

Co-authored-by: Mary <git@mary.my.id>

---------

Co-authored-by: Mary <git@mary.my.id>
2024-10-02 00:08:46 +09:00
dan a7ee561e40 Paralellize image uploads (#5535)
* Remove unsafe type coercions from posting embed

* Extract resolveEmbed into a separate function

* Refactor to if-else because these are mutually exclusive

* Refactor resolveEmbed to early returns

* Separate resolving embed and media

* Parallelize image upload

* Prioritize not dropping media
2024-10-02 00:03:27 +09:00
Eric Bailey b59d6dfbb5 Remove local file check (#5544) 2024-09-30 17:43:14 -05:00
Samuel Newman 2129f69fa0 ALF text input for generic search input (#5511)
* alf text input for generic search input

* clearer ref naming

* Adjust props and definition

* Migrate props

* Migrate props

* Migrate props

* Replace on search screen

* rm old props

---------

Co-authored-by: Eric Bailey <git@esb.lol>
2024-09-30 16:53:11 -05:00
Eric Bailey b4941d8556 Use Inter variable font (#5540) 2024-09-30 16:04:19 -05:00
Hailey adaa79a461 Fix name in feature_request.yml (#5542) 2024-09-30 14:04:10 -07:00
Eric Bailey 26c09ff107 Fix sticky offset, gear color (#5537) 2024-09-30 16:04:04 -05:00
Hailey 669cdf222c Move email test to root tests dir (#5527) 2024-09-30 13:56:49 -07:00
Hailey 66f3068c58 revamp issue templates, add new arch template (#5532) 2024-09-30 13:55:21 -07:00
Samuel Newman 9ca2928aac use PressableScale for animation (#5541) 2024-09-30 21:49:53 +03:00
237 changed files with 48690 additions and 35447 deletions
-35
View File
@@ -1,35 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
<!-- A clear and concise description of what the bug is. -->
**To Reproduce**
Steps to reproduce the behavior:
1.
**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
**Screenshots**
<!-- If applicable, add screenshots to help explain your problem. -->
**Details**
- Platform: <!-- desktop chrome windows, mobile safari, iOS, Android -->
- Platform version:
- App version:
**Additional context**
<!-- Add any other context about the problem here. -->
+64
View File
@@ -0,0 +1,64 @@
name: "Bug Report"
description: "Create a report for an issue you have experience in the app."
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to report an issue you've found in the app! Before you submit this issue, please make sure you've searched for similar issues that are already open and being tracked. If you find an open issue that seems relevant to yours, it is best to leave a response there with your information instead of opening a new issue, since it helps to consolidate the info in one place.
- type: textarea
attributes:
label: Steps to Reproduce
description: |
Please provide the steps that may reproduce the issue that you are experiencing. The more concise of a
reproduction that you provide, the easier it will be for us to identify the issue and fix it in a timely manner.
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: textarea
attributes:
label: Attachments
description: |
If possible, please provide any images or videos that may help us understand the issue you are experiencing.
validations:
required: false
- type: dropdown
attributes:
label: What platform(s) does this occur on?
multiple: true
options:
- iOS
- Android
- Web (Desktop)
- Web (Mobile)
validations:
required: true
- type: input
attributes:
label: Device Info
description: |
Please provide the device information that you are using when experiencing this issue. This can include the
device model, operating system version, and any other relevant information.
validations:
required: false
- type: input
attributes:
label: What version of the app are you using?
description: |
You can easily find this information by visting the Settings screen in the app, and tapping on the version info
at the bottom of the screen. All the relevant information will be copied to your clipboard, and you will be
able to share it here.
validations:
required: true
- type: textarea
attributes:
label: Additional Information
description: |
Please provide any additional information that you think may be helpful in identifying the issue you are
experiencing. This can include any error messages, logs, or other information that you think may be relevant.
validations:
required: false
@@ -0,0 +1,69 @@
name: "Bug Report - Fabric/New Architecture"
description: "Create a bug report for builds using the New Architecture. If you are not sure what that means, don't use this template, and instead use the default bug report template."
labels: ["bug", "fabric"]
body:
- type: checkboxes
attributes:
label: You're running a build that uses the React Native New Architecture, right?
description: This template is specifically for issues related to builds that use the React Native New Architecture. If you are not sure what that means, this template is probably not relevant to you, and you should use the default Bug Report template.
options:
- label: I am running a build that uses the New Architecture
required: true
- type: markdown
attributes:
value: |
Thanks for taking the time to report an issue you've found in the app! Before you submit this issue, please make sure you've searched for similar issues that are already open and being tracked. If you find an open issue that seems relevant to yours, it is best to leave a response there with your information instead of opening a new issue, since it helps to consolidate the info in one place.
- type: textarea
attributes:
label: Steps to Reproduce
description: |
Please provide the steps that may reproduce the issue that you are experiencing. The more concise of a
reproduction that you provide, the easier it will be for us to identify the issue and fix it in a timely manner.
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: textarea
attributes:
label: Attachments
description: |
If possible, please provide any images or videos that may help us understand the issue you are experiencing.
validations:
required: false
- type: dropdown
attributes:
label: What platform(s) does this occur on?
multiple: true
options:
- iOS
- Android
validations:
required: true
- type: input
attributes:
label: Device Info
description: |
Please provide the device information that you are using when experiencing this issue. This can include the
device model, operating system version, and any other relevant information.
validations:
required: false
- type: input
attributes:
label: What version of the app are you using?
description: |
You can easily find this information by visting the Settings screen in the app, and tapping on the version info
at the bottom of the screen. All the relevant information will be copied to your clipboard, and you will be
able to share it here.
validations:
required: true
- type: textarea
attributes:
label: Additional Information
description: |
Please provide any additional information that you think may be helpful in identifying the issue you are
experiencing. This can include any error messages, logs, or other information that you think may be relevant.
validations:
required: false
-24
View File
@@ -1,24 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: feature-request
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
**Describe the solution you'd like**
<!-- A clear and concise description of what you want to happen. -->
**Describe alternatives you've considered**
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
**Additional context**
<!-- Add any other context or screenshots about the feature request here. -->
@@ -0,0 +1,42 @@
name: "Feature Request"
description: "Suggest an idea for the Bluesky app."
labels: ["feature-request"]
body:
- type: markdown
attributes:
value: |
Thanks for submitting a feature request! Before you submit this request, please make sure you've searched for similar requests that are already open and being tracked. If you find an open request that seems relevant to yours, it is best to leave a response there with your information instead of opening a new request, since it helps to consolidate the info in one place.
- type: textarea
attributes:
label: Describe the Feature
description: |
Please provide a detailed description of the feature you would like to see implemented in the app. The more
information you provide, the easier it will be for us to understand the feature you are requesting and to
implement it in a timely manner.
validations:
required: true
- type: textarea
attributes:
label: Attachments
description: |
If helpful, provide some images or videos of similar features in other apps that you think would be helpful
for us to understand the feature you are requesting, or places within the app that you think the feature belongs
in or is missing from.
validations:
required: false
- type: textarea
attributes:
label: Describe Alternatives
description: |
If there are any alternative solutions or features that you think would be helpful to implement in place of the
feature you are requesting, please provide a description of those as well.
validations:
required: false
- type: textarea
attributes:
label: Additional Context
description: |
If there is any additional context or information that you think would be helpful for us to know about the
feature you are requesting, please provide that information here.
validations:
required: false
+3
View File
@@ -15,3 +15,6 @@ android
ios
src/locale/locales
lib/react-compiler-runtime
bskyweb/static
coverage
web-build
+1 -2
View File
@@ -163,8 +163,7 @@ appId: xyz.blueskyweb.app
- tapOn:
label: "Adds and removes users on curatelists from the profile"
id: "bottomBarSearchBtn"
- tapOn:
id: "searchTextInput"
- tapOn: "Search"
- inputText: "bob"
- tapOn:
id: "searchAutoCompleteResult-bob.test"
-4
View File
@@ -17,10 +17,6 @@ appId: xyz.blueskyweb.app
id: "viewHeaderDrawerBtn"
- tapOn:
id: "profileCardButton"
- swipe:
from:
id: "profilePager-selector"
direction: LEFT
- tapOn:
id: "profilePager-selector-4"
- tapOn: "alice-favs"
-4
View File
@@ -20,10 +20,6 @@ appId: xyz.blueskyweb.app
- tapOn:
label: "Feeds button disappears after pinning a feed"
id: "bottomBarProfileBtn"
- swipe:
from:
id: "profilePager-selector"
direction: LEFT
- tapOn:
id: "profilePager-selector-4"
- tapOn: "alice-favs"
-15
View File
@@ -17,21 +17,6 @@ appId: xyz.blueskyweb.app
- tapOn:
id: "bottomBarProfileBtn"
# Can see feeds
- swipe:
from:
id: "profilePager-selector"
direction: LEFT
- tapOn:
id: "profilePager-selector-4"
- assertVisible: "alice-favs"
- swipe:
from:
id: "profilePager-selector"
direction: RIGHT
- tapOn:
id: "profilePager-selector-0"
# Open and close edit profile modal
- tapOn:
id: "profileHeaderEditProfileButton"
+1 -2
View File
@@ -15,8 +15,7 @@ appId: xyz.blueskyweb.app
id: "bottomBarSearchBtn"
- tapOn:
id: "bottomBarSearchBtn"
- tapOn:
id: "searchTextInput"
- tapOn: "Search"
- inputText: "b"
- tapOn:
id: "searchAutoCompleteResult-bob.test"
+1 -2
View File
@@ -12,8 +12,7 @@ appId: xyz.blueskyweb.app
# Navigate to another user profile via autocomplete
- tapOn:
id: "bottomBarSearchBtn"
- tapOn:
id: "searchTextInput"
- tapOn: "Search"
- inputText: "b"
- tapOn:
id: "searchAutoCompleteResult-bob.test"
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#080B12" fill-rule="evenodd" d="M4 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v6.386c1.064-.002 2 .86 2 2.001V19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-6.613c0-1.142.936-2.003 2-2.001V4Zm2 6.946 6 2 6-2V4H6v6.946ZM9 8a1 1 0 0 1 1-1h4a1 1 0 1 1 0 2h-4a1 1 0 0 1-1-1Zm2.367 6.843L4 12.387V19h16v-6.613l-7.367 2.456a2 2 0 0 1-1.265 0Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 426 B

+7
View File
@@ -87,6 +87,13 @@ func run(args []string) {
Value: cli.NewStringSlice("https://bsky.app", "https://main.bsky.dev", "https://app.staging.bsky.dev"),
EnvVars: []string{"CORS_ALLOWED_ORIGINS"},
},
&cli.StringFlag{
Name: "static-cdn-host",
Usage: "scheme, hostname, and port of static content CDN, don't end with a slash",
Required: false,
Value: "",
EnvVars: []string{"STATIC_CDN_HOST"},
},
},
},
}
+34 -28
View File
@@ -41,12 +41,13 @@ type Server struct {
}
type Config struct {
debug bool
httpAddress string
appviewHost string
ogcardHost string
linkHost string
ipccHost string
debug bool
httpAddress string
appviewHost string
ogcardHost string
linkHost string
ipccHost string
staticCDNHost string
}
func serve(cctx *cli.Context) error {
@@ -58,6 +59,8 @@ func serve(cctx *cli.Context) error {
ipccHost := cctx.String("ipcc-host")
basicAuthPassword := cctx.String("basic-auth-password")
corsOrigins := cctx.StringSlice("cors-allowed-origins")
staticCDNHost := cctx.String("static-cdn-host")
staticCDNHost = strings.TrimSuffix(staticCDNHost, "/")
// Echo
e := echo.New()
@@ -94,12 +97,13 @@ func serve(cctx *cli.Context) error {
echo: e,
xrpcc: xrpcc,
cfg: &Config{
debug: debug,
httpAddress: httpAddress,
appviewHost: appviewHost,
ogcardHost: ogcardHost,
linkHost: linkHost,
ipccHost: ipccHost,
debug: debug,
httpAddress: httpAddress,
appviewHost: appviewHost,
ogcardHost: ogcardHost,
linkHost: linkHost,
ipccHost: ipccHost,
staticCDNHost: staticCDNHost,
},
}
@@ -204,14 +208,8 @@ func serve(cctx *cli.Context) error {
path := c.Request().URL.Path
maxAge := 1 * (60 * 60) // default is 1 hour
// Cache javascript and images files for 1 week, which works because
// they're always versioned (e.g. /static/js/main.64c14927.js)
if strings.HasPrefix(path, "/static/js/") || strings.HasPrefix(path, "/static/images/") || strings.HasPrefix(path, "/static/media/") {
maxAge = 7 * (60 * 60 * 24) // 1 week
}
// fonts can be cached for a year
if strings.HasSuffix(path, ".otf") {
// all assets in /static/js, /static/css, /static/media are content-hashed and can be cached for a long time
if strings.HasPrefix(path, "/static/js/") || strings.HasPrefix(path, "/static/css/") || strings.HasPrefix(path, "/static/media/") {
maxAge = 365 * (60 * 60 * 24) // 1 year
}
@@ -255,6 +253,7 @@ func serve(cctx *cli.Context) error {
e.GET("/support/community-guidelines", server.WebGeneric)
e.GET("/support/copyright", server.WebGeneric)
e.GET("/intent/compose", server.WebGeneric)
e.GET("/intent/verify-email", server.WebGeneric)
e.GET("/messages", server.WebGeneric)
e.GET("/messages/:conversation", server.WebGeneric)
@@ -279,6 +278,7 @@ func serve(cctx *cli.Context) error {
// starter packs
e.GET("/starter-pack/:handleOrDID/:rkey", server.WebStarterPack)
e.GET("/starter-pack-short/:code", server.WebGeneric)
e.GET("/start/:handleOrDID/:rkey", server.WebStarterPack)
// ipcc
@@ -337,15 +337,21 @@ func (srv *Server) Shutdown() error {
return srv.httpd.Shutdown(ctx)
}
// NewTemplateContext returns a new pongo2 context with some default values.
func (srv *Server) NewTemplateContext() pongo2.Context {
return pongo2.Context{
"staticCDNHost": srv.cfg.staticCDNHost,
}
}
func (srv *Server) errorHandler(err error, c echo.Context) {
code := http.StatusInternalServerError
if he, ok := err.(*echo.HTTPError); ok {
code = he.Code
}
c.Logger().Error(err)
data := pongo2.Context{
"statusCode": code,
}
data := srv.NewTemplateContext()
data["statusCode"] = code
c.Render(code, "error.html", data)
}
@@ -389,18 +395,18 @@ func (srv *Server) LinkProxyMiddleware(url *url.URL) echo.MiddlewareFunc {
// handler for endpoint that have no specific server-side handling
func (srv *Server) WebGeneric(c echo.Context) error {
data := pongo2.Context{}
data := srv.NewTemplateContext()
return c.Render(http.StatusOK, "base.html", data)
}
func (srv *Server) WebHome(c echo.Context) error {
data := pongo2.Context{}
data := srv.NewTemplateContext()
return c.Render(http.StatusOK, "home.html", data)
}
func (srv *Server) WebPost(c echo.Context) error {
ctx := c.Request().Context()
data := pongo2.Context{}
data := srv.NewTemplateContext()
// sanity check arguments. don't 4xx, just let app handle if not expected format
rkeyParam := c.Param("rkey")
@@ -475,7 +481,7 @@ func (srv *Server) WebPost(c echo.Context) error {
func (srv *Server) WebStarterPack(c echo.Context) error {
req := c.Request()
ctx := req.Context()
data := pongo2.Context{}
data := srv.NewTemplateContext()
data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path)
// sanity check arguments. don't 4xx, just let app handle if not expected format
rkeyParam := c.Param("rkey")
@@ -513,7 +519,7 @@ func (srv *Server) WebStarterPack(c echo.Context) error {
func (srv *Server) WebProfile(c echo.Context) error {
ctx := c.Request().Context()
data := pongo2.Context{}
data := srv.NewTemplateContext()
// sanity check arguments. don't 4xx, just let app handle if not expected format
handleOrDIDParam := c.Param("handleOrDID")
+20 -6
View File
@@ -13,8 +13,8 @@
<!-- Hello Humans! API docs at https://atproto.com -->
<link rel="preload" as="font" type="font/ttf" href="/static/media/InterVariable.c9f788f6e7ebaec75d7c.ttf">
<link rel="preload" as="font" type="font/ttf" href="/static/media/InterVariable-Italic.55d6a3f35e9b605ba6f4.ttf">
<link rel="preload" as="font" type="font/ttf" href="{{ staticCDNHost }}/static/media/InterVariable.c9f788f6e7ebaec75d7c.ttf">
<link rel="preload" as="font" type="font/ttf" href="{{ staticCDNHost }}/static/media/InterVariable-Italic.55d6a3f35e9b605ba6f4.ttf">
<style>
/**
@@ -24,6 +24,20 @@
*
* THIS NEEDS TO BE DUPLICATED IN `bskyweb/templates/base.html`
*/
@font-face {
font-family: 'InterVariable';
src: url("{{ staticCDNHost }}/static/media/InterVariable.c9f788f6e7ebaec75d7c.ttf") format('truetype');
font-weight: 300 1000;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'InterVariableItalic';
src: url("{{ staticCDNHost }}/static/media/InterVariable-Italic.55d6a3f35e9b605ba6f4.ttf") format('truetype');
font-weight: 300 1000;
font-style: italic;
font-display: swap;
}
html {
background-color: white;
scrollbar-gutter: stable both-edges;
@@ -64,10 +78,10 @@
</style>
{% include "scripts.html" %}
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
<link rel="mask-icon" href="/static/safari-pinned-tab.svg" color="#1185fe">
<link rel="apple-touch-icon" sizes="180x180" href="{{ staticCDNHost }}/static/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="{{ staticCDNHost }}/static/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{ staticCDNHost }}/static/favicon-16x16.png">
<link rel="mask-icon" href="{{ staticCDNHost }}/static/safari-pinned-tab.svg" color="#1185fe">
<meta name="theme-color">
<meta name="application-name" content="Bluesky">
<meta name="generator" content="bskyweb">
+5
View File
@@ -96,6 +96,11 @@ jest.mock('expo-modules-core', () => ({
getIsReducedMotionEnabled: () => false,
}
}
if (moduleName === 'BottomSheet') {
return {
dismissAll: () => {},
}
}
}),
requireNativeViewManager: jest.fn().mockImplementation(moduleName => {
return () => null
+1
View File
@@ -18,6 +18,7 @@ module.exports = {
'tr',
'uk',
'zh-CN',
'zh-HK',
'zh-TW',
],
catalogs: [
+41 -14
View File
@@ -34,6 +34,7 @@ class ViewController: UIViewController, WKScriptMessageHandler, WKNavigationDele
webView.navigationDelegate = self
self.view.addSubview(webView)
self.webView = webView
self.webView?.load(URLRequest(url: URL(string: "https://bsky.app/?splash=true&clip=true")!))
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
@@ -42,16 +43,14 @@ class ViewController: UIViewController, WKScriptMessageHandler, WKNavigationDele
let payload = try? JSONDecoder().decode(WebViewActionPayload.self, from: data) else {
return
}
switch payload.action {
case .present:
guard let url = self.starterPackUrl else {
return
}
self.presentAppStoreOverlay()
defaults?.setValue(url.absoluteString, forKey: "starterPackUri")
if let url = self.starterPackUrl {
defaults?.setValue(url.absoluteString, forKey: "starterPackUri")
}
case .store:
guard let keyToStoreAs = payload.keyToStoreAs, let jsonToStore = payload.jsonToStore else {
return
@@ -66,23 +65,51 @@ class ViewController: UIViewController, WKScriptMessageHandler, WKNavigationDele
guard let url = navigationAction.request.url else {
return .allow
}
// Store the previous one to compare later, but only set starterPackUrl when we find the right one
prevUrl = url
// pathComponents starts with "/" as the first component, then each path name. so...
// ["/", "start", "name", "rkey"]
if url.pathComponents.count == 4,
url.pathComponents[1] == "start" {
if isStarterPackUrl(url){
self.starterPackUrl = url
}
return .allow
}
func isStarterPackUrl(_ url: URL) -> Bool {
var host: String?
if #available(iOS 16.0, *) {
host = url.host()
} else {
host = url.host
}
switch host {
case "bsky.app":
if url.pathComponents.count == 4,
(url.pathComponents[1] == "start" || url.pathComponents[1] == "starter-pack") {
return true
}
return false
case "go.bsky.app":
if url.pathComponents.count == 2 {
return true
}
return false
default:
return false
}
}
func handleURL(url: URL) {
let urlString = "\(url.absoluteString)?clip=true"
if let url = URL(string: urlString) {
self.webView?.load(URLRequest(url: url))
if isStarterPackUrl(url) {
let urlString = "\(url.absoluteString)?clip=true"
if let url = URL(string: urlString) {
self.webView?.load(URLRequest(url: url))
}
} else {
self.webView?.load(URLRequest(url: URL(string: "https://bsky.app/?splash=true&clip=true")!))
}
}
@@ -1,4 +1,5 @@
import UIKit
import AVKit
let IMAGE_EXTENSIONS: [String] = ["png", "jpg", "jpeg", "gif", "heic"]
let MOVIE_EXTENSIONS: [String] = ["mov", "mp4", "m4v"]
@@ -119,16 +120,11 @@ class ShareViewController: UIViewController {
private func handleVideos(items: [NSItemProvider]) async {
let firstItem = items.first
if let dataUri = try? await firstItem?.loadItem(forTypeIdentifier: "public.movie") as? URL {
let ext = String(dataUri.lastPathComponent.split(separator: ".").last ?? "mp4")
if let tempUrl = getTempUrl(ext: ext) {
let data = try? Data(contentsOf: dataUri)
try? data?.write(to: tempUrl)
if let encoded = tempUrl.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
let url = URL(string: "\(self.appScheme)://intent/compose?videoUri=\(encoded)") {
_ = self.openURL(url)
}
if let dataUrl = try? await firstItem?.loadItem(forTypeIdentifier: "public.movie") as? URL {
let ext = String(dataUrl.lastPathComponent.split(separator: ".").last ?? "mp4")
if let videoUriInfo = saveVideoWithInfo(dataUrl),
let url = URL(string: "\(self.appScheme)://intent/compose?videoUri=\(videoUriInfo)") {
_ = self.openURL(url)
}
}
@@ -153,6 +149,24 @@ class ShareViewController: UIViewController {
return nil
}
private func saveVideoWithInfo(_ dataUrl: URL) -> String? {
let ext = String(dataUrl.lastPathComponent.split(separator: ".").last ?? "mp4")
guard let tempUrl = getTempUrl(ext: ext) else {
return nil
}
let data = try? Data(contentsOf: dataUrl)
try? data?.write(to: tempUrl)
guard let track = AVURLAsset(url: dataUrl).tracks(withMediaType: AVMediaType.video).first else {
_ = try? FileManager().removeItem(at: tempUrl)
return nil
}
let size = track.naturalSize.applying(track.preferredTransform)
return "\(tempUrl.absoluteString)|\(size.width)|\(size.height)"
}
private func completeRequest() {
self.extensionContext?.completeRequest(returningItems: nil)
}
+49
View File
@@ -0,0 +1,49 @@
apply plugin: 'com.android.library'
group = 'expo.modules.bottomsheet'
version = '0.1.0'
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
apply from: expoModulesCorePlugin
applyKotlinExpoModulesCorePlugin()
useCoreDependencies()
useExpoPublishing()
// If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
// The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
// Most of the time, you may like to manage the Android SDK versions yourself.
def useManagedAndroidSdkVersions = false
if (useManagedAndroidSdkVersions) {
useDefaultAndroidSdkVersions()
} else {
buildscript {
// Simple helper that allows the root project to override versions declared by this library.
ext.safeExtGet = { prop, fallback ->
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}
}
project.android {
compileSdkVersion safeExtGet("compileSdkVersion", 34)
defaultConfig {
minSdkVersion safeExtGet("minSdkVersion", 21)
targetSdkVersion safeExtGet("targetSdkVersion", 34)
}
}
}
android {
namespace "expo.modules.bottomsheet"
defaultConfig {
versionCode 1
versionName "0.1.0"
}
lintOptions {
abortOnError false
}
}
dependencies {
implementation project(':expo-modules-core')
implementation 'com.google.android.material:material:1.12.0'
implementation "com.facebook.react:react-native:+"
}
@@ -0,0 +1,2 @@
<manifest>
</manifest>
@@ -0,0 +1,53 @@
package expo.modules.bottomsheet
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
class BottomSheetModule : Module() {
override fun definition() =
ModuleDefinition {
Name("BottomSheet")
AsyncFunction("dismissAll") {
SheetManager.dismissAll()
}
View(BottomSheetView::class) {
Events(
arrayOf(
"onAttemptDismiss",
"onSnapPointChange",
"onStateChange",
),
)
AsyncFunction("dismiss") { view: BottomSheetView ->
view.dismiss()
}
AsyncFunction("updateLayout") { view: BottomSheetView ->
view.updateLayout()
}
Prop("disableDrag") { view: BottomSheetView, prop: Boolean ->
view.disableDrag = prop
}
Prop("minHeight") { view: BottomSheetView, prop: Float ->
view.minHeight = prop
}
Prop("maxHeight") { view: BottomSheetView, prop: Float ->
view.maxHeight = prop
}
Prop("preventDismiss") { view: BottomSheetView, prop: Boolean ->
view.preventDismiss = prop
}
Prop("preventExpansion") { view: BottomSheetView, prop: Boolean ->
view.preventExpansion = prop
}
}
}
}
@@ -0,0 +1,339 @@
package expo.modules.bottomsheet
import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.view.ViewStructure
import android.view.accessibility.AccessibilityEvent
import android.widget.FrameLayout
import androidx.core.view.allViews
import com.facebook.react.bridge.LifecycleEventListener
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.uimanager.UIManagerHelper
import com.facebook.react.uimanager.events.EventDispatcher
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import expo.modules.kotlin.AppContext
import expo.modules.kotlin.viewevent.EventDispatcher
import expo.modules.kotlin.views.ExpoView
import java.util.ArrayList
class BottomSheetView(
context: Context,
appContext: AppContext,
) : ExpoView(context, appContext),
LifecycleEventListener {
private var innerView: View? = null
private var dialog: BottomSheetDialog? = null
private lateinit var dialogRootViewGroup: DialogRootViewGroup
private var eventDispatcher: EventDispatcher? = null
private val screenHeight =
context.resources.displayMetrics.heightPixels
.toFloat()
private val onAttemptDismiss by EventDispatcher()
private val onSnapPointChange by EventDispatcher()
private val onStateChange by EventDispatcher()
// Props
var disableDrag = false
set (value) {
field = value
this.setDraggable(!value)
}
var preventDismiss = false
set(value) {
field = value
this.dialog?.setCancelable(!value)
}
var preventExpansion = false
var minHeight = 0f
set(value) {
field =
if (value < 0) {
0f
} else {
value
}
}
var maxHeight = this.screenHeight
set(value) {
field =
if (value > this.screenHeight) {
this.screenHeight.toFloat()
} else {
value
}
}
private var isOpen: Boolean = false
set(value) {
field = value
onStateChange(
mapOf(
"state" to if (value) "open" else "closed",
),
)
}
private var isOpening: Boolean = false
set(value) {
field = value
if (value) {
onStateChange(
mapOf(
"state" to "opening",
),
)
}
}
private var isClosing: Boolean = false
set(value) {
field = value
if (value) {
onStateChange(
mapOf(
"state" to "closing",
),
)
}
}
private var selectedSnapPoint = 0
set(value) {
if (field == value) return
field = value
onSnapPointChange(
mapOf(
"snapPoint" to value,
),
)
}
// Lifecycle
init {
(appContext.reactContext as? ReactContext)?.let {
it.addLifecycleEventListener(this)
this.eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(it, this.id)
this.dialogRootViewGroup = DialogRootViewGroup(context)
this.dialogRootViewGroup.eventDispatcher = this.eventDispatcher
}
SheetManager.add(this)
}
override fun onLayout(
changed: Boolean,
l: Int,
t: Int,
r: Int,
b: Int,
) {
this.present()
}
private fun destroy() {
this.isClosing = false
this.isOpen = false
this.dialog = null
this.innerView = null
SheetManager.remove(this)
}
// Presentation
private fun present() {
if (this.isOpen || this.isOpening || this.isClosing) return
val contentHeight = this.getContentHeight()
val dialog = BottomSheetDialog(context)
dialog.setContentView(dialogRootViewGroup)
dialog.setCancelable(!preventDismiss)
dialog.setOnShowListener {
val bottomSheet = dialog.findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet)
bottomSheet?.let {
// Let the outside view handle the background color on its own, the default for this is
// white and we don't want that.
it.setBackgroundColor(0)
val behavior = BottomSheetBehavior.from(it)
behavior.isFitToContents = true
behavior.halfExpandedRatio = this.clampRatio(this.getTargetHeight() / this.screenHeight)
if (contentHeight > this.screenHeight) {
behavior.state = BottomSheetBehavior.STATE_EXPANDED
this.selectedSnapPoint = 2
} else {
behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
this.selectedSnapPoint = 1
}
behavior.skipCollapsed = true
behavior.isDraggable = true
behavior.isHideable = true
behavior.addBottomSheetCallback(
object : BottomSheetBehavior.BottomSheetCallback() {
override fun onStateChanged(
bottomSheet: View,
newState: Int,
) {
when (newState) {
BottomSheetBehavior.STATE_EXPANDED -> {
selectedSnapPoint = 2
}
BottomSheetBehavior.STATE_COLLAPSED -> {
selectedSnapPoint = 1
}
BottomSheetBehavior.STATE_HALF_EXPANDED -> {
selectedSnapPoint = 1
}
BottomSheetBehavior.STATE_HIDDEN -> {
selectedSnapPoint = 0
}
}
}
override fun onSlide(
bottomSheet: View,
slideOffset: Float,
) { }
},
)
}
}
dialog.setOnDismissListener {
this.isClosing = true
this.destroy()
}
this.isOpening = true
dialog.show()
this.dialog = dialog
}
fun updateLayout() {
val dialog = this.dialog ?: return
val contentHeight = this.getContentHeight()
val bottomSheet = dialog.findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet)
bottomSheet?.let {
val behavior = BottomSheetBehavior.from(it)
behavior.halfExpandedRatio = this.clampRatio(this.getTargetHeight() / this.screenHeight)
if (contentHeight > this.screenHeight && behavior.state != BottomSheetBehavior.STATE_EXPANDED) {
behavior.state = BottomSheetBehavior.STATE_EXPANDED
} else if (contentHeight < this.screenHeight && behavior.state != BottomSheetBehavior.STATE_HALF_EXPANDED) {
behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
}
}
}
fun dismiss() {
this.dialog?.dismiss()
}
// Util
private fun getContentHeight(): Float {
val innerView = this.innerView ?: return 0f
var index = 0
innerView.allViews.forEach {
if (index == 1) {
return it.height.toFloat()
}
index++
}
return 0f
}
private fun getTargetHeight(): Float {
val contentHeight = this.getContentHeight()
val height =
if (contentHeight > maxHeight) {
maxHeight
} else if (contentHeight < minHeight) {
minHeight
} else {
contentHeight
}
return height
}
private fun clampRatio(ratio: Float): Float {
if (ratio < 0.01) {
return 0.01f
} else if (ratio > 0.99) {
return 0.99f
}
return ratio
}
private fun setDraggable(draggable: Boolean) {
val dialog = this.dialog ?: return
val bottomSheet = dialog.findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet)
bottomSheet?.let {
val behavior = BottomSheetBehavior.from(it)
behavior.isDraggable = draggable
}
}
override fun onHostResume() { }
override fun onHostPause() { }
override fun onHostDestroy() {
(appContext.reactContext as? ReactContext)?.let {
it.removeLifecycleEventListener(this)
this.destroy()
}
}
// View overrides to pass to DialogRootViewGroup instead
override fun dispatchProvideStructure(structure: ViewStructure?) {
dialogRootViewGroup.dispatchProvideStructure(structure)
}
override fun setId(id: Int) {
super.setId(id)
dialogRootViewGroup.id = id
}
override fun addView(
child: View?,
index: Int,
) {
this.innerView = child
(child as ViewGroup).let {
dialogRootViewGroup.addView(child, index)
}
}
override fun removeView(view: View?) {
UiThreadUtil.assertOnUiThread()
if (view != null) {
dialogRootViewGroup.removeView(view)
}
}
override fun removeViewAt(index: Int) {
UiThreadUtil.assertOnUiThread()
val child = getChildAt(index)
dialogRootViewGroup.removeView(child)
}
override fun addChildrenForAccessibility(outChildren: ArrayList<View>?) { }
override fun dispatchPopulateAccessibilityEvent(event: AccessibilityEvent?): Boolean = false
}
@@ -0,0 +1,171 @@
package expo.modules.bottomsheet
import android.annotation.SuppressLint
import android.content.Context
import android.view.MotionEvent
import android.view.View
import com.facebook.react.bridge.GuardedRunnable
import com.facebook.react.config.ReactFeatureFlags
import com.facebook.react.uimanager.JSPointerDispatcher
import com.facebook.react.uimanager.JSTouchDispatcher
import com.facebook.react.uimanager.RootView
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.UIManagerModule
import com.facebook.react.uimanager.events.EventDispatcher
import com.facebook.react.views.view.ReactViewGroup
// SEE https://github.com/facebook/react-native/blob/309cdea337101cfe2212cfb6abebf1e783e43282/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.kt#L378
/**
* DialogRootViewGroup is the ViewGroup which contains all the children of a Modal. It gets all
* child information forwarded from [ReactModalHostView] and uses that to create children. It is
* also responsible for acting as a RootView and handling touch events. It does this the same way
* as ReactRootView.
*
* To get layout to work properly, we need to layout all the elements within the Modal as if they
* can fill the entire window. To do that, we need to explicitly set the styleWidth and
* styleHeight on the LayoutShadowNode to be the window size. This is done through the
* UIManagerModule, and will then cause the children to layout as if they can fill the window.
*/
class DialogRootViewGroup(
private val context: Context?,
) : ReactViewGroup(context),
RootView {
private var hasAdjustedSize = false
private var viewWidth = 0
private var viewHeight = 0
private val jSTouchDispatcher = JSTouchDispatcher(this)
private var jSPointerDispatcher: JSPointerDispatcher? = null
private var sizeChangeListener: OnSizeChangeListener? = null
var eventDispatcher: EventDispatcher? = null
interface OnSizeChangeListener {
fun onSizeChange(
width: Int,
height: Int,
)
}
init {
if (ReactFeatureFlags.dispatchPointerEvents) {
jSPointerDispatcher = JSPointerDispatcher(this)
}
}
override fun onSizeChanged(
w: Int,
h: Int,
oldw: Int,
oldh: Int,
) {
super.onSizeChanged(w, h, oldw, oldh)
viewWidth = w
viewHeight = h
updateFirstChildView()
sizeChangeListener?.onSizeChange(w, h)
}
fun setOnSizeChangeListener(listener: OnSizeChangeListener) {
sizeChangeListener = listener
}
private fun updateFirstChildView() {
if (childCount > 0) {
hasAdjustedSize = false
val viewTag = getChildAt(0).id
reactContext.runOnNativeModulesQueueThread(
object : GuardedRunnable(reactContext) {
override fun runGuarded() {
val uiManager: UIManagerModule =
reactContext
.reactApplicationContext
.getNativeModule(UIManagerModule::class.java) ?: return
uiManager.updateNodeSize(viewTag, viewWidth, viewHeight)
}
},
)
} else {
hasAdjustedSize = true
}
}
override fun addView(
child: View,
index: Int,
params: LayoutParams,
) {
super.addView(child, index, params)
if (hasAdjustedSize) {
updateFirstChildView()
}
}
override fun handleException(t: Throwable) {
reactContext.reactApplicationContext.handleException(RuntimeException(t))
}
private val reactContext: ThemedReactContext
get() = context as ThemedReactContext
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
eventDispatcher?.let { jSTouchDispatcher.handleTouchEvent(event, it) }
jSPointerDispatcher?.handleMotionEvent(event, eventDispatcher, true)
return super.onInterceptTouchEvent(event)
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
eventDispatcher?.let { jSTouchDispatcher.handleTouchEvent(event, it) }
jSPointerDispatcher?.handleMotionEvent(event, eventDispatcher, false)
super.onTouchEvent(event)
// In case when there is no children interested in handling touch event, we return true from
// the root view in order to receive subsequent events related to that gesture
return true
}
override fun onInterceptHoverEvent(event: MotionEvent): Boolean {
jSPointerDispatcher?.handleMotionEvent(event, eventDispatcher, true)
return super.onHoverEvent(event)
}
override fun onHoverEvent(event: MotionEvent): Boolean {
jSPointerDispatcher?.handleMotionEvent(event, eventDispatcher, false)
return super.onHoverEvent(event)
}
@Deprecated("Deprecated in Java")
override fun onChildStartedNativeGesture(ev: MotionEvent?) {
eventDispatcher?.let {
if (ev != null) {
jSTouchDispatcher.onChildStartedNativeGesture(ev, it)
}
}
}
override fun onChildStartedNativeGesture(
childView: View,
ev: MotionEvent,
) {
eventDispatcher?.let { jSTouchDispatcher.onChildStartedNativeGesture(ev, it) }
jSPointerDispatcher?.onChildStartedNativeGesture(childView, ev, eventDispatcher)
}
override fun onChildEndedNativeGesture(
childView: View,
ev: MotionEvent,
) {
eventDispatcher?.let { jSTouchDispatcher.onChildEndedNativeGesture(ev, it) }
jSPointerDispatcher?.onChildEndedNativeGesture()
}
override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
// No-op - override in order to still receive events to onInterceptTouchEvent
// even when some other view disallow that
}
}
@@ -0,0 +1,28 @@
package expo.modules.bottomsheet
import java.lang.ref.WeakReference
class SheetManager {
companion object {
private val sheets = mutableSetOf<WeakReference<BottomSheetView>>()
fun add(view: BottomSheetView) {
sheets.add(WeakReference(view))
}
fun remove(view: BottomSheetView) {
sheets.forEach {
if (it.get() == view) {
sheets.remove(it)
return
}
}
}
fun dismissAll() {
sheets.forEach {
it.get()?.dismiss()
}
}
}
}
@@ -0,0 +1,9 @@
{
"platforms": ["ios", "android"],
"ios": {
"modules": ["BottomSheetModule"]
},
"android": {
"modules": ["expo.modules.bottomsheet.BottomSheetModule"]
}
}
+23
View File
@@ -0,0 +1,23 @@
import {BottomSheet} from './src/BottomSheet'
import {
BottomSheetSnapPoint,
BottomSheetState,
BottomSheetViewProps,
} from './src/BottomSheet.types'
import {BottomSheetNativeComponent} from './src/BottomSheetNativeComponent'
import {
BottomSheetOutlet,
BottomSheetPortalProvider,
BottomSheetProvider,
} from './src/BottomSheetPortal'
export {
BottomSheet,
BottomSheetNativeComponent,
BottomSheetOutlet,
BottomSheetPortalProvider,
BottomSheetProvider,
BottomSheetSnapPoint,
type BottomSheetState,
type BottomSheetViewProps,
}
@@ -0,0 +1,21 @@
Pod::Spec.new do |s|
s.name = 'BottomSheet'
s.version = '1.0.0'
s.summary = 'A bottom sheet for use in Bluesky'
s.description = 'A bottom sheet for use in Bluesky'
s.author = ''
s.homepage = 'https://github.com/bluesky-social/social-app'
s.platforms = { :ios => '15.0', :tvos => '15.0' }
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,swift}"
end
@@ -0,0 +1,47 @@
import ExpoModulesCore
public class BottomSheetModule: Module {
public func definition() -> ModuleDefinition {
Name("BottomSheet")
AsyncFunction("dismissAll") {
SheetManager.shared.dismissAll()
}
View(SheetView.self) {
Events([
"onAttemptDismiss",
"onSnapPointChange",
"onStateChange"
])
AsyncFunction("dismiss") { (view: SheetView) in
view.dismiss()
}
AsyncFunction("updateLayout") { (view: SheetView) in
view.updateLayout()
}
Prop("cornerRadius") { (view: SheetView, prop: Float) in
view.cornerRadius = CGFloat(prop)
}
Prop("minHeight") { (view: SheetView, prop: Double) in
view.minHeight = prop
}
Prop("maxHeight") { (view: SheetView, prop: Double) in
view.maxHeight = prop
}
Prop("preventDismiss") { (view: SheetView, prop: Bool) in
view.preventDismiss = prop
}
Prop("preventExpansion") { (view: SheetView, prop: Bool) in
view.preventExpansion = prop
}
}
}
}
@@ -0,0 +1,28 @@
//
// SheetManager.swift
// Pods
//
// Created by Hailey on 10/1/24.
//
import ExpoModulesCore
class SheetManager {
static let shared = SheetManager()
private var sheetViews = NSHashTable<SheetView>(options: .weakMemory)
func add(_ view: SheetView) {
sheetViews.add(view)
}
func remove(_ view: SheetView) {
sheetViews.remove(view)
}
func dismissAll() {
sheetViews.allObjects.forEach { sheetView in
sheetView.dismiss()
}
}
}
+192
View File
@@ -0,0 +1,192 @@
import ExpoModulesCore
import UIKit
class SheetView: ExpoView, UISheetPresentationControllerDelegate {
// Views
private var sheetVc: SheetViewController?
private var innerView: UIView?
private var touchHandler: RCTTouchHandler?
// Events
private let onAttemptDismiss = EventDispatcher()
private let onSnapPointChange = EventDispatcher()
private let onStateChange = EventDispatcher()
// Open event firing
private var isOpen: Bool = false {
didSet {
onStateChange([
"state": isOpen ? "open" : "closed"
])
}
}
// React view props
var preventDismiss = false
var preventExpansion = false
var cornerRadius: CGFloat?
var minHeight = 0.0
var maxHeight: CGFloat! {
didSet {
let screenHeight = Util.getScreenHeight() ?? 0
if maxHeight > screenHeight {
maxHeight = screenHeight
}
}
}
private var isOpening = false {
didSet {
if isOpening {
onStateChange([
"state": "opening"
])
}
}
}
private var isClosing = false {
didSet {
if isClosing {
onStateChange([
"state": "closing"
])
}
}
}
private var selectedDetentIdentifier: UISheetPresentationController.Detent.Identifier? {
didSet {
if selectedDetentIdentifier == .large {
onSnapPointChange([
"snapPoint": 2
])
} else {
onSnapPointChange([
"snapPoint": 1
])
}
}
}
private var prevLayoutDetentIdentifier: UISheetPresentationController.Detent.Identifier?
// MARK: - Lifecycle
required init (appContext: AppContext? = nil) {
super.init(appContext: appContext)
self.maxHeight = Util.getScreenHeight()
self.touchHandler = RCTTouchHandler(bridge: appContext?.reactBridge)
SheetManager.shared.add(self)
}
deinit {
self.destroy()
}
// We don't want this view to actually get added to the tree, so we'll simply store it for adding
// to the SheetViewController
override func insertReactSubview(_ subview: UIView!, at atIndex: Int) {
self.touchHandler?.attach(to: subview)
self.innerView = subview
}
// We'll grab the content height from here so we know the initial detent to set
override func layoutSubviews() {
super.layoutSubviews()
guard let innerView = self.innerView else {
return
}
if innerView.subviews.count != 1 {
return
}
self.present()
}
private func destroy() {
self.isClosing = false
self.isOpen = false
self.sheetVc = nil
self.touchHandler?.detach(from: self.innerView)
self.touchHandler = nil
self.innerView = nil
SheetManager.shared.remove(self)
}
// MARK: - Presentation
func present() {
guard !self.isOpen,
!self.isOpening,
!self.isClosing,
let innerView = self.innerView,
let contentHeight = innerView.subviews.first?.frame.height,
let rvc = self.reactViewController() else {
return
}
let sheetVc = SheetViewController()
sheetVc.setDetents(contentHeight: self.clampHeight(contentHeight), preventExpansion: self.preventExpansion)
if let sheet = sheetVc.sheetPresentationController {
sheet.delegate = self
sheet.preferredCornerRadius = self.cornerRadius
self.selectedDetentIdentifier = sheet.selectedDetentIdentifier
}
sheetVc.view.addSubview(innerView)
self.sheetVc = sheetVc
self.isOpening = true
rvc.present(sheetVc, animated: true) { [weak self] in
self?.isOpening = false
self?.isOpen = true
}
}
func updateLayout() {
if self.prevLayoutDetentIdentifier == self.selectedDetentIdentifier,
let contentHeight = self.innerView?.subviews.first?.frame.size.height {
self.sheetVc?.updateDetents(contentHeight: self.clampHeight(contentHeight),
preventExpansion: self.preventExpansion)
self.selectedDetentIdentifier = self.sheetVc?.getCurrentDetentIdentifier()
}
self.prevLayoutDetentIdentifier = self.selectedDetentIdentifier
}
func dismiss() {
self.isClosing = true
self.sheetVc?.dismiss(animated: true) { [weak self] in
self?.destroy()
}
}
// MARK: - Utils
private func clampHeight(_ height: CGFloat) -> CGFloat {
if height < self.minHeight {
return self.minHeight
} else if height > self.maxHeight {
return self.maxHeight
}
return height
}
// MARK: - UISheetPresentationControllerDelegate
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
self.onAttemptDismiss()
return !self.preventDismiss
}
func presentationControllerWillDismiss(_ presentationController: UIPresentationController) {
self.isClosing = true
}
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
self.destroy()
}
func sheetPresentationControllerDidChangeSelectedDetentIdentifier(_ sheetPresentationController: UISheetPresentationController) {
self.selectedDetentIdentifier = sheetPresentationController.selectedDetentIdentifier
}
}
@@ -0,0 +1,76 @@
//
// SheetViewController.swift
// Pods
//
// Created by Hailey on 9/30/24.
//
import Foundation
import UIKit
class SheetViewController: UIViewController {
init() {
super.init(nibName: nil, bundle: nil)
self.modalPresentationStyle = .formSheet
self.isModalInPresentation = false
if let sheet = self.sheetPresentationController {
sheet.prefersGrabberVisible = false
}
}
func setDetents(contentHeight: CGFloat, preventExpansion: Bool) {
guard let sheet = self.sheetPresentationController,
let screenHeight = Util.getScreenHeight()
else {
return
}
if contentHeight > screenHeight - 100 {
sheet.detents = [
.large()
]
sheet.selectedDetentIdentifier = .large
} else {
if #available(iOS 16.0, *) {
sheet.detents = [
.custom { _ in
return contentHeight
}
]
} else {
sheet.detents = [
.medium()
]
}
if !preventExpansion {
sheet.detents.append(.large())
}
sheet.selectedDetentIdentifier = .medium
}
}
func updateDetents(contentHeight: CGFloat, preventExpansion: Bool) {
if let sheet = self.sheetPresentationController {
sheet.animateChanges {
self.setDetents(contentHeight: contentHeight, preventExpansion: preventExpansion)
if #available(iOS 16.0, *) {
sheet.invalidateDetents()
}
}
}
}
func getCurrentDetentIdentifier() -> UISheetPresentationController.Detent.Identifier? {
guard let sheet = self.sheetPresentationController else {
return nil
}
return sheet.selectedDetentIdentifier
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
+18
View File
@@ -0,0 +1,18 @@
//
// Util.swift
// Pods
//
// Created by Hailey on 10/2/24.
//
class Util {
static func getScreenHeight() -> CGFloat? {
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first {
let safeAreaInsets = window.safeAreaInsets
let fullScreenHeight = UIScreen.main.bounds.height
return fullScreenHeight - (safeAreaInsets.top + safeAreaInsets.bottom)
}
return nil
}
}
+24
View File
@@ -0,0 +1,24 @@
import React from 'react'
import {BottomSheetViewProps} from './BottomSheet.types'
import {BottomSheetNativeComponent} from './BottomSheetNativeComponent'
import {useBottomSheetPortal_INTERNAL} from './BottomSheetPortal'
export const BottomSheet = React.forwardRef<
BottomSheetNativeComponent,
BottomSheetViewProps
>(function BottomSheet(props, ref) {
const Portal = useBottomSheetPortal_INTERNAL()
if (__DEV__ && !Portal) {
throw new Error(
'BottomSheet: You need to wrap your component tree with a <BottomSheetPortalProvider> to use the bottom sheet.',
)
}
return (
<Portal>
<BottomSheetNativeComponent {...props} ref={ref} />
</Portal>
)
})
@@ -0,0 +1,35 @@
import React from 'react'
import {ColorValue, NativeSyntheticEvent} from 'react-native'
export type BottomSheetState = 'closed' | 'closing' | 'open' | 'opening'
export enum BottomSheetSnapPoint {
Hidden,
Partial,
Full,
}
export type BottomSheetAttemptDismissEvent = NativeSyntheticEvent<object>
export type BottomSheetSnapPointChangeEvent = NativeSyntheticEvent<{
snapPoint: BottomSheetSnapPoint
}>
export type BottomSheetStateChangeEvent = NativeSyntheticEvent<{
state: BottomSheetState
}>
export interface BottomSheetViewProps {
children: React.ReactNode
cornerRadius?: number
preventDismiss?: boolean
preventExpansion?: boolean
backgroundColor?: ColorValue
containerBackgroundColor?: ColorValue
disableDrag?: boolean
minHeight?: number
maxHeight?: number
onAttemptDismiss?: (event: BottomSheetAttemptDismissEvent) => void
onSnapPointChange?: (event: BottomSheetSnapPointChangeEvent) => void
onStateChange?: (event: BottomSheetStateChangeEvent) => void
}
@@ -0,0 +1,5 @@
import {BottomSheetViewProps} from './BottomSheet.types'
export function BottomSheet(_: BottomSheetViewProps) {
throw new Error('BottomSheet is not available on web')
}
@@ -0,0 +1,103 @@
import * as React from 'react'
import {
Dimensions,
NativeSyntheticEvent,
Platform,
StyleProp,
View,
ViewStyle,
} from 'react-native'
import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core'
import {BottomSheetState, BottomSheetViewProps} from './BottomSheet.types'
import {BottomSheetPortalProvider} from './BottomSheetPortal'
const screenHeight = Dimensions.get('screen').height
const NativeView: React.ComponentType<
BottomSheetViewProps & {
ref: React.RefObject<any>
style: StyleProp<ViewStyle>
}
> = requireNativeViewManager('BottomSheet')
const NativeModule = requireNativeModule('BottomSheet')
export class BottomSheetNativeComponent extends React.Component<
BottomSheetViewProps,
{
open: boolean
}
> {
ref = React.createRef<any>()
constructor(props: BottomSheetViewProps) {
super(props)
this.state = {
open: false,
}
}
present() {
this.setState({open: true})
}
dismiss() {
this.ref.current?.dismiss()
}
private onStateChange = (
event: NativeSyntheticEvent<{state: BottomSheetState}>,
) => {
const {state} = event.nativeEvent
const isOpen = state !== 'closed'
this.setState({open: isOpen})
this.props.onStateChange?.(event)
}
private updateLayout = () => {
this.ref.current?.updateLayout()
}
static dismissAll = async () => {
await NativeModule.dismissAll()
}
render() {
const {children, backgroundColor, ...rest} = this.props
const cornerRadius = rest.cornerRadius ?? 0
if (!this.state.open) {
return null
}
return (
<NativeView
{...rest}
onStateChange={this.onStateChange}
ref={this.ref}
style={{
position: 'absolute',
height: screenHeight,
width: '100%',
}}
containerBackgroundColor={backgroundColor}>
<View
style={[
{
flex: 1,
backgroundColor,
},
Platform.OS === 'android' && {
borderTopLeftRadius: cornerRadius,
borderTopRightRadius: cornerRadius,
},
]}>
<View onLayout={this.updateLayout}>
<BottomSheetPortalProvider>{children}</BottomSheetPortalProvider>
</View>
</View>
</NativeView>
)
}
}
@@ -0,0 +1,5 @@
import {BottomSheetViewProps} from './BottomSheet.types'
export function BottomSheetNativeComponent(_: BottomSheetViewProps) {
throw new Error('BottomSheetNativeComponent is not available on web')
}
@@ -0,0 +1,40 @@
import React from 'react'
import {createPortalGroup_INTERNAL} from './lib/Portal'
type PortalContext = React.ElementType<{children: React.ReactNode}>
const Context = React.createContext({} as PortalContext)
export const useBottomSheetPortal_INTERNAL = () => React.useContext(Context)
export function BottomSheetPortalProvider({
children,
}: {
children: React.ReactNode
}) {
const portal = React.useMemo(() => {
return createPortalGroup_INTERNAL()
}, [])
return (
<Context.Provider value={portal.Portal}>
<portal.Provider>
{children}
<portal.Outlet />
</portal.Provider>
</Context.Provider>
)
}
const defaultPortal = createPortalGroup_INTERNAL()
export const BottomSheetOutlet = defaultPortal.Outlet
export function BottomSheetProvider({children}: {children: React.ReactNode}) {
return (
<Context.Provider value={defaultPortal.Portal}>
<defaultPortal.Provider>{children}</defaultPortal.Provider>
</Context.Provider>
)
}
+67
View File
@@ -0,0 +1,67 @@
import React from 'react'
type Component = React.ReactElement
type ContextType = {
outlet: Component | null
append(id: string, component: Component): void
remove(id: string): void
}
type ComponentMap = {
[id: string]: Component
}
export function createPortalGroup_INTERNAL() {
const Context = React.createContext<ContextType>({
outlet: null,
append: () => {},
remove: () => {},
})
function Provider(props: React.PropsWithChildren<{}>) {
const map = React.useRef<ComponentMap>({})
const [outlet, setOutlet] = React.useState<ContextType['outlet']>(null)
const append = React.useCallback<ContextType['append']>((id, component) => {
if (map.current[id]) return
map.current[id] = <React.Fragment key={id}>{component}</React.Fragment>
setOutlet(<>{Object.values(map.current)}</>)
}, [])
const remove = React.useCallback<ContextType['remove']>(id => {
delete map.current[id]
setOutlet(<>{Object.values(map.current)}</>)
}, [])
const contextValue = React.useMemo(
() => ({
outlet,
append,
remove,
}),
[outlet, append, remove],
)
return (
<Context.Provider value={contextValue}>{props.children}</Context.Provider>
)
}
function Outlet() {
const ctx = React.useContext(Context)
return ctx.outlet
}
function Portal({children}: React.PropsWithChildren<{}>) {
const {append, remove} = React.useContext(Context)
const id = React.useId()
React.useEffect(() => {
append(id, children as Component)
return () => remove(id)
}, [id, children, append, remove])
return null
}
return {Provider, Outlet, Portal}
}
@@ -2,6 +2,7 @@ package xyz.blueskyweb.app.exporeceiveandroidintents
import android.content.Intent
import android.graphics.Bitmap
import android.media.MediaMetadataRetriever
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
@@ -143,7 +144,10 @@ class ExpoReceiveAndroidIntentsModule : Module() {
appContext.currentActivity?.contentResolver?.openInputStream(uri)?.use {
it.copyTo(out)
}
"bluesky://intent/compose?videoUri=${URLEncoder.encode(file.path, "UTF-8")}".toUri().let {
val info = getVideoInfo(uri) ?: return
"bluesky://intent/compose?videoUri=${URLEncoder.encode(file.path, "UTF-8")}|${info["width"]}|${info["height"]}".toUri().let {
val newIntent = Intent(Intent.ACTION_VIEW, it)
appContext.currentActivity?.startActivity(newIntent)
}
@@ -166,6 +170,24 @@ class ExpoReceiveAndroidIntentsModule : Module() {
)
}
private fun getVideoInfo(uri: Uri): Map<String, Any>? {
val retriever = MediaMetadataRetriever()
retriever.setDataSource(appContext.currentActivity, uri)
val width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toIntOrNull()
val height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toIntOrNull()
if (width == null || height == null) {
return null
}
return mapOf(
"width" to width,
"height" to height,
"path" to uri.path.toString(),
)
}
private fun createFile(extension: String): File = File.createTempFile(extension, "temp.$extension", appContext.currentActivity?.cacheDir)
// We will pas the width and height to the app here, since getting measurements
+11 -10
View File
@@ -1,6 +1,6 @@
{
"name": "bsky.app",
"version": "1.91.2",
"version": "1.92.0",
"private": true,
"engines": {
"node": ">=18"
@@ -53,7 +53,7 @@
"icons:optimize": "svgo -f ./assets/icons"
},
"dependencies": {
"@atproto/api": "^0.13.8",
"@atproto/api": "^0.13.11",
"@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
"@emoji-mart/react": "^1.1.1",
@@ -68,11 +68,14 @@
"@fortawesome/free-regular-svg-icons": "^6.1.1",
"@fortawesome/free-solid-svg-icons": "^6.1.1",
"@fortawesome/react-native-fontawesome": "^0.3.2",
"@haileyok/bluesky-video": "0.1.10",
"@haileyok/bluesky-video": "0.2.2",
"@lingui/react": "^4.5.0",
"@mattermost/react-native-paste-input": "^0.7.1",
"@miblanchard/react-native-slider": "^2.3.1",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-dismissable-layer": "^1.1.1",
"@radix-ui/react-dropdown-menu": "2.0.1",
"@radix-ui/react-focus-guards": "^1.1.1",
"@radix-ui/react-focus-scope": "^1.1.0",
"@react-native-async-storage/async-storage": "1.23.1",
"@react-native-masked-view/masked-view": "0.3.0",
"@react-native-menu/menu": "^1.1.0",
@@ -81,8 +84,7 @@
"@react-navigation/drawer": "^6.6.15",
"@react-navigation/native": "^6.1.17",
"@react-navigation/native-stack": "^6.9.26",
"@sentry/react-native": "5.32.0",
"@tamagui/focus-scope": "^1.84.1",
"@sentry/react-native": "5.24.3",
"@tanstack/query-async-storage-persister": "^5.25.0",
"@tanstack/react-query": "^5.8.1",
"@tanstack/react-query-persist-client": "^5.25.0",
@@ -137,7 +139,6 @@
"expo-sharing": "^12.0.1",
"expo-splash-screen": "~0.27.4",
"expo-status-bar": "~1.12.1",
"expo-system-ui": "~3.0.4",
"expo-task-manager": "~11.8.1",
"expo-updates": "~0.25.14",
"expo-web-browser": "~13.0.3",
@@ -171,11 +172,11 @@
"react-native-compressor": "^1.8.24",
"react-native-date-picker": "^4.4.2",
"react-native-drawer-layout": "^4.0.0-alpha.3",
"react-native-gesture-handler": "~2.16.2",
"react-native-gesture-handler": "2.20.0",
"react-native-get-random-values": "~1.11.0",
"react-native-image-crop-picker": "0.41.2",
"react-native-ios-context-menu": "^1.15.3",
"react-native-keyboard-controller": "^1.12.1",
"react-native-keyboard-controller": "^1.14.0",
"react-native-mmkv": "^2.12.2",
"react-native-pager-view": "6.2.3",
"react-native-picker-select": "^9.1.3",
@@ -204,7 +205,7 @@
"zod": "^3.20.2"
},
"devDependencies": {
"@atproto/dev-env": "^0.3.39",
"@atproto/dev-env": "^0.3.55",
"@babel/core": "^7.23.2",
"@babel/preset-env": "^7.20.0",
"@babel/runtime": "^7.20.0",
+63 -17
View File
@@ -38,37 +38,65 @@ index b0d71dc..41b9a0e 100644
- (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
index e9b330f..ec5f58c 100644
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h
+++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h
@@ -16,4 +16,6 @@
@@ -15,5 +15,8 @@
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) RCTDirectEventBlock onRefresh;
@property (nonatomic, weak) UIScrollView *scrollView;
+- (void)forwarderBeginRefreshing;
+@property (nonatomic, copy) UIColor *customTintColor;
+
+- (void)forwarderBeginRefreshing;
@end
diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
index b09e653..f93cb46 100644
index b09e653..288e60c 100644
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
+++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
@@ -198,9 +198,53 @@ - (void)refreshControlValueChanged
[self setCurrentRefreshingState:super.refreshing];
_refreshingProgrammatically = NO;
@@ -22,6 +22,7 @@ @implementation RCTRefreshControl {
NSString *_title;
UIColor *_titleColor;
CGFloat _progressViewOffset;
+ UIColor *_customTintColor;
}
+ if (@available(iOS 17.4, *)) {
+ if (_currentRefreshingState) {
+ UIImpactFeedbackGenerator *feedbackGenerator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight];
+ [feedbackGenerator prepare];
+ [feedbackGenerator impactOccurred];
+ }
+ }
- (instancetype)init
@@ -56,6 +57,12 @@ - (void)layoutSubviews
_isInitialRender = false;
}
+- (void)didMoveToSuperview
+{
+ [super didMoveToSuperview];
+ [self setTintColor:_customTintColor];
+}
+
if (_onRefresh) {
_onRefresh(nil);
- (void)beginRefreshingProgrammatically
{
UInt64 beginRefreshingTimestamp = _currentRefreshingStateTimestamp;
@@ -203,4 +210,58 @@ - (void)refreshControlValueChanged
}
}
+- (void)setCustomTintColor:(UIColor *)customTintColor
+{
+ _customTintColor = customTintColor;
+ [self setTintColor:customTintColor];
+}
+
+// Fix for https://github.com/facebook/react-native/issues/43388
+// A bug in iOS 17.4 causes the haptic to not play when refreshing if the tintColor
+// is set before the refresh control gets added to the scrollview. We'll call this
+// function whenever the superview changes. We'll also call it if the value of customTintColor
+// changes.
+- (void)setTintColor:(UIColor *)tintColor
+{
+ if ([self.superview isKindOfClass:[UIScrollView class]] && self.tintColor != tintColor) {
+ [super setTintColor:tintColor];
+ }
+}
+
+/*
+ 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
@@ -106,6 +134,24 @@ index b09e653..f93cb46 100644
+}
+
@end
diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m
index 40aaf9c..1c60164 100644
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m
+++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m
@@ -22,11 +22,12 @@ - (UIView *)view
RCT_EXPORT_VIEW_PROPERTY(onRefresh, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(refreshing, BOOL)
-RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(title, NSString)
RCT_EXPORT_VIEW_PROPERTY(titleColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(progressViewOffset, CGFloat)
+RCT_REMAP_VIEW_PROPERTY(tintColor, customTintColor, UIColor)
+
RCT_EXPORT_METHOD(setNativeRefreshing : (nonnull NSNumber *)viewTag toRefreshing : (BOOL)refreshing)
{
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
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
+2 -2
View File
@@ -23,10 +23,10 @@ const outputFile = entrypoints
const ext = path.extname(file)
if (ext === '.js') {
return `<script defer="defer" src="/static/js/${file}"></script>`
return `<script defer="defer" src="{{ staticCDNHost }}/static/js/${file}"></script>`
}
if (ext === '.css') {
return `<link rel="stylesheet" href="/static/css/${file}">`
return `<link rel="stylesheet" href="{{ staticCDNHost }}/static/css/${file}">`
}
return ''
+45 -39
View File
@@ -1,6 +1,6 @@
import 'react-native-url-polyfill/auto'
import 'lib/sentry' // must be near top
import 'view/icons'
import '#/lib/sentry' // must be near top
import '#/view/icons'
import React, {useEffect, useState} from 'react'
import {GestureHandlerRootView} from 'react-native-gesture-handler'
@@ -51,6 +51,7 @@ import {
} from '#/state/session'
import {readLastActiveAccount} from '#/state/session/util'
import {Provider as ShellStateProvider} from '#/state/shell'
import {Provider as ComposerProvider} from '#/state/shell/composer'
import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out'
import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
@@ -67,6 +68,7 @@ import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
import {Provider as PortalProvider} from '#/components/Portal'
import {Splash} from '#/Splash'
import {BottomSheetProvider} from '../modules/bottom-sheet'
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
SplashScreen.preventAutoHideAsync()
@@ -125,35 +127,37 @@ function InnerApp() {
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<QueryProvider currentDid={currentAccount?.did}>
<StatsigProvider>
<MessagesProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<LabelDefsProvider>
<ModerationOptsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<HiddenRepliesProvider>
<UnreadNotifsProvider>
<BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider>
<ProgressGuideProvider>
<GestureHandlerRootView
style={s.h100pct}>
<TestCtrls />
<Shell />
<NuxDialogs />
</GestureHandlerRootView>
</ProgressGuideProvider>
</MutedThreadsProvider>
</BackgroundNotificationPreferencesProvider>
</UnreadNotifsProvider>
</HiddenRepliesProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</ModerationOptsProvider>
</LabelDefsProvider>
</MessagesProvider>
</StatsigProvider>
<ComposerProvider>
<StatsigProvider>
<MessagesProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<LabelDefsProvider>
<ModerationOptsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<HiddenRepliesProvider>
<UnreadNotifsProvider>
<BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider>
<ProgressGuideProvider>
<GestureHandlerRootView
style={s.h100pct}>
<TestCtrls />
<Shell />
<NuxDialogs />
</GestureHandlerRootView>
</ProgressGuideProvider>
</MutedThreadsProvider>
</BackgroundNotificationPreferencesProvider>
</UnreadNotifsProvider>
</HiddenRepliesProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</ModerationOptsProvider>
</LabelDefsProvider>
</MessagesProvider>
</StatsigProvider>
</ComposerProvider>
</QueryProvider>
</React.Fragment>
</VideoVolumeProvider>
@@ -194,14 +198,16 @@ function App() {
<DialogStateProvider>
<LightboxStateProvider>
<PortalProvider>
<StarterPackProvider>
<SafeAreaProvider
initialMetrics={initialWindowMetrics}>
<IntentDialogProvider>
<InnerApp />
</IntentDialogProvider>
</SafeAreaProvider>
</StarterPackProvider>
<BottomSheetProvider>
<StarterPackProvider>
<SafeAreaProvider
initialMetrics={initialWindowMetrics}>
<IntentDialogProvider>
<InnerApp />
</IntentDialogProvider>
</SafeAreaProvider>
</StarterPackProvider>
</BottomSheetProvider>
</PortalProvider>
</LightboxStateProvider>
</DialogStateProvider>
+30 -27
View File
@@ -41,6 +41,7 @@ import {
} from '#/state/session'
import {readLastActiveAccount} from '#/state/session/util'
import {Provider as ShellStateProvider} from '#/state/shell'
import {Provider as ComposerProvider} from '#/state/shell/composer'
import {useComposerKeyboardShortcut} from '#/state/shell/composer/useComposerKeyboardShortcut'
import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out'
import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
@@ -116,33 +117,35 @@ function InnerApp() {
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<QueryProvider currentDid={currentAccount?.did}>
<StatsigProvider>
<MessagesProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<LabelDefsProvider>
<ModerationOptsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<HiddenRepliesProvider>
<UnreadNotifsProvider>
<BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider>
<SafeAreaProvider>
<ProgressGuideProvider>
<Shell />
<NuxDialogs />
</ProgressGuideProvider>
</SafeAreaProvider>
</MutedThreadsProvider>
</BackgroundNotificationPreferencesProvider>
</UnreadNotifsProvider>
</HiddenRepliesProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</ModerationOptsProvider>
</LabelDefsProvider>
</MessagesProvider>
</StatsigProvider>
<ComposerProvider>
<StatsigProvider>
<MessagesProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<LabelDefsProvider>
<ModerationOptsProvider>
<LoggedOutViewProvider>
<SelectedFeedProvider>
<HiddenRepliesProvider>
<UnreadNotifsProvider>
<BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider>
<SafeAreaProvider>
<ProgressGuideProvider>
<Shell />
<NuxDialogs />
</ProgressGuideProvider>
</SafeAreaProvider>
</MutedThreadsProvider>
</BackgroundNotificationPreferencesProvider>
</UnreadNotifsProvider>
</HiddenRepliesProvider>
</SelectedFeedProvider>
</LoggedOutViewProvider>
</ModerationOptsProvider>
</LabelDefsProvider>
</MessagesProvider>
</StatsigProvider>
</ComposerProvider>
</QueryProvider>
<ToastContainer />
</React.Fragment>
+1 -1
View File
@@ -78,8 +78,8 @@ import {BottomBar} from '#/view/shell/bottom-bar/BottomBar'
import {createNativeStackNavigatorWithAuth} from '#/view/shell/createNativeStackNavigatorWithAuth'
import {SharedPreferencesTesterScreen} from '#/screens/E2E/SharedPreferencesTesterScreen'
import HashtagScreen from '#/screens/Hashtag'
import {MessagesScreen} from '#/screens/Messages/ChatList'
import {MessagesConversationScreen} from '#/screens/Messages/Conversation'
import {MessagesScreen} from '#/screens/Messages/List'
import {MessagesSettingsScreen} from '#/screens/Messages/Settings'
import {ModerationScreen} from '#/screens/Moderation'
import {PostLikedByScreen} from '#/screens/Post/PostLikedBy'
+31 -3
View File
@@ -276,13 +276,13 @@ export const atoms = {
letterSpacing: tokens.TRACKING,
},
font_normal: {
fontWeight: tokens.fontWeight.regular,
fontWeight: tokens.fontWeight.normal,
},
font_bold: {
fontWeight: tokens.fontWeight.semibold,
fontWeight: tokens.fontWeight.bold,
},
font_heavy: {
fontWeight: tokens.fontWeight.extrabold,
fontWeight: tokens.fontWeight.heavy,
},
italic: {
fontStyle: 'italic',
@@ -901,4 +901,32 @@ export const atoms = {
hidden: {
display: 'none',
},
/*
* Transition
*/
transition_none: web({
transitionProperty: 'none',
}),
transition_all: web({
transitionProperty: 'all',
transitionTimingFunction: 'cubic-bezier(0.17, 0.73, 0.14, 1)',
transitionDuration: '100ms',
}),
transition_color: web({
transitionProperty:
'color, background-color, border-color, text-decoration-color, fill, stroke',
transitionTimingFunction: 'cubic-bezier(0.17, 0.73, 0.14, 1)',
transitionDuration: '100ms',
}),
transition_opacity: web({
transitionProperty: 'opacity',
transitionTimingFunction: 'cubic-bezier(0.17, 0.73, 0.14, 1)',
transitionDuration: '100ms',
}),
transition_transform: web({
transitionProperty: 'transform',
transitionTimingFunction: 'cubic-bezier(0.17, 0.73, 0.14, 1)',
transitionDuration: '100ms',
}),
} as const
+17 -1
View File
@@ -1,3 +1,5 @@
import {useFonts} from 'expo-font'
import {isWeb} from '#/platform/detection'
import {Device, device} from '#/storage'
@@ -61,5 +63,19 @@ export function applyFonts(
* Disable contextual ligatures
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/font-variant}
*/
style.fontVariant = ['no-contextual']
style.fontVariant = (style.fontVariant || []).concat('no-contextual')
}
/*
* IMPORTANT: This is unused. Expo statically extracts these fonts.
*
* All used fonts MUST be configured here. Unused fonts can be commented out.
*
* This is used for both web fonts and native fonts.
*/
export function DO_NOT_USE() {
return useFonts({
InterVariable: require('../../assets/fonts/inter/InterVariable.ttf'),
'InterVariable-Italic': require('../../assets/fonts/inter/InterVariable-Italic.ttf'),
})
}
+5 -8
View File
@@ -1,6 +1,6 @@
import {Platform} from 'react-native'
import {isAndroid} from '#/platform/detection'
export const TRACKING = Platform.OS === 'android' ? 0.1 : 0
export const TRACKING = isAndroid ? 0.1 : 0
export const color = {
temp_purple: 'rgb(105 0 255)',
@@ -51,12 +51,9 @@ export const borderRadius = {
* These correspond to Inter font files we actually load.
*/
export const fontWeight = {
regular: '400',
// medium: '500',
semibold: '600',
// bold: '700',
extrabold: '800',
// black: '900',
normal: '400',
bold: isAndroid ? '700' : '600',
heavy: isAndroid ? '900' : '800',
} as const
export const gradients = {
+2 -4
View File
@@ -1,9 +1,8 @@
import React from 'react'
import {ColorSchemeName, useColorScheme} from 'react-native'
import * as SystemUI from 'expo-system-ui'
import {isWeb} from 'platform/detection'
import {useThemePrefs} from 'state/shell'
import {isWeb} from '#/platform/detection'
import {useThemePrefs} from '#/state/shell'
import {dark, dim, light} from '#/alf/themes'
import {ThemeName} from '#/alf/types'
@@ -12,7 +11,6 @@ export function useColorModeTheme(): ThemeName {
React.useLayoutEffect(() => {
updateDocument(theme)
SystemUI.setBackgroundColorAsync(getBackgroundColor(theme))
}, [theme])
return theme
+10 -7
View File
@@ -14,7 +14,7 @@ import {
} from 'react-native'
import {LinearGradient} from 'expo-linear-gradient'
import {atoms as a, flatten, select, tokens, useTheme, web} from '#/alf'
import {atoms as a, flatten, select, tokens, useTheme} from '#/alf'
import {Props as SVGIconProps} from '#/components/icons/common'
import {Text} from '#/components/Typography'
@@ -87,6 +87,7 @@ export type ButtonProps = Pick<
style?: StyleProp<ViewStyle>
hoverStyle?: StyleProp<ViewStyle>
children: NonTextElements | ((context: ButtonContext) => NonTextElements)
PressableComponent?: React.ComponentType<PressableProps>
}
export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean}
@@ -114,6 +115,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
disabled = false,
style,
hoverStyle: hoverStyleProp,
PressableComponent = Pressable,
...rest
},
ref,
@@ -352,7 +354,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
})
} else if (size === 'small') {
baseStyles.push({
paddingVertical: 8,
paddingVertical: 9,
paddingHorizontal: 12,
borderRadius: 6,
gap: 6,
@@ -374,7 +376,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
}
} else if (size === 'small') {
if (shape === 'round') {
baseStyles.push({height: 36, width: 36})
baseStyles.push({height: 34, width: 34})
} else {
baseStyles.push({height: 34, width: 34})
}
@@ -449,10 +451,11 @@ export const Button = React.forwardRef<View, ButtonProps>(
const flattenedBaseStyles = flatten([baseStyles, style])
return (
<Pressable
<PressableComponent
role="button"
accessibilityHint={undefined} // optional
{...rest}
// @ts-ignore - this will always be a pressable
ref={ref}
aria-label={label}
aria-pressed={state.pressed}
@@ -500,7 +503,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
<Context.Provider value={context}>
{typeof children === 'function' ? children(context) : children}
</Context.Provider>
</Pressable>
</PressableComponent>
)
},
)
@@ -627,9 +630,9 @@ export function useSharedButtonTextStyles() {
}
if (size === 'large') {
baseStyles.push(a.text_md, a.leading_tight, web({top: -0.4}))
baseStyles.push(a.text_md, a.leading_tight)
} else if (size === 'small') {
baseStyles.push(a.text_sm, a.leading_tight, web({top: -0.4}))
baseStyles.push(a.text_sm, a.leading_tight)
} else if (size === 'tiny') {
baseStyles.push(a.text_xs, a.leading_tight)
}
+5
View File
@@ -6,9 +6,14 @@ import {
DialogControlRefProps,
DialogOuterProps,
} from '#/components/Dialog/types'
import {BottomSheetSnapPoint} from '../../../modules/bottom-sheet/src/BottomSheet.types'
export const Context = React.createContext<DialogContextProps>({
close: () => {},
isNativeDialog: false,
nativeSnapPoint: BottomSheetSnapPoint.Hidden,
disableDrag: false,
setDisableDrag: () => {},
})
export function useDialogContext() {
+170 -207
View File
@@ -1,86 +1,48 @@
import React, {useImperativeHandle} from 'react'
import {
Dimensions,
Keyboard,
NativeScrollEvent,
NativeSyntheticEvent,
Pressable,
ScrollView,
StyleProp,
TextInput,
View,
ViewStyle,
} from 'react-native'
import Animated, {useAnimatedStyle} from 'react-native-reanimated'
import {
KeyboardAwareScrollView,
useKeyboardHandler,
} from 'react-native-keyboard-controller'
import {runOnJS} from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import BottomSheet, {
BottomSheetBackdropProps,
BottomSheetFlatList,
BottomSheetFlatListMethods,
BottomSheetScrollView,
BottomSheetScrollViewMethods,
BottomSheetTextInput,
BottomSheetView,
useBottomSheet,
WINDOW_HEIGHT,
} from '@discord/bottom-sheet/src'
import {BottomSheetFlatListProps} from '@discord/bottom-sheet/src/components/bottomSheetScrollable/types'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logger} from '#/logger'
import {isAndroid, isIOS} from '#/platform/detection'
import {useA11y} from '#/state/a11y'
import {useDialogStateControlContext} from '#/state/dialogs'
import {atoms as a, flatten, useTheme} from '#/alf'
import {Context} from '#/components/Dialog/context'
import {List, ListMethods, ListProps} from '#/view/com/util/List'
import {atoms as a, useTheme} from '#/alf'
import {Context, useDialogContext} from '#/components/Dialog/context'
import {
DialogControlProps,
DialogInnerProps,
DialogOuterProps,
} from '#/components/Dialog/types'
import {createInput} from '#/components/forms/TextField'
import {FullWindowOverlay} from '#/components/FullWindowOverlay'
import {Portal} from '#/components/Portal'
import {BottomSheet, BottomSheetSnapPoint} from '../../../modules/bottom-sheet'
import {
BottomSheetSnapPointChangeEvent,
BottomSheetStateChangeEvent,
} from '../../../modules/bottom-sheet/src/BottomSheet.types'
import {BottomSheetNativeComponent} from '../../../modules/bottom-sheet/src/BottomSheetNativeComponent'
export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
export * from '#/components/Dialog/types'
export * from '#/components/Dialog/utils'
// @ts-ignore
export const Input = createInput(BottomSheetTextInput)
function Backdrop(props: BottomSheetBackdropProps) {
const t = useTheme()
const bottomSheet = useBottomSheet()
const animatedStyle = useAnimatedStyle(() => {
const opacity =
(Math.abs(WINDOW_HEIGHT - props.animatedPosition.value) - 50) / 1000
return {
opacity: Math.min(Math.max(opacity, 0), 0.55),
}
})
const onPress = React.useCallback(() => {
bottomSheet.close()
}, [bottomSheet])
return (
<Animated.View
style={[
t.atoms.bg_contrast_300,
{
top: 0,
left: 0,
right: 0,
bottom: 0,
position: 'absolute',
},
animatedStyle,
]}>
<Pressable
accessibilityRole="button"
accessibilityLabel="Dialog backdrop"
accessibilityHint="Press the backdrop to close the dialog"
style={{flex: 1}}
onPress={onPress}
/>
</Animated.View>
)
}
export const Input = createInput(TextInput)
export function Outer({
children,
@@ -90,22 +52,19 @@ export function Outer({
testID,
}: React.PropsWithChildren<DialogOuterProps>) {
const t = useTheme()
const sheet = React.useRef<BottomSheet>(null)
const sheetOptions = nativeOptions?.sheet || {}
const hasSnapPoints = !!sheetOptions.snapPoints
const insets = useSafeAreaInsets()
const ref = React.useRef<BottomSheetNativeComponent>(null)
const closeCallbacks = React.useRef<(() => void)[]>([])
const {setDialogIsOpen} = useDialogStateControlContext()
const {setDialogIsOpen, setFullyExpandedCount} =
useDialogStateControlContext()
/*
* Used to manage open/closed, but index is otherwise handled internally by `BottomSheet`
*/
const [openIndex, setOpenIndex] = React.useState(-1)
const prevSnapPoint = React.useRef<BottomSheetSnapPoint>(
BottomSheetSnapPoint.Hidden,
)
/*
* `openIndex` is the index of the snap point to open the bottom sheet to. If >0, the bottom sheet is open.
*/
const isOpen = openIndex > -1
const [disableDrag, setDisableDrag] = React.useState(false)
const [snapPoint, setSnapPoint] = React.useState<BottomSheetSnapPoint>(
BottomSheetSnapPoint.Partial,
)
const callQueuedCallbacks = React.useCallback(() => {
for (const cb of closeCallbacks.current) {
@@ -119,25 +78,19 @@ export function Outer({
closeCallbacks.current = []
}, [])
const open = React.useCallback<DialogControlProps['open']>(
({index} = {}) => {
// Run any leftover callbacks that might have been queued up before calling `.open()`
callQueuedCallbacks()
setDialogIsOpen(control.id, true)
// can be set to any index of `snapPoints`, but `0` is the first i.e. "open"
setOpenIndex(index || 0)
sheet.current?.snapToIndex(index || 0)
},
[setDialogIsOpen, control.id, callQueuedCallbacks],
)
const open = React.useCallback<DialogControlProps['open']>(() => {
// Run any leftover callbacks that might have been queued up before calling `.open()`
callQueuedCallbacks()
setDialogIsOpen(control.id, true)
ref.current?.present()
}, [setDialogIsOpen, control.id, callQueuedCallbacks])
// This is the function that we call when we want to dismiss the dialog.
const close = React.useCallback<DialogControlProps['close']>(cb => {
if (typeof cb === 'function') {
closeCallbacks.current.push(cb)
}
sheet.current?.close()
ref.current?.dismiss()
}, [])
// This is the actual thing we are doing once we "confirm" the dialog. We want the dialog's close animation to
@@ -146,12 +99,39 @@ export function Outer({
// This removes the dialog from our list of stored dialogs. Not super necessary on iOS, but on Android this
// tells us that we need to toggle the accessibility overlay setting
setDialogIsOpen(control.id, false)
setOpenIndex(-1)
callQueuedCallbacks()
onClose?.()
}, [callQueuedCallbacks, control.id, onClose, setDialogIsOpen])
const onSnapPointChange = (e: BottomSheetSnapPointChangeEvent) => {
const {snapPoint} = e.nativeEvent
setSnapPoint(snapPoint)
if (
snapPoint === BottomSheetSnapPoint.Full &&
prevSnapPoint.current !== BottomSheetSnapPoint.Full
) {
setFullyExpandedCount(c => c + 1)
} else if (
snapPoint !== BottomSheetSnapPoint.Full &&
prevSnapPoint.current === BottomSheetSnapPoint.Full
) {
setFullyExpandedCount(c => c - 1)
}
prevSnapPoint.current = snapPoint
}
const onStateChange = (e: BottomSheetStateChangeEvent) => {
if (e.nativeEvent.state === 'closed') {
onCloseAnimationComplete()
if (prevSnapPoint.current === BottomSheetSnapPoint.Full) {
setFullyExpandedCount(c => c - 1)
}
prevSnapPoint.current = BottomSheetSnapPoint.Hidden
}
}
useImperativeHandle(
control.ref,
() => ({
@@ -161,159 +141,142 @@ export function Outer({
[open, close],
)
React.useEffect(() => {
return () => {
setDialogIsOpen(control.id, false)
}
}, [control.id, setDialogIsOpen])
const context = React.useMemo(() => ({close}), [close])
const context = React.useMemo(
() => ({
close,
isNativeDialog: true,
nativeSnapPoint: snapPoint,
disableDrag,
setDisableDrag,
}),
[close, snapPoint, disableDrag, setDisableDrag],
)
return (
isOpen && (
<Portal>
<FullWindowOverlay>
<View
// iOS
accessibilityViewIsModal
// Android
importantForAccessibility="yes"
style={[a.absolute, a.inset_0]}
testID={testID}
onTouchMove={() => Keyboard.dismiss()}>
<BottomSheet
enableDynamicSizing={!hasSnapPoints}
enablePanDownToClose
keyboardBehavior="interactive"
android_keyboardInputMode="adjustResize"
keyboardBlurBehavior="restore"
topInset={insets.top}
{...sheetOptions}
snapPoints={sheetOptions.snapPoints || ['100%']}
ref={sheet}
index={openIndex}
backgroundStyle={{backgroundColor: 'transparent'}}
backdropComponent={Backdrop}
handleIndicatorStyle={{backgroundColor: t.palette.primary_500}}
handleStyle={{display: 'none'}}
onClose={onCloseAnimationComplete}>
<Context.Provider value={context}>
<View
style={[
a.absolute,
a.inset_0,
t.atoms.bg,
{
borderTopLeftRadius: 40,
borderTopRightRadius: 40,
height: Dimensions.get('window').height * 2,
},
]}
/>
{children}
</Context.Provider>
</BottomSheet>
</View>
</FullWindowOverlay>
</Portal>
)
<BottomSheet
ref={ref}
cornerRadius={20}
backgroundColor={t.atoms.bg.backgroundColor}
{...nativeOptions}
onSnapPointChange={onSnapPointChange}
onStateChange={onStateChange}
disableDrag={disableDrag}>
<Context.Provider value={context}>
<View testID={testID}>{children}</View>
</Context.Provider>
</BottomSheet>
)
}
export function Inner({children, style}: DialogInnerProps) {
const insets = useSafeAreaInsets()
return (
<BottomSheetView
<View
style={[
a.py_xl,
a.pt_2xl,
a.px_xl,
{
paddingTop: 40,
borderTopLeftRadius: 40,
borderTopRightRadius: 40,
paddingBottom: insets.bottom + a.pb_5xl.paddingBottom,
paddingBottom: insets.bottom + insets.top,
},
flatten(style),
style,
]}>
{children}
</BottomSheetView>
</View>
)
}
export const ScrollableInner = React.forwardRef<
BottomSheetScrollViewMethods,
DialogInnerProps
>(function ScrollableInner({children, style}, ref) {
const insets = useSafeAreaInsets()
return (
<BottomSheetScrollView
keyboardShouldPersistTaps="handled"
style={[
a.flex_1, // main diff is this
a.p_xl,
a.h_full,
{
paddingTop: 40,
borderTopLeftRadius: 40,
borderTopRightRadius: 40,
},
style,
]}
contentContainerStyle={a.pb_4xl}
ref={ref}>
{children}
<View style={{height: insets.bottom + a.pt_5xl.paddingTop}} />
</BottomSheetScrollView>
)
})
export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
function ScrollableInner({children, style, ...props}, ref) {
const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext()
const insets = useSafeAreaInsets()
const [keyboardHeight, setKeyboardHeight] = React.useState(0)
useKeyboardHandler({
onEnd: e => {
'worklet'
runOnJS(setKeyboardHeight)(e.height)
},
})
const basePading =
(isIOS ? 30 : 50) + (isIOS ? keyboardHeight / 4 : keyboardHeight)
const fullPaddingBase = insets.bottom + insets.top + basePading
const fullPadding = isIOS ? fullPaddingBase : fullPaddingBase + 50
const paddingBottom =
nativeSnapPoint === BottomSheetSnapPoint.Full ? fullPadding : basePading
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
const {contentOffset} = e.nativeEvent
if (contentOffset.y > 0 && !disableDrag) {
setDisableDrag(true)
} else if (contentOffset.y <= 1 && disableDrag) {
setDisableDrag(false)
}
}
return (
<KeyboardAwareScrollView
style={[style]}
contentContainerStyle={[a.pt_2xl, a.px_xl, {paddingBottom}]}
ref={ref}
{...props}
bounces={nativeSnapPoint === BottomSheetSnapPoint.Full}
bottomOffset={30}
scrollEventThrottle={50}
onScroll={isAndroid ? onScroll : undefined}>
{children}
</KeyboardAwareScrollView>
)
},
)
export const InnerFlatList = React.forwardRef<
BottomSheetFlatListMethods,
BottomSheetFlatListProps<any> & {webInnerStyle?: StyleProp<ViewStyle>}
>(function InnerFlatList({style, contentContainerStyle, ...props}, ref) {
ListMethods,
ListProps<any> & {webInnerStyle?: StyleProp<ViewStyle>}
>(function InnerFlatList({style, ...props}, ref) {
const insets = useSafeAreaInsets()
const {nativeSnapPoint} = useDialogContext()
return (
<BottomSheetFlatList
<List
keyboardShouldPersistTaps="handled"
contentContainerStyle={[a.pb_4xl, flatten(contentContainerStyle)]}
bounces={nativeSnapPoint === BottomSheetSnapPoint.Full}
ListFooterComponent={
<View style={{height: insets.bottom + a.pt_5xl.paddingTop}} />
}
ref={ref}
{...props}
style={[
a.flex_1,
a.p_xl,
a.pt_0,
a.h_full,
{
marginTop: 40,
},
flatten(style),
]}
style={[style]}
/>
)
})
export function Handle() {
const t = useTheme()
const {_} = useLingui()
const {screenReaderEnabled} = useA11y()
const {close} = useDialogContext()
return (
<View style={[a.absolute, a.w_full, a.align_center, a.z_10, {height: 40}]}>
<View
style={[
a.rounded_sm,
{
top: a.pt_lg.paddingTop,
width: 35,
height: 4,
alignSelf: 'center',
backgroundColor: t.palette.contrast_900,
opacity: 0.5,
},
]}
/>
<View style={[a.absolute, a.w_full, a.align_center, a.z_10, {height: 20}]}>
<Pressable
accessible={screenReaderEnabled}
onPress={() => close()}
accessibilityLabel={_(msg`Dismiss`)}
accessibilityHint={_(msg`Double tap to close the dialog`)}>
<View
style={[
a.rounded_sm,
{
top: 10,
width: 35,
height: 5,
alignSelf: 'center',
backgroundColor: t.palette.contrast_975,
opacity: 0.5,
},
]}
/>
</Pressable>
</View>
)
}
+24 -24
View File
@@ -10,7 +10,9 @@ import {
import Animated, {FadeIn, FadeInDown} from 'react-native-reanimated'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {FocusScope} from '@tamagui/focus-scope'
import {DismissableLayer} from '@radix-ui/react-dismissable-layer'
import {useFocusGuards} from '@radix-ui/react-focus-guards'
import {FocusScope} from '@radix-ui/react-focus-scope'
import {logger} from '#/logger'
import {useDialogStateControlContext} from '#/state/dialogs'
@@ -31,6 +33,7 @@ export * from '#/components/Dialog/utils'
export {Input} from '#/components/forms/TextField'
const stopPropagation = (e: any) => e.stopPropagation()
const preventDefault = (e: any) => e.preventDefault()
export function Outer({
children,
@@ -85,24 +88,13 @@ export function Outer({
[close, open],
)
React.useEffect(() => {
if (!isOpen) return
function handler(e: KeyboardEvent) {
if (e.key === 'Escape') {
e.stopPropagation()
close()
}
}
document.addEventListener('keydown', handler)
return () => document.removeEventListener('keydown', handler)
}, [close, isOpen])
const context = React.useMemo(
() => ({
close,
isNativeDialog: false,
nativeSnapPoint: 0,
disableDrag: false,
setDisableDrag: () => {},
}),
[close],
)
@@ -164,9 +156,11 @@ export function Inner({
accessibilityDescribedBy,
}: DialogInnerProps) {
const t = useTheme()
const {close} = React.useContext(Context)
const {gtMobile} = useBreakpoints()
useFocusGuards()
return (
<FocusScope loop enabled trapped>
<FocusScope loop asChild trapped>
<Animated.View
role="dialog"
aria-role="dialog"
@@ -179,7 +173,7 @@ export function Inner({
onTouchEnd={stopPropagation}
entering={FadeInDown.duration(100)}
// exiting={FadeOut.duration(100)}
style={[
style={flatten([
a.relative,
a.rounded_md,
a.w_full,
@@ -194,8 +188,14 @@ export function Inner({
shadowRadius: 30,
},
flatten(style),
]}>
{children}
])}>
<DismissableLayer
onInteractOutside={preventDefault}
onFocusOutside={preventDefault}
onDismiss={close}
style={{display: 'flex', flexDirection: 'column'}}>
{children}
</DismissableLayer>
</Animated.View>
</FocusScope>
)
@@ -229,10 +229,6 @@ export const InnerFlatList = React.forwardRef<
)
})
export function Handle() {
return null
}
export function Close() {
const {_} = useLingui()
const {close} = React.useContext(Context)
@@ -258,3 +254,7 @@ export function Close() {
</View>
)
}
export function Handle() {
return null
}
+20
View File
@@ -0,0 +1,20 @@
import {useCallback} from 'react'
import {useDialogStateControlContext} from '#/state/dialogs'
/**
* If we're calling a system API like the image picker that opens a sheet
* wrap it in this function to make sure the status bar is the correct color.
*/
export function useSheetWrapper() {
const {setFullyExpandedCount} = useDialogStateControlContext()
return useCallback(
async <T>(promise: Promise<T>): Promise<T> => {
setFullyExpandedCount(c => c + 1)
const res = await promise
setFullyExpandedCount(c => c - 1)
return res
},
[setFullyExpandedCount],
)
}
+7 -4
View File
@@ -4,9 +4,10 @@ import type {
GestureResponderEvent,
ScrollViewProps,
} from 'react-native'
import {BottomSheetProps} from '@discord/bottom-sheet/src'
import {ViewStyleProp} from '#/alf'
import {BottomSheetViewProps} from '../../../modules/bottom-sheet'
import {BottomSheetSnapPoint} from '../../../modules/bottom-sheet/src/BottomSheet.types'
type A11yProps = Required<AccessibilityProps>
@@ -37,6 +38,10 @@ export type DialogControlProps = DialogControlRefProps & {
export type DialogContextProps = {
close: DialogControlProps['close']
isNativeDialog: boolean
nativeSnapPoint: BottomSheetSnapPoint
disableDrag: boolean
setDisableDrag: React.Dispatch<React.SetStateAction<boolean>>
}
export type DialogControlOpenOptions = {
@@ -52,9 +57,7 @@ export type DialogControlOpenOptions = {
export type DialogOuterProps = {
control: DialogControlProps
onClose?: () => void
nativeOptions?: {
sheet?: Omit<BottomSheetProps, 'children'>
}
nativeOptions?: Omit<BottomSheetViewProps, 'children'>
webOptions?: {}
testID?: string
}
@@ -1,31 +0,0 @@
import React from 'react'
import {useKeyboardHandler} from 'react-native-keyboard-controller'
import Animated, {
useAnimatedStyle,
useSharedValue,
} from 'react-native-reanimated'
export function KeyboardControllerPadding({maxHeight}: {maxHeight?: number}) {
const keyboardHeight = useSharedValue(0)
useKeyboardHandler(
{
onMove: e => {
'worklet'
if (maxHeight && e.height > maxHeight) {
keyboardHeight.value = maxHeight
} else {
keyboardHeight.value = e.height
}
},
},
[maxHeight],
)
const animatedStyle = useAnimatedStyle(() => ({
height: keyboardHeight.value,
}))
return <Animated.View style={animatedStyle} />
}
@@ -1,7 +0,0 @@
export function KeyboardControllerPadding({
maxHeight: _,
}: {
maxHeight?: number
}) {
return null
}
+8 -10
View File
@@ -1,20 +1,19 @@
import React, {useMemo, useCallback} from 'react'
import React, {useCallback, useMemo} from 'react'
import {ActivityIndicator, FlatList, View} from 'react-native'
import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
import {useLikedByQuery} from '#/state/queries/post-liked-by'
import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {atoms as a, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
import * as Dialog from '#/components/Dialog'
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
import {useLikedByQuery} from '#/state/queries/post-liked-by'
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
import {atoms as a, useTheme} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
interface LikesDialogProps {
control: Dialog.DialogOuterProps['control']
@@ -25,7 +24,6 @@ export function LikesDialog(props: LikesDialogProps) {
return (
<Dialog.Outer control={props.control}>
<Dialog.Handle />
<LikesDialogInner {...props} />
</Dialog.Outer>
)
+3 -3
View File
@@ -103,17 +103,17 @@ export function useLink({
linkRequiresWarning(href, displayText),
)
if (requiresWarning) {
if (isWeb) {
e.preventDefault()
}
if (requiresWarning) {
openModal({
name: 'link-warning',
text: displayText,
href: href,
})
} else {
e.preventDefault()
if (isExternal) {
openLink(href)
} else {
+10 -10
View File
@@ -4,7 +4,7 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import flattenReactChildren from 'react-keyed-flatten-children'
import {isNative} from 'platform/detection'
import {isNative} from '#/platform/detection'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
@@ -82,19 +82,20 @@ export function Outer({
style?: StyleProp<ViewStyle>
}>) {
const context = React.useContext(Context)
const {_} = useLingui()
return (
<Dialog.Outer control={context.control}>
<Dialog.Outer
control={context.control}
nativeOptions={{preventExpansion: true}}>
<Dialog.Handle />
{/* Re-wrap with context since Dialogs are portal-ed to root */}
<Context.Provider value={context}>
<Dialog.ScrollableInner label="Menu TODO">
<Dialog.ScrollableInner label={_(msg`Menu`)} style={[a.py_sm]}>
<View style={[a.gap_lg]}>
{children}
{isNative && showCancel && <Cancel />}
</View>
<View style={{height: a.gap_lg.gap}} />
</Dialog.ScrollableInner>
</Context.Provider>
</Dialog.Outer>
@@ -116,15 +117,14 @@ export function Item({children, label, style, onPress, ...rest}: ItemProps) {
{...rest}
accessibilityHint=""
accessibilityLabel={label}
onPress={e => {
onPress(e)
onFocus={onFocus}
onBlur={onBlur}
onPress={async e => {
await onPress(e)
if (!e.defaultPrevented) {
control?.close()
}
}}
onFocus={onFocus}
onBlur={onBlur}
onPressIn={e => {
onPressIn()
rest.onPressIn?.(e)
+3 -3
View File
@@ -5,12 +5,12 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {differenceInSeconds} from 'date-fns'
import {HITSLOP_10} from '#/lib/constants'
import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {isNative} from '#/platform/detection'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {HITSLOP_10} from 'lib/constants'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {useSession} from 'state/session'
import {useSession} from '#/state/session'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
+11 -6
View File
@@ -4,9 +4,10 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonColor, ButtonProps, ButtonText} from '#/components/Button'
import {Button, ButtonColor, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {Text} from '#/components/Typography'
import {BottomSheetViewProps} from '../../modules/bottom-sheet'
export {
type DialogControlProps as PromptControlProps,
@@ -25,9 +26,11 @@ export function Outer({
children,
control,
testID,
nativeOptions,
}: React.PropsWithChildren<{
control: Dialog.DialogControlProps
testID?: string
nativeOptions?: Omit<BottomSheetViewProps, 'children'>
}>) {
const {gtMobile} = useBreakpoints()
const titleId = React.useId()
@@ -39,10 +42,12 @@ export function Outer({
)
return (
<Dialog.Outer control={control} testID={testID}>
<Dialog.Outer
control={control}
testID={testID}
nativeOptions={{preventExpansion: true, ...nativeOptions}}>
<Dialog.Handle />
<Context.Provider value={context}>
<Dialog.Handle />
<Dialog.ScrollableInner
accessibilityLabelledBy={titleId}
accessibilityDescribedBy={descriptionId}
@@ -141,7 +146,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: ButtonProps['onPress']
onPress: (e: GestureResponderEvent) => void
color?: ButtonColor
/**
* Optional i18n string. If undefined, it will default to "Confirm".
@@ -194,7 +199,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: ButtonProps['onPress']
onConfirm: (e: GestureResponderEvent) => void
confirmButtonColor?: ButtonColor
showCancel?: boolean
}>) {
@@ -4,7 +4,6 @@ import {AppBskyLabelerDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
export {useDialogControl as useReportDialogControl} from '#/components/Dialog'
import {getLabelingServiceTitle} from '#/lib/moderation'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, useButtonContext} from '#/components/Button'
@@ -6,6 +6,7 @@ import {useLingui} from '@lingui/react'
import {getLabelingServiceTitle} from '#/lib/moderation'
import {ReportOption} from '#/lib/moderation/useReportOptions'
import {isAndroid} from '#/platform/detection'
import {useAgent} from '#/state/session'
import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
import * as Toast from '#/view/com/util/Toast'
@@ -225,6 +226,8 @@ export function SubmitView({
{submitting && <ButtonIcon icon={Loader} />}
</Button>
</View>
{/* Maybe fix this later -h */}
{isAndroid ? <View style={{height: 300}} /> : null}
</View>
)
}
+3 -8
View File
@@ -1,5 +1,6 @@
import React from 'react'
import {Pressable, View} from 'react-native'
import {ScrollView} from 'react-native-gesture-handler'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -8,12 +9,10 @@ import {useMyLabelersQuery} from '#/state/queries/preferences'
export {useDialogControl as useReportDialogControl} from '#/components/Dialog'
import {AppBskyLabelerDefs} from '@atproto/api'
import {BottomSheetScrollViewMethods} from '@discord/bottom-sheet/src'
import {atoms as a} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {useDelayedLoading} from '#/components/hooks/useDelayedLoading'
import {useOnKeyboardDidShow} from '#/components/hooks/useOnKeyboard'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import {SelectLabelerView} from './SelectLabelerView'
@@ -25,7 +24,6 @@ export function ReportDialog(props: ReportDialogProps) {
return (
<Dialog.Outer control={props.control}>
<Dialog.Handle />
<ReportDialogInner {...props} />
</Dialog.Outer>
)
@@ -37,13 +35,10 @@ function ReportDialogInner(props: ReportDialogProps) {
isLoading: isLabelerLoading,
data: labelers,
error,
} = useMyLabelersQuery()
} = useMyLabelersQuery({excludeNonConfigurableLabelers: true})
const isLoading = useDelayedLoading(500, isLabelerLoading)
const ref = React.useRef<BottomSheetScrollViewMethods>(null)
useOnKeyboardDidShow(() => {
ref.current?.scrollToEnd({animated: true})
})
const ref = React.useRef<ScrollView>(null)
return (
<Dialog.ScrollableInner label={_(msg`Report dialog`)} ref={ref}>
@@ -149,7 +149,6 @@ export function QrCodeDialog({
return (
<Dialog.Outer control={control}>
<Dialog.Handle />
<Dialog.ScrollableInner
label={_(msg`Create a QR code for a starter pack`)}>
<View style={[a.flex_1, a.align_center, a.gap_5xl]}>
+8 -8
View File
@@ -6,14 +6,14 @@ import {AppBskyGraphDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {saveImageToMediaLibrary} from '#/lib/media/manip'
import {shareUrl} from '#/lib/sharing'
import {logEvent} from '#/lib/statsig/statsig'
import {getStarterPackOgCard} from '#/lib/strings/starter-pack'
import {logger} from '#/logger'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {saveImageToMediaLibrary} from 'lib/media/manip'
import {shareUrl} from 'lib/sharing'
import {logEvent} from 'lib/statsig/statsig'
import {getStarterPackOgCard} from 'lib/strings/starter-pack'
import {isNative, isWeb} from 'platform/detection'
import * as Toast from 'view/com/util/Toast'
import {isNative, isWeb} from '#/platform/detection'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {DialogControlProps} from '#/components/Dialog'
@@ -32,6 +32,7 @@ interface Props {
export function ShareDialog(props: Props) {
return (
<Dialog.Outer control={props.control}>
<Dialog.Handle />
<ShareDialogInner {...props} />
</Dialog.Outer>
)
@@ -84,7 +85,6 @@ function ShareDialogInner({
return (
<>
<Dialog.Handle />
<Dialog.ScrollableInner label={_(msg`Share link dialog`)}>
{!imageLoaded || !link ? (
<View style={[a.p_xl, a.align_center]}>
@@ -3,13 +3,13 @@ import type {ListRenderItemInfo} from 'react-native'
import {View} from 'react-native'
import {AppBskyActorDefs, ModerationOpts} from '@atproto/api'
import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
import {BottomSheetFlatListMethods} from '@discord/bottom-sheet'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
import {isWeb} from 'platform/detection'
import {useSession} from 'state/session'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {isWeb} from '#/platform/detection'
import {useSession} from '#/state/session'
import {ListMethods} from '#/view/com/util/List'
import {WizardAction, WizardState} from '#/screens/StarterPack/Wizard/State'
import {atoms as a, native, useTheme, web} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
@@ -45,7 +45,7 @@ export function WizardEditListDialog({
const {currentAccount} = useSession()
const initialNumToRender = useInitialNumToRender()
const listRef = useRef<BottomSheetFlatListMethods>(null)
const listRef = useRef<ListMethods>(null)
const getData = () => {
if (state.currentStep === 'Feeds') return state.feeds
@@ -76,10 +76,7 @@ export function WizardEditListDialog({
)
return (
<Dialog.Outer
control={control}
testID="newChatDialog"
nativeOptions={{sheet: {snapPoints: ['95%']}}}>
<Dialog.Outer control={control} testID="newChatDialog">
<Dialog.Handle />
<Dialog.InnerFlatList
ref={listRef}
@@ -89,6 +86,7 @@ export function WizardEditListDialog({
ListHeaderComponent={
<View
style={[
native(a.pt_4xl),
a.flex_row,
a.justify_between,
a.border_b,
@@ -103,13 +101,7 @@ export function WizardEditListDialog({
height: 48,
},
]
: [
a.pb_sm,
a.align_end,
{
height: 68,
},
],
: [a.pb_sm, a.align_end],
]}>
<View style={{width: 60}} />
<Text style={[a.font_bold, a.text_xl]}>
@@ -143,8 +135,6 @@ export function WizardEditListDialog({
paddingHorizontal: 0,
marginTop: 0,
paddingTop: 0,
borderTopLeftRadius: 40,
borderTopRightRadius: 40,
}),
]}
webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]}
-1
View File
@@ -85,7 +85,6 @@ export function TagMenu({
<Dialog.Outer control={control}>
<Dialog.Handle />
<Dialog.Inner label={_(msg`Tag menu: ${displayTag}`)}>
{isPreferencesLoading ? (
<View style={[a.w_full, a.align_center]}>
+19 -12
View File
@@ -53,11 +53,14 @@ export function childIsString(
)
}
export function renderChildrenWithEmoji(children: StringChild) {
export function renderChildrenWithEmoji(
children: StringChild,
props: Omit<TextProps, 'children'> = {},
) {
const normalized = Array.isArray(children) ? children : [children]
return (
<UITextView>
<UITextView {...props}>
{normalized.map(child => {
if (typeof child !== 'string') return child
@@ -68,10 +71,12 @@ export function renderChildrenWithEmoji(children: StringChild) {
}
return child.split(EMOJI).map((stringPart, index) => (
<UITextView key={index}>
<UITextView key={index} {...props}>
{stringPart}
{emojis[index] ? (
<UITextView style={{color: 'black', fontFamily: 'System'}}>
<UITextView
{...props}
style={[props?.style, {color: 'black', fontFamily: 'System'}]}>
{emojis[index]}
</UITextView>
) : null}
@@ -163,15 +168,17 @@ export function Text({
}
}
const shared = {
uiTextView: true,
selectable,
style: s,
dataSet: Object.assign({tooltip: title}, dataSet || {}),
...rest,
}
return (
<UITextView
selectable={selectable}
uiTextView
style={s}
{...rest}
// @ts-ignore
dataSet={Object.assign({tooltip: title}, dataSet || {})}>
{isIOS && emoji ? renderChildrenWithEmoji(children) : children}
<UITextView {...shared}>
{isIOS && emoji ? renderChildrenWithEmoji(children, shared) : children}
</UITextView>
)
}
@@ -31,7 +31,6 @@ export function BirthDateSettingsDialog({
return (
<Dialog.Outer control={control}>
<Dialog.Handle />
<Dialog.ScrollableInner label={_(msg`My Birthday`)}>
<View style={[a.gap_sm, a.pb_lg]}>
<Text style={[a.text_2xl, a.font_bold]}>
-1
View File
@@ -50,7 +50,6 @@ export function EmbedConsentDialog({
return (
<Dialog.Outer control={control}>
<Dialog.Handle />
<Dialog.ScrollableInner
label={_(msg`External Media`)}
style={[gtMobile ? {width: 'auto', maxWidth: 400} : a.w_full]}>
-255
View File
@@ -1,255 +0,0 @@
import React, {
useCallback,
useImperativeHandle,
useMemo,
useRef,
useState,
} from 'react'
import {Modal, ScrollView, TextInput, View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {cleanError} from '#/lib/strings/errors'
import {
Gif,
useFeaturedGifsQuery,
useGifSearchQuery,
} from '#/state/queries/tenor'
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
import {FlatList_INTERNAL} from '#/view/com/util/Views'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import * as TextField from '#/components/forms/TextField'
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
import {Button, ButtonText} from '../Button'
import {Handle} from '../Dialog'
import {useThrottledValue} from '../hooks/useThrottledValue'
import {ListFooter, ListMaybePlaceholder} from '../Lists'
import {GifPreview} from './GifSelect.shared'
export function GifSelectDialog({
controlRef,
onClose,
onSelectGif: onSelectGifProp,
}: {
controlRef: React.RefObject<{open: () => void}>
onClose: () => void
onSelectGif: (gif: Gif) => void
}) {
const t = useTheme()
const [open, setOpen] = useState(false)
useImperativeHandle(controlRef, () => ({
open: () => setOpen(true),
}))
const close = useCallback(() => {
setOpen(false)
onClose()
}, [onClose])
const onSelectGif = useCallback(
(gif: Gif) => {
onSelectGifProp(gif)
close()
},
[onSelectGifProp, close],
)
const renderErrorBoundary = useCallback(
(error: any) => <ModalError details={String(error)} close={close} />,
[close],
)
return (
<Modal
visible={open}
animationType="slide"
presentationStyle="formSheet"
onRequestClose={close}
aria-modal
accessibilityViewIsModal>
<View style={[a.flex_1, t.atoms.bg]}>
<Handle />
<ErrorBoundary renderError={renderErrorBoundary}>
<GifList onSelectGif={onSelectGif} close={close} />
</ErrorBoundary>
</View>
</Modal>
)
}
function GifList({
onSelectGif,
}: {
close: () => void
onSelectGif: (gif: Gif) => void
}) {
const {_} = useLingui()
const t = useTheme()
const {gtMobile} = useBreakpoints()
const textInputRef = useRef<TextInput>(null)
const listRef = useRef<FlatList_INTERNAL>(null)
const [undeferredSearch, setSearch] = useState('')
const search = useThrottledValue(undeferredSearch, 500)
const isSearching = search.length > 0
const trendingQuery = useFeaturedGifsQuery()
const searchQuery = useGifSearchQuery(search)
const {
data,
fetchNextPage,
isFetchingNextPage,
hasNextPage,
error,
isLoading,
isError,
refetch,
} = isSearching ? searchQuery : trendingQuery
const flattenedData = useMemo(() => {
return data?.pages.flatMap(page => page.results) || []
}, [data])
const renderItem = useCallback(
({item}: {item: Gif}) => {
return <GifPreview gif={item} onSelectGif={onSelectGif} />
},
[onSelectGif],
)
const onEndReached = React.useCallback(() => {
if (isFetchingNextPage || !hasNextPage || error) return
fetchNextPage()
}, [isFetchingNextPage, hasNextPage, error, fetchNextPage])
const hasData = flattenedData.length > 0
const onGoBack = useCallback(() => {
if (isSearching) {
// clear the input and reset the state
textInputRef.current?.clear()
setSearch('')
} else {
close()
}
}, [isSearching])
const listHeader = useMemo(() => {
return (
<View style={[a.relative, a.mb_lg, a.pt_4xl, a.flex_row, a.align_center]}>
{/* cover top corners */}
<View
style={[
a.absolute,
a.inset_0,
{
borderBottomLeftRadius: 8,
borderBottomRightRadius: 8,
},
t.atoms.bg,
]}
/>
<TextField.Root>
<TextField.Icon icon={Search} />
<TextField.Input
label={_(msg`Search GIFs`)}
placeholder={_(msg`Search Tenor`)}
onChangeText={text => {
setSearch(text)
listRef.current?.scrollToOffset({offset: 0, animated: false})
}}
returnKeyType="search"
clearButtonMode="while-editing"
inputRef={textInputRef}
maxLength={50}
/>
</TextField.Root>
</View>
)
}, [t.atoms.bg, _])
return (
<FlatList_INTERNAL
ref={listRef}
key={gtMobile ? '3 cols' : '2 cols'}
data={flattenedData}
renderItem={renderItem}
numColumns={gtMobile ? 3 : 2}
columnWrapperStyle={a.gap_sm}
contentContainerStyle={a.px_lg}
ListHeaderComponent={
<>
{listHeader}
{!hasData && (
<ListMaybePlaceholder
isLoading={isLoading}
isError={isError}
onRetry={refetch}
onGoBack={onGoBack}
emptyType="results"
sideBorders={false}
topBorder={false}
errorTitle={_(msg`Failed to load GIFs`)}
errorMessage={_(msg`There was an issue connecting to Tenor.`)}
emptyMessage={
isSearching
? _(msg`No search results found for "${search}".`)
: _(
msg`No featured GIFs found. There may be an issue with Tenor.`,
)
}
/>
)}
</>
}
stickyHeaderIndices={[0]}
onEndReached={onEndReached}
onEndReachedThreshold={4}
keyExtractor={(item: Gif) => item.id}
keyboardDismissMode="on-drag"
ListFooterComponent={
hasData ? (
<ListFooter
isFetchingNextPage={isFetchingNextPage}
error={cleanError(error)}
onRetry={fetchNextPage}
style={{borderTopWidth: 0}}
/>
) : null
}
/>
)
}
function ModalError({details, close}: {details?: string; close: () => void}) {
const {_} = useLingui()
return (
<ScrollView
style={[a.flex_1, a.gap_md]}
centerContent
contentContainerStyle={a.px_lg}>
<ErrorScreen
title={_(msg`Oh no!`)}
message={_(
msg`There was an unexpected issue in the application. Please let us know if this happened to you!`,
)}
details={details}
/>
<Button
label={_(msg`Close dialog`)}
onPress={close}
color="primary"
size="large"
variant="solid">
<ButtonText>
<Trans>Close</Trans>
</ButtonText>
</Button>
</ScrollView>
)
}
@@ -1,53 +0,0 @@
import React, {useCallback} from 'react'
import {Image} from 'expo-image'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logEvent} from '#/lib/statsig/statsig'
import {Gif} from '#/state/queries/tenor'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button} from '../Button'
export function GifPreview({
gif,
onSelectGif,
}: {
gif: Gif
onSelectGif: (gif: Gif) => void
}) {
const {gtTablet} = useBreakpoints()
const {_} = useLingui()
const t = useTheme()
const onPress = useCallback(() => {
logEvent('composer:gif:select', {})
onSelectGif(gif)
}, [onSelectGif, gif])
return (
<Button
label={_(msg`Select GIF "${gif.title}"`)}
style={[a.flex_1, gtTablet ? {maxWidth: '33%'} : {maxWidth: '50%'}]}
onPress={onPress}>
{({pressed}) => (
<Image
style={[
a.flex_1,
a.mb_sm,
a.rounded_sm,
{aspectRatio: 1, opacity: pressed ? 0.8 : 1},
t.atoms.bg_contrast_25,
]}
source={{
uri: gif.media_formats.tinygif.url,
}}
contentFit="cover"
accessibilityLabel={gif.title}
accessibilityHint=""
cachePolicy="none"
accessibilityIgnoresInvertColors
/>
)}
</Button>
)
}
+64 -11
View File
@@ -6,10 +6,12 @@ import React, {
useState,
} from 'react'
import {TextInput, View} from 'react-native'
import {BottomSheetFlatListMethods} from '@discord/bottom-sheet'
import {useWindowDimensions} from 'react-native'
import {Image} from 'expo-image'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logEvent} from '#/lib/statsig/statsig'
import {cleanError} from '#/lib/strings/errors'
import {isWeb} from '#/platform/detection'
import {
@@ -19,15 +21,15 @@ import {
} from '#/state/queries/tenor'
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {ListMethods} from '#/view/com/util/List'
import {atoms as a, ios, native, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import * as TextField from '#/components/forms/TextField'
import {useThrottledValue} from '#/components/hooks/useThrottledValue'
import {ArrowLeft_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow'
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
import {Button, ButtonIcon, ButtonText} from '../Button'
import {ListFooter, ListMaybePlaceholder} from '../Lists'
import {GifPreview} from './GifSelect.shared'
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
export function GifSelectDialog({
controlRef,
@@ -59,8 +61,12 @@ export function GifSelectDialog({
return (
<Dialog.Outer
control={control}
nativeOptions={{sheet: {snapPoints: ['100%']}}}
onClose={onClose}>
onClose={onClose}
nativeOptions={{
bottomInset: 0,
// use system corner radius on iOS
...ios({cornerRadius: undefined}),
}}>
<Dialog.Handle />
<ErrorBoundary renderError={renderErrorBoundary}>
<GifList control={control} onSelectGif={onSelectGif} />
@@ -80,9 +86,10 @@ function GifList({
const t = useTheme()
const {gtMobile} = useBreakpoints()
const textInputRef = useRef<TextInput>(null)
const listRef = useRef<BottomSheetFlatListMethods>(null)
const listRef = useRef<ListMethods>(null)
const [undeferredSearch, setSearch] = useState('')
const search = useThrottledValue(undeferredSearch, 500)
const {height} = useWindowDimensions()
const isSearching = search.length > 0
@@ -95,7 +102,7 @@ function GifList({
isFetchingNextPage,
hasNextPage,
error,
isLoading,
isPending,
isError,
refetch,
} = isSearching ? searchQuery : trendingQuery
@@ -132,6 +139,7 @@ function GifList({
return (
<View
style={[
native(a.pt_4xl),
a.relative,
a.mb_lg,
a.flex_row,
@@ -196,13 +204,14 @@ function GifList({
data={flattenedData}
renderItem={renderItem}
numColumns={gtMobile ? 3 : 2}
columnWrapperStyle={a.gap_sm}
columnWrapperStyle={[a.gap_sm]}
contentContainerStyle={[native([a.px_xl, {minHeight: height}])]}
ListHeaderComponent={
<>
{listHeader}
{!hasData && (
<ListMaybePlaceholder
isLoading={isLoading}
isLoading={isPending}
isError={isError}
onRetry={refetch}
onGoBack={onGoBack}
@@ -273,3 +282,47 @@ function DialogError({details}: {details?: string}) {
</Dialog.ScrollableInner>
)
}
export function GifPreview({
gif,
onSelectGif,
}: {
gif: Gif
onSelectGif: (gif: Gif) => void
}) {
const {gtTablet} = useBreakpoints()
const {_} = useLingui()
const t = useTheme()
const onPress = useCallback(() => {
logEvent('composer:gif:select', {})
onSelectGif(gif)
}, [onSelectGif, gif])
return (
<Button
label={_(msg`Select GIF "${gif.title}"`)}
style={[a.flex_1, gtTablet ? {maxWidth: '33%'} : {maxWidth: '50%'}]}
onPress={onPress}>
{({pressed}) => (
<Image
style={[
a.flex_1,
a.mb_sm,
a.rounded_sm,
{aspectRatio: 1, opacity: pressed ? 0.8 : 1},
t.atoms.bg_contrast_25,
]}
source={{
uri: gif.media_formats.tinygif.url,
}}
contentFit="cover"
accessibilityLabel={gif.title}
accessibilityHint=""
cachePolicy="none"
accessibilityIgnoresInvertColors
/>
)}
</Button>
)
}
@@ -204,7 +204,9 @@ export function PostInteractionSettingsDialogControlledInner(
label={_(msg`Edit post interaction settings`)}
style={[{maxWidth: 500}, a.w_full]}>
{isLoading ? (
<Loader size="xl" />
<View style={[a.flex_1, a.py_4xl, a.align_center, a.justify_center]}>
<Loader size="xl" />
</View>
) : (
<PostInteractionSettingsForm
replySettingsDisabled={!isThreadgateOwnedByViewer}
@@ -231,7 +233,6 @@ export function PostInteractionSettingsForm({
}: PostInteractionSettingsFormProps) {
const t = useTheme()
const {_} = useLingui()
const control = Dialog.useDialogContext()
const {data: lists} = useMyListsQuery('curate')
const [quotesEnabled, setQuotesEnabled] = React.useState(
!(
@@ -437,7 +438,6 @@ export function PostInteractionSettingsForm({
<Button
label={_(msg`Save`)}
onPress={onSave}
onAccessibilityEscape={control.close}
color="primary"
size="large"
variant="solid"
-1
View File
@@ -43,7 +43,6 @@ export function SwitchAccountDialog({
return (
<Dialog.Outer control={control}>
<Dialog.Handle />
<Dialog.ScrollableInner label={_(msg`Switch Account`)}>
<View style={[a.gap_lg]}>
<Text style={[a.text_2xl, a.font_bold]}>
@@ -44,7 +44,6 @@ export function NeueTypography() {
return (
<Dialog.Outer control={control} onClose={onClose}>
<Dialog.Handle />
<Dialog.ScrollableInner label={_(msg`Introducing new font settings`)}>
<View style={[a.gap_xl]}>
<View style={[a.gap_md]}>
+1 -1
View File
@@ -41,7 +41,7 @@ const queuedNuxs: {
id: Nux.NeueTypography,
enabled(props) {
if (props.currentProfile.createdAt) {
if (new Date(props.currentProfile.createdAt) < new Date('2024-10-01')) {
if (new Date(props.currentProfile.createdAt) < new Date('2024-10-09')) {
return true
}
}
+3 -3
View File
@@ -136,7 +136,7 @@ let ConvoMenu = ({
<Menu.Outer>
<Menu.Item
label={_(msg`Leave conversation`)}
onPress={leaveConvoControl.open}>
onPress={() => leaveConvoControl.open()}>
<Menu.ItemText>
<Trans>Leave conversation</Trans>
</Menu.ItemText>
@@ -195,7 +195,7 @@ let ConvoMenu = ({
</Menu.Item>
<Menu.Item
label={_(msg`Report conversation`)}
onPress={reportControl.open}>
onPress={() => reportControl.open()}>
<Menu.ItemText>
<Trans>Report conversation</Trans>
</Menu.ItemText>
@@ -206,7 +206,7 @@ let ConvoMenu = ({
<Menu.Group>
<Menu.Item
label={_(msg`Leave conversation`)}
onPress={leaveConvoControl.open}>
onPress={() => leaveConvoControl.open()}>
<Menu.ItemText>
<Trans>Leave conversation</Trans>
</Menu.ItemText>
+80
View File
@@ -0,0 +1,80 @@
import React from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {subDays} from 'date-fns'
import {atoms as a, useTheme} from '#/alf'
import {Text} from '../Typography'
import {localDateString} from './util'
const timeFormatter = new Intl.DateTimeFormat(undefined, {
hour: 'numeric',
minute: 'numeric',
})
const weekdayFormatter = new Intl.DateTimeFormat(undefined, {
weekday: 'long',
})
const longDateFormatter = new Intl.DateTimeFormat(undefined, {
weekday: 'short',
month: 'long',
day: 'numeric',
})
const longDateFormatterWithYear = new Intl.DateTimeFormat(undefined, {
weekday: 'short',
month: 'long',
day: 'numeric',
year: 'numeric',
})
let DateDivider = ({date: dateStr}: {date: string}): React.ReactNode => {
const {_} = useLingui()
const t = useTheme()
let date: string
const time = timeFormatter.format(new Date(dateStr))
const timestamp = new Date(dateStr)
const today = new Date()
const yesterday = subDays(today, 1)
const oneWeekAgo = subDays(today, 7)
if (localDateString(today) === localDateString(timestamp)) {
date = _(msg`Today`)
} else if (localDateString(yesterday) === localDateString(timestamp)) {
date = _(msg`Yesterday`)
} else {
if (timestamp < oneWeekAgo) {
if (timestamp.getFullYear() === today.getFullYear()) {
date = longDateFormatter.format(timestamp)
} else {
date = longDateFormatterWithYear.format(timestamp)
}
} else {
date = weekdayFormatter.format(timestamp)
}
}
return (
<View style={[a.w_full, a.my_lg]}>
<Text
style={[
a.text_xs,
a.text_center,
t.atoms.bg,
t.atoms.text_contrast_medium,
a.px_md,
]}>
<Trans>
<Text style={[a.text_xs, t.atoms.text_contrast_medium, a.font_bold]}>
{date}
</Text>{' '}
at {time}
</Trans>
</Text>
</View>
)
}
DateDivider = React.memo(DateDivider)
export {DateDivider}
+88 -90
View File
@@ -17,13 +17,15 @@ import {useLingui} from '@lingui/react'
import {ConvoItem} from '#/state/messages/convo/types'
import {useSession} from '#/state/session'
import {TimeElapsed} from 'view/com/util/TimeElapsed'
import {TimeElapsed} from '#/view/com/util/TimeElapsed'
import {atoms as a, useTheme} from '#/alf'
import {ActionsWrapper} from '#/components/dms/ActionsWrapper'
import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography'
import {isOnlyEmoji, RichText} from '../RichText'
import {DateDivider} from './DateDivider'
import {MessageItemEmbed} from './MessageItemEmbed'
import {localDateString} from './util'
let MessageItem = ({
item,
@@ -33,14 +35,37 @@ let MessageItem = ({
const t = useTheme()
const {currentAccount} = useSession()
const {message, nextMessage} = item
const {message, nextMessage, prevMessage} = item
const isPending = item.type === 'pending-message'
const isFromSelf = message.sender?.did === currentAccount?.did
const nextIsMessage = ChatBskyConvoDefs.isMessageView(nextMessage)
const isNextFromSelf =
ChatBskyConvoDefs.isMessageView(nextMessage) &&
nextMessage.sender?.did === currentAccount?.did
nextIsMessage && nextMessage.sender?.did === currentAccount?.did
const isNextFromSameSender = isNextFromSelf === isFromSelf
const isNewDay = useMemo(() => {
if (!prevMessage) return true
const thisDate = new Date(message.sentAt)
const prevDate = new Date(prevMessage.sentAt)
return localDateString(thisDate) !== localDateString(prevDate)
}, [message, prevMessage])
const isLastMessageOfDay = useMemo(() => {
if (!nextMessage || !nextIsMessage) return true
const thisDate = new Date(message.sentAt)
const prevDate = new Date(nextMessage.sentAt)
return localDateString(thisDate) !== localDateString(prevDate)
}, [message.sentAt, nextIsMessage, nextMessage])
const needsTail = isLastMessageOfDay || !isNextFromSameSender
const isLastInGroup = useMemo(() => {
// if this message is pending, it means the next message is pending too
@@ -48,24 +73,19 @@ let MessageItem = ({
return false
}
// if the next message is from a different sender, then it's the last in the group
if (isFromSelf ? !isNextFromSelf : isNextFromSelf) {
return true
}
// or, if there's a 3 minute gap between this message and the next
// or, if there's a 5 minute gap between this message and the next
if (ChatBskyConvoDefs.isMessageView(nextMessage)) {
const thisDate = new Date(message.sentAt)
const nextDate = new Date(nextMessage.sentAt)
const diff = nextDate.getTime() - thisDate.getTime()
// 3 minutes
return diff > 3 * 60 * 1000
// 5 minutes
return diff > 5 * 60 * 1000
}
return true
}, [message, nextMessage, isFromSelf, isNextFromSelf, isPending])
}, [message, nextMessage, isPending])
const lastInGroupRef = useRef(isLastInGroup)
if (lastInGroupRef.current !== isLastInGroup) {
@@ -80,52 +100,59 @@ let MessageItem = ({
}, [message.text, message.facets])
return (
<View style={[isFromSelf ? a.mr_md : a.ml_md]}>
<ActionsWrapper isFromSelf={isFromSelf} message={message}>
{AppBskyEmbedRecord.isView(message.embed) && (
<MessageItemEmbed embed={message.embed} />
)}
{rt.text.length > 0 && (
<View
style={
!isOnlyEmoji(message.text) && [
a.py_sm,
a.my_2xs,
a.rounded_md,
{
paddingLeft: 14,
paddingRight: 14,
backgroundColor: isFromSelf
? isPending
? pendingColor
: t.palette.primary_500
: t.palette.contrast_50,
borderRadius: 17,
},
isFromSelf ? a.self_end : a.self_start,
isFromSelf
? {borderBottomRightRadius: isLastInGroup ? 2 : 17}
: {borderBottomLeftRadius: isLastInGroup ? 2 : 17},
]
}>
<RichText
value={rt}
style={[a.text_md, isFromSelf && {color: t.palette.white}]}
interactiveStyle={a.underline}
enableTags
emojiMultiplier={3}
/>
</View>
)}
</ActionsWrapper>
<>
{isNewDay && <DateDivider date={message.sentAt} />}
<View
style={[
isFromSelf ? a.mr_md : a.ml_md,
nextIsMessage && !isNextFromSameSender && a.mb_md,
]}>
<ActionsWrapper isFromSelf={isFromSelf} message={message}>
{AppBskyEmbedRecord.isView(message.embed) && (
<MessageItemEmbed embed={message.embed} />
)}
{rt.text.length > 0 && (
<View
style={
!isOnlyEmoji(message.text) && [
a.py_sm,
a.my_2xs,
a.rounded_md,
{
paddingLeft: 14,
paddingRight: 14,
backgroundColor: isFromSelf
? isPending
? pendingColor
: t.palette.primary_500
: t.palette.contrast_50,
borderRadius: 17,
},
isFromSelf ? a.self_end : a.self_start,
isFromSelf
? {borderBottomRightRadius: needsTail ? 2 : 17}
: {borderBottomLeftRadius: needsTail ? 2 : 17},
]
}>
<RichText
value={rt}
style={[a.text_md, isFromSelf && {color: t.palette.white}]}
interactiveStyle={a.underline}
enableTags
emojiMultiplier={3}
/>
</View>
)}
</ActionsWrapper>
{isLastInGroup && (
<MessageItemMetadata
item={item}
style={isFromSelf ? a.text_right : a.text_left}
/>
)}
</View>
{isLastInGroup && (
<MessageItemMetadata
item={item}
style={isFromSelf ? a.text_right : a.text_left}
/>
)}
</View>
</>
)
}
MessageItem = React.memo(MessageItem)
@@ -165,31 +192,12 @@ let MessageItemMetadata = ({
const diff = now.getTime() - date.getTime()
// if under 1 minute
if (diff < 1000 * 60) {
// if under 30 seconds
if (diff < 1000 * 30) {
return _(msg`Now`)
}
// if in the last day
if (localDateString(now) === localDateString(date)) {
return time
}
// if yesterday
const yesterday = new Date(now)
yesterday.setDate(yesterday.getDate() - 1)
if (localDateString(yesterday) === localDateString(date)) {
return _(msg`Yesterday, ${time}`)
}
return i18n.date(date, {
hour: 'numeric',
minute: 'numeric',
day: 'numeric',
month: 'numeric',
year: 'numeric',
})
return time
},
[_],
)
@@ -242,15 +250,5 @@ let MessageItemMetadata = ({
</Text>
)
}
MessageItemMetadata = React.memo(MessageItemMetadata)
export {MessageItemMetadata}
function localDateString(date: Date) {
// can't use toISOString because it should be in local time
const mm = date.getMonth()
const dd = date.getDate()
const yyyy = date.getFullYear()
// not padding with 0s because it's not necessary, it's just used for comparison
return `${yyyy}-${mm}-${dd}`
}
+5 -5
View File
@@ -7,11 +7,11 @@ import {useLingui} from '@lingui/react'
import {richTextToString} from '#/lib/strings/rich-text-helpers'
import {getTranslatorLink} from '#/locale/helpers'
import {isWeb} from '#/platform/detection'
import {useConvoActive} from '#/state/messages/convo'
import {useLanguagePrefs} from '#/state/preferences'
import {useOpenLink} from '#/state/preferences/in-app-browser'
import {isWeb} from 'platform/detection'
import {useConvoActive} from 'state/messages/convo'
import {useSession} from 'state/session'
import {useSession} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useTheme} from '#/alf'
import {ReportDialog} from '#/components/dms/ReportDialog'
@@ -120,7 +120,7 @@ export let MessageMenu = ({
<Menu.Item
testID="messageDropdownDeleteBtn"
label={_(msg`Delete message for me`)}
onPress={deleteControl.open}>
onPress={() => deleteControl.open()}>
<Menu.ItemText>{_(msg`Delete for me`)}</Menu.ItemText>
<Menu.ItemIcon icon={Trash} position="right" />
</Menu.Item>
@@ -128,7 +128,7 @@ export let MessageMenu = ({
<Menu.Item
testID="messageDropdownReportBtn"
label={_(msg`Report message`)}
onPress={reportControl.open}>
onPress={() => reportControl.open()}>
<Menu.ItemText>{_(msg`Report`)}</Menu.ItemText>
<Menu.ItemIcon icon={Warning} position="right" />
</Menu.Item>
+9 -17
View File
@@ -4,12 +4,13 @@ import {AppBskyActorDefs} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logEvent} from '#/lib/statsig/statsig'
import {useMaybeConvoForUser} from '#/state/queries/messages/get-convo-for-members'
import {logEvent} from 'lib/statsig/statsig'
import {atoms as a, useTheme} from '#/alf'
import {Message_Stroke2_Corner0_Rounded as Message} from '../icons/Message'
import {Link} from '../Link'
import {canBeMessaged} from './util'
import {ButtonIcon} from '#/components/Button'
import {canBeMessaged} from '#/components/dms/util'
import {Message_Stroke2_Corner0_Rounded as Message} from '#/components/icons/Message'
import {Link} from '#/components/Link'
export function MessageProfileButton({
profile,
@@ -40,15 +41,9 @@ export function MessageProfileButton({
a.align_center,
t.atoms.bg_contrast_25,
a.rounded_full,
{width: 36, height: 36},
{width: 34, height: 34},
]}>
<Message
style={[
t.atoms.text,
{marginLeft: 1, marginBottom: 1, opacity: 0.3},
]}
size="md"
/>
<Message style={[t.atoms.text, {opacity: 0.3}]} size="md" />
</View>
)
} else {
@@ -66,12 +61,9 @@ export function MessageProfileButton({
shape="round"
label={_(msg`Message ${profile.handle}`)}
to={`/messages/${convo.id}`}
style={[a.justify_center, {width: 36, height: 36}]}
style={[a.justify_center]}
onPress={onPress}>
<Message
style={[t.atoms.text, {marginLeft: 1, marginBottom: 1}]}
size="md"
/>
<ButtonIcon icon={Message} size="md" />
</Link>
)
} else {
-175
View File
@@ -1,175 +0,0 @@
import React, {useCallback, useEffect} from 'react'
import {View} from 'react-native'
import {ChatBskyActorDeclaration} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useUpdateActorDeclaration} from '#/state/queries/messages/actor-declaration'
import {useProfileQuery} from '#/state/queries/profile'
import {useSession} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useTheme, web} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import * as Toggle from '#/components/forms/Toggle'
import {Message_Stroke2_Corner0_Rounded} from '#/components/icons/Message'
import {Text} from '#/components/Typography'
export function MessagesNUX() {
const control = Dialog.useDialogControl()
const {currentAccount} = useSession()
const {data: profile} = useProfileQuery({
did: currentAccount!.did,
})
useEffect(() => {
if (profile && typeof profile.associated?.chat === 'undefined') {
const timeout = setTimeout(() => {
control.open()
}, 1000)
return () => {
clearTimeout(timeout)
}
}
}, [profile, control])
if (!profile) return null
return (
<Dialog.Outer control={control}>
<Dialog.Handle />
<DialogInner chatDeclation={profile.associated?.chat} />
</Dialog.Outer>
)
}
function DialogInner({
chatDeclation,
}: {
chatDeclation?: ChatBskyActorDeclaration.Record
}) {
const control = Dialog.useDialogContext()
const {_} = useLingui()
const t = useTheme()
const [initialized, setInitialzed] = React.useState(false)
const {mutate: updateDeclaration} = useUpdateActorDeclaration({
onError: () => {
Toast.show(_(msg`Failed to update settings`), 'xmark')
},
})
const onSelectItem = useCallback(
(keys: string[]) => {
const key = keys[0]
if (!key) return
updateDeclaration(key as 'all' | 'none' | 'following')
},
[updateDeclaration],
)
useEffect(() => {
if (!chatDeclation && !initialized) {
updateDeclaration('following')
setInitialzed(true)
}
}, [chatDeclation, updateDeclaration, initialized])
return (
<Dialog.ScrollableInner
label={_(msg`Introducing Direct Messages`)}
style={web({maxWidth: 440})}>
<View style={a.gap_xl}>
<View style={[a.align_center, a.pt_sm, a.pb_xs]}>
<Message_Stroke2_Corner0_Rounded width={64} />
<Text style={[a.text_2xl, a.font_bold, a.text_center, a.mt_md]}>
<Trans>Direct messages are here!</Trans>
</Text>
<Text style={[a.text_md, a.text_center, a.mt_sm]}>
<Trans>Privately chat with other users.</Trans>
</Text>
</View>
<View
style={[
a.gap_xs,
a.border,
a.overflow_hidden,
a.rounded_sm,
t.atoms.border_contrast_low,
]}>
<View
style={[
a.p_md,
a.border_b,
t.atoms.bg_contrast_25,
t.atoms.border_contrast_low,
]}>
<Text style={[a.text_sm, a.font_bold]}>
<Trans>Who can message you?</Trans>
</Text>
<Text
style={[
a.mt_xs,
a.text_sm,
a.italic,
t.atoms.text_contrast_medium,
]}>
<Trans>You can change this at any time.</Trans>
</Text>
</View>
<View style={[a.px_md, a.py_xs]}>
<Toggle.Group
label={_(msg`Who can message you?`)}
type="radio"
values={[chatDeclation?.allowIncoming ?? 'following']}
onChange={onSelectItem}>
<View>
<Toggle.Item
name="all"
label={_(msg`Everyone`)}
style={[a.justify_between, a.py_sm, a.rounded_2xs]}>
<Toggle.LabelText>
<Trans>Everyone</Trans>
</Toggle.LabelText>
<Toggle.Radio />
</Toggle.Item>
<Toggle.Item
name="following"
label={_(msg`Users I follow`)}
style={[a.justify_between, a.py_sm, a.rounded_2xs]}>
<Toggle.LabelText>
<Trans>Users I follow</Trans>
</Toggle.LabelText>
<Toggle.Radio />
</Toggle.Item>
<Toggle.Item
name="none"
label={_(msg`No one`)}
style={[a.justify_between, a.py_sm, a.rounded_2xs]}>
<Toggle.LabelText>
<Trans>No one</Trans>
</Toggle.LabelText>
<Toggle.Radio />
</Toggle.Item>
</View>
</Toggle.Group>
</View>
</View>
<Button
label={_(msg`Start chatting`)}
accessibilityHint={_(msg`Close modal`)}
size="large"
color="primary"
variant="solid"
onPress={() => control.close()}>
<ButtonText>
<Trans>Get started</Trans>
</ButtonText>
</Button>
</View>
<Dialog.Close />
</Dialog.ScrollableInner>
)
}
+2 -6
View File
@@ -10,13 +10,11 @@ import {useLingui} from '@lingui/react'
import {useMutation} from '@tanstack/react-query'
import {ReportOption} from '#/lib/moderation/useReportOptions'
import {isAndroid} from '#/platform/detection'
import {useAgent} from '#/state/session'
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'
@@ -41,14 +39,11 @@ let ReportDialog = ({
}): React.ReactNode => {
const {_} = useLingui()
return (
<Dialog.Outer
control={control}
nativeOptions={isAndroid ? {sheet: {snapPoints: ['100%']}} : {}}>
<Dialog.Outer control={control}>
<Dialog.Handle />
<Dialog.ScrollableInner label={_(msg`Report this message`)}>
<DialogInner params={params} />
<Dialog.Close />
<KeyboardControllerPadding />
</Dialog.ScrollableInner>
</Dialog.Outer>
)
@@ -277,6 +272,7 @@ function PreviewMessage({message}: {message: ChatBskyConvoDefs.MessageView}) {
message,
key: '',
nextMessage: null,
prevMessage: null,
}}
style={[a.text_left, a.mb_0]}
/>
+3 -5
View File
@@ -2,9 +2,9 @@ import React, {useCallback} from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
import {logEvent} from 'lib/statsig/statsig'
import {FAB} from '#/view/com/util/fab/FAB'
import * as Toast from '#/view/com/util/Toast'
import {useTheme} from '#/alf'
@@ -55,10 +55,8 @@ export function NewChat({
accessibilityHint=""
/>
<Dialog.Outer
control={control}
testID="newChatDialog"
nativeOptions={{sheet: {snapPoints: ['100%']}}}>
<Dialog.Outer control={control} testID="newChatDialog">
<Dialog.Handle />
<SearchablePeopleList
title={_(msg`Start a new chat`)}
onSelectChat={onCreateChat}
@@ -5,10 +5,8 @@ import React, {
useRef,
useState,
} from 'react'
import type {TextInput as TextInputType} from 'react-native'
import {View} from 'react-native'
import {TextInput, View} from 'react-native'
import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
import {BottomSheetFlatListMethods} from '@discord/bottom-sheet'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -16,18 +14,17 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {isWeb} from '#/platform/detection'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
import {useListConvosQuery} from '#/state/queries/messages/list-converations'
import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
import {useSession} from '#/state/session'
import {useActorAutocompleteQuery} from 'state/queries/actor-autocomplete'
import {ListMethods} from '#/view/com/util/List'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, native, useTheme, web} from '#/alf'
import {Button} from '#/components/Button'
import {Button, ButtonIcon} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {TextInput} from '#/components/dms/dialogs/TextInput'
import {canBeMessaged} from '#/components/dms/util'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {Text} from '#/components/Typography'
@@ -66,9 +63,9 @@ export function SearchablePeopleList({
const {_} = useLingui()
const moderationOpts = useModerationOpts()
const control = Dialog.useDialogContext()
const listRef = useRef<BottomSheetFlatListMethods>(null)
const listRef = useRef<ListMethods>(null)
const {currentAccount} = useSession()
const inputRef = useRef<TextInputType>(null)
const inputRef = useRef<TextInput>(null)
const [searchText, setSearchText] = useState('')
@@ -101,15 +98,15 @@ export function SearchablePeopleList({
})
}
_items = _items.sort(a => {
_items = _items.sort(item => {
// @ts-ignore
return a.enabled ? -1 : 1
return item.enabled ? -1 : 1
})
}
} else {
const placeholders: Item[] = Array(10)
.fill(0)
.map((_, i) => ({
.map((__, i) => ({
type: 'placeholder',
key: i + '',
}))
@@ -155,9 +152,9 @@ export function SearchablePeopleList({
}
// only sort follows
followsItems = followsItems.sort(a => {
followsItems = followsItems.sort(item => {
// @ts-ignore
return a.enabled ? -1 : 1
return item.enabled ? -1 : 1
})
// then append
@@ -177,9 +174,9 @@ export function SearchablePeopleList({
}
}
_items = _items.sort(a => {
_items = _items.sort(item => {
// @ts-ignore
return a.enabled ? -1 : 1
return item.enabled ? -1 : 1
})
} else {
_items.push(...placeholders)
@@ -242,57 +239,46 @@ export function SearchablePeopleList({
<View
style={[
a.relative,
a.pt_md,
web(a.pt_lg),
native(a.pt_4xl),
a.pb_xs,
a.px_lg,
a.border_b,
t.atoms.border_contrast_low,
t.atoms.bg,
native([a.pt_lg]),
]}>
<View
style={[
a.relative,
native(a.align_center),
a.justify_center,
{height: 32},
]}>
<Button
label={_(msg`Close`)}
size="small"
shape="round"
variant="ghost"
color="secondary"
style={[
a.absolute,
a.z_20,
native({
left: -7,
}),
web({
right: -4,
}),
]}
onPress={() => control.close()}>
{isWeb ? (
<X size="md" fill={t.palette.contrast_500} />
) : (
<ChevronLeft size="md" fill={t.palette.contrast_500} />
)}
</Button>
<View style={[a.relative, native(a.align_center), a.justify_center]}>
<Text
style={[
a.z_10,
a.text_lg,
a.font_bold,
a.font_heavy,
a.leading_tight,
t.atoms.text_contrast_high,
]}>
{title}
</Text>
{isWeb ? (
<Button
label={_(msg`Close`)}
size="small"
shape="round"
variant={isWeb ? 'ghost' : 'solid'}
color="secondary"
style={[
a.absolute,
a.z_20,
web({right: -4}),
native({right: 0}),
native({height: 32, width: 32, borderRadius: 16}),
]}
onPress={() => control.close()}>
<ButtonIcon icon={X} size="md" />
</Button>
) : null}
</View>
<View style={[native([a.pt_sm]), web([a.pt_xs])]}>
<View style={[, web([a.pt_xs])]}>
<SearchInput
inputRef={inputRef}
value={searchText}
@@ -309,7 +295,6 @@ export function SearchablePeopleList({
t.atoms.border_contrast_low,
t.atoms.bg,
t.atoms.text_contrast_high,
t.palette.contrast_500,
_,
title,
searchText,
@@ -326,14 +311,7 @@ export function SearchablePeopleList({
keyExtractor={(item: Item) => item.key}
style={[
web([a.py_0, {height: '100vh', maxHeight: 600}, a.px_0]),
native({
height: '100%',
paddingHorizontal: 0,
marginTop: 0,
paddingTop: 0,
borderTopLeftRadius: 40,
borderTopRightRadius: 40,
}),
native({height: '100%'}),
]}
webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]}
keyboardDismissMode="on-drag"
@@ -396,7 +374,8 @@ function ProfileCard({
<View style={[a.flex_1, a.gap_2xs]}>
<Text
style={[t.atoms.text, a.font_bold, a.leading_tight, a.self_start]}
numberOfLines={1}>
numberOfLines={1}
emoji>
{displayName}
</Text>
<Text
@@ -474,7 +453,7 @@ function SearchInput({
value: string
onChangeText: (text: string) => void
onEscape: () => void
inputRef: React.RefObject<TextInputType>
inputRef: React.RefObject<TextInput>
}) {
const t = useTheme()
const {_} = useLingui()
@@ -2,9 +2,9 @@ import React, {useCallback} from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
import {logEvent} from 'lib/statsig/statsig'
import * as Toast from '#/view/com/util/Toast'
import * as Dialog from '#/components/Dialog'
import {SearchablePeopleList} from './SearchablePeopleList'
@@ -17,10 +17,8 @@ export function SendViaChatDialog({
onSelectChat: (chatId: string) => void
}) {
return (
<Dialog.Outer
control={control}
testID="sendViaChatChatDialog"
nativeOptions={{sheet: {snapPoints: ['100%']}}}>
<Dialog.Outer control={control} testID="sendViaChatChatDialog">
<Dialog.Handle />
<SendViaChatDialogInner control={control} onSelectChat={onSelectChat} />
</Dialog.Outer>
)
+9
View File
@@ -16,3 +16,12 @@ export function canBeMessaged(profile: AppBskyActorDefs.ProfileView) {
return false
}
}
export function localDateString(date: Date) {
// can't use toISOString because it should be in local time
const mm = date.getMonth()
const dd = date.getDate()
const yyyy = date.getFullYear()
// not padding with 0s because it's not necessary, it's just used for comparison
return `${yyyy}-${mm}-${dd}`
}
+83
View File
@@ -0,0 +1,83 @@
import React from 'react'
import {TextInput, View} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {HITSLOP_10} from '#/lib/constants'
import {isNative} from '#/platform/detection'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import * as TextField from '#/components/forms/TextField'
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as MagnifyingGlassIcon} from '#/components/icons/MagnifyingGlass2'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
type SearchInputProps = Omit<TextField.InputProps, 'label'> & {
label?: TextField.InputProps['label']
/**
* Called when the user presses the (X) button
*/
onClearText?: () => void
}
export const SearchInput = React.forwardRef<TextInput, SearchInputProps>(
function SearchInput({value, label, onClearText, ...rest}, ref) {
const t = useTheme()
const {_} = useLingui()
const showClear = value && value.length > 0
return (
<View style={[a.w_full, a.relative]}>
<TextField.Root>
<TextField.Icon icon={MagnifyingGlassIcon} />
<TextField.Input
inputRef={ref}
label={label || _(msg`Search`)}
value={value}
placeholder={_(msg`Search`)}
returnKeyType="search"
keyboardAppearance={t.scheme}
selectTextOnFocus={isNative}
autoFocus={false}
accessibilityRole="search"
autoCorrect={false}
autoComplete="off"
autoCapitalize="none"
style={[
showClear
? {
paddingRight: 24,
}
: {},
]}
{...rest}
/>
</TextField.Root>
{showClear && (
<View
style={[
a.absolute,
a.z_10,
a.my_auto,
a.inset_0,
a.justify_center,
a.pr_sm,
{left: 'auto'},
]}>
<Button
testID="searchTextInputClearBtn"
onPress={onClearText}
label={_(msg`Clear search query`)}
hitSlop={HITSLOP_10}
size="tiny"
shape="round"
variant="ghost"
color="secondary">
<ButtonIcon icon={X} size="xs" />
</Button>
</View>
)}
</View>
)
},
)
+1 -1
View File
@@ -126,7 +126,7 @@ export type InputProps = Omit<TextInputProps, 'value' | 'onChangeText'> & {
value?: string
onChangeText?: (value: string) => void
isInvalid?: boolean
inputRef?: React.RefObject<TextInput>
inputRef?: React.RefObject<TextInput> | React.ForwardedRef<TextInput>
}
export function createInput(Component: typeof TextInput) {
+1 -1
View File
@@ -2,8 +2,8 @@ import React from 'react'
import {Pressable, View, ViewStyle} from 'react-native'
import Animated, {LinearTransition} from 'react-native-reanimated'
import {HITSLOP_10} from '#/lib/constants'
import {isNative} from '#/platform/detection'
import {HITSLOP_10} from 'lib/constants'
import {
atoms as a,
flatten,
+2
View File
@@ -25,6 +25,7 @@ import {
tr,
uk,
zhCN,
zhHK,
zhTW,
} from 'date-fns/locale'
@@ -52,6 +53,7 @@ const locales: Record<AppLanguage, Locale | undefined> = {
tr,
uk,
['zh-CN']: zhCN,
['zh-HK']: zhHK,
['zh-TW']: zhTW,
}
+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Envelope_Open_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M4 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v6.386c1.064-.002 2 .86 2 2.001V19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-6.613c0-1.142.936-2.003 2-2.001V4Zm2 6.946 6 2 6-2V4H6v6.946ZM9 8a1 1 0 0 1 1-1h4a1 1 0 1 1 0 2h-4a1 1 0 0 1-1-1Zm2.367 6.843L4 12.387V19h16v-6.613l-7.367 2.456a2 2 0 0 1-1.265 0Z',
})
@@ -3,11 +3,14 @@ import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useAgent, useSession} from 'state/session'
import {atoms as a} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {isNative} from '#/platform/detection'
import {useAgent, useSession} from '#/state/session'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {DialogControlProps} from '#/components/Dialog'
import {Divider} from '#/components/Divider'
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as Resend} from '#/components/icons/ArrowRotateCounterClockwise'
import {useIntentDialogs} from '#/components/intents/IntentDialogs'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
@@ -23,7 +26,9 @@ export function VerifyEmailIntentDialog() {
)
}
function Inner({control}: {control: DialogControlProps}) {
function Inner({}: {control: DialogControlProps}) {
const t = useTheme()
const {gtMobile} = useBreakpoints()
const {_} = useLingui()
const {verifyEmailState: state} = useIntentDialogs()
const [status, setStatus] = React.useState<
@@ -58,43 +63,47 @@ function Inner({control}: {control: DialogControlProps}) {
}
return (
<Dialog.ScrollableInner label={_(msg`Verify email dialog`)}>
<Dialog.Close />
<Dialog.ScrollableInner
label={_(msg`Verify email dialog`)}
style={[
gtMobile ? {width: 'auto', maxWidth: 400, minWidth: 200} : a.w_full,
]}>
<View style={[a.gap_xl]}>
{status === 'loading' ? (
<View style={[a.py_2xl, a.align_center, a.justify_center]}>
<Loader size="xl" />
<Loader size="xl" fill={t.atoms.text_contrast_low.color} />
</View>
) : status === 'success' ? (
<>
<Text style={[a.font_bold, a.text_2xl]}>
<View style={[a.gap_sm, isNative && a.pb_xl]}>
<Text style={[a.font_heavy, a.text_2xl]}>
<Trans>Email Verified</Trans>
</Text>
<Text style={[a.text_md, a.leading_tight]}>
<Text style={[a.text_md, a.leading_snug]}>
<Trans>
Thanks, you have successfully verified your email address.
Thanks, you have successfully verified your email address. You
can close this dialog.
</Trans>
</Text>
</>
</View>
) : status === 'failure' ? (
<>
<Text style={[a.font_bold, a.text_2xl]}>
<View style={[a.gap_sm]}>
<Text style={[a.font_heavy, a.text_2xl]}>
<Trans>Invalid Verification Code</Trans>
</Text>
<Text style={[a.text_md, a.leading_tight]}>
<Text style={[a.text_md, a.leading_snug]}>
<Trans>
The verification code you have provided is invalid. Please make
sure that you have used the correct verification link or request
a new one.
</Trans>
</Text>
</>
</View>
) : (
<>
<Text style={[a.font_bold, a.text_2xl]}>
<View style={[a.gap_sm, isNative && a.pb_xl]}>
<Text style={[a.font_heavy, a.text_2xl]}>
<Trans>Email Resent</Trans>
</Text>
<Text style={[a.text_md, a.leading_tight]}>
<Text style={[a.text_md, a.leading_snug]}>
<Trans>
We have sent another verification email to{' '}
<Text style={[a.text_md, a.font_bold]}>
@@ -103,38 +112,29 @@ function Inner({control}: {control: DialogControlProps}) {
.
</Trans>
</Text>
</>
</View>
)}
{status !== 'loading' ? (
<View style={[a.w_full, a.flex_row, a.gap_sm, {marginLeft: 'auto'}]}>
{status === 'failure' && (
<>
<Divider />
<Button
label={_(msg`Close`)}
onPress={() => control.close()}
label={_(msg`Resend Verification Email`)}
onPress={onPressResendEmail}
variant="solid"
color={status === 'failure' ? 'secondary' : 'primary'}
color="secondary_inverted"
size="large"
style={{marginLeft: 'auto'}}>
disabled={sending}>
<ButtonIcon icon={sending ? Loader : Resend} position="left" />
<ButtonText>
<Trans>Close</Trans>
<Trans>Resend Email</Trans>
</ButtonText>
</Button>
{status === 'failure' ? (
<Button
label={_(msg`Resend Verification Email`)}
onPress={onPressResendEmail}
variant="solid"
color="primary"
size="large"
disabled={sending}>
<ButtonText>
<Trans>Resend Email</Trans>
</ButtonText>
{sending ? <Loader size="sm" style={{color: 'white'}} /> : null}
</Button>
) : null}
</View>
) : null}
</>
)}
</View>
<Dialog.Close />
</Dialog.ScrollableInner>
)
}

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