Compare commits

...

102 Commits

Author SHA1 Message Date
Dan Abramov a3d7e3e56c Narrow down the condition slightly 2024-11-22 23:24:17 +00:00
Dan Abramov 4e09534475 Remove noreferrer on links
We do have <meta name="referrer" content="origin-when-cross-origin"> in HTML, should be sufficient.
2024-11-22 23:07:07 +00:00
Dan Abramov ad5445cd08 Add an opt out for sending utm 2024-11-22 22:46:48 +00:00
Eric Bailey af4b7cc354 Add ChainLink3 2024-11-22 22:14:59 +00:00
Dan Abramov 5e8535a1ba Pass referer on native 2024-11-22 21:40:15 +00:00
dan ba04bb58cd Fix leaking background (#6642) 2024-11-22 19:51:36 +00:00
dan 7fa47ef3c9 Simplify play icon shadow (#6641)
* Simplify play icon shadow

* Make it consistent across themes
2024-11-22 19:43:32 +00:00
rshigg ced67876cc Add ability to block account from post dropdown menu (#6091)
Co-authored-by: Hailey <me@haileyok.com>
Co-authored-by: Hailey <hailey@blueskyweb.xyz>
2024-11-22 11:36:00 -08:00
Samuel Newman 247bc100cb Bump Typescript version to 5.7 (#6640) 2024-11-22 19:27:30 +00:00
Samuel Newman bc2a3aee0c fix avi follow deadzone (#6639) 2024-11-22 18:56:07 +00:00
Samuel Newman 3781074921 Add gif support to web (#6433)
* add gif support to web

* rm set dimensions

* rm effect from preview

* rm log

* rm use of {cause: error}

* fix lint
2024-11-22 17:58:29 +00:00
Samuel Newman 76ca72cf72 Dismissable toasts (#6345)
* dismissable toast

* adjust top offset

* improve a11y

* stretchy pull-down

* Dismiss web on tap

* Simplify code

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2024-11-22 16:11:59 +00:00
Samuel Newman 5d4aaa5b5f [a11y] Video - fix labels and make more detailed (#6635)
* fix labels and make more detailed

* move overall label to parent
2024-11-22 15:23:24 +00:00
Paul Coroneos 437bdcf9a9 Remove unnecessary lint rule suppression (#6544)
* ▫️remove deprecated react native eslint package. add react hooks and modern native eslint package. fix bugs

* in retrospect lets just remove the async from uselocalelanguage

* finally clean it all up

* revert yarn.lock

* restore logic
2024-11-22 15:22:48 +00:00
Samuel Newman d9ba324d7f Use android native animations (#6562) 2024-11-22 14:56:13 +00:00
Eric Bailey 72326844cd Improve error name and message for signed-in only feeds (#6577)
* Improve error name and message for logged-in only feeds

* Revert msg edit

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2024-11-22 14:45:59 +00:00
Samuel Newman a5dbb35237 Use outline style for repost cancel button (#6509)
* use outline style for repost cancel button

* use trans macro in JSX
2024-11-22 13:41:24 +00:00
dan a0acd514ab [Lightbox] Fix jump when zooming out on iOS (#6633) 2024-11-22 13:34:16 +00:00
Paul Coroneos 5028753367 Fix lightbox spinner (#6561)
* refactor imageitem useanimatedreaction logic

* revert copy/paste fix

* revert last commit

* Fix conditions

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2024-11-22 11:59:47 +00:00
surfdude29 058a29c6e3 Add CFBundleLocalizations key to Info.plist (#6567) 2024-11-22 11:34:37 +00:00
Frudrax Cheng 09ce65b24e Fix display language not switching correctly to Chinese on native. (#6621)
* Update deviceLocales.ts

* Update deviceLocales.ts
2024-11-22 11:32:25 +00:00
dan c858700c17 [Android] Lower Feed maxRenderPerBatch (#6624) 2024-11-22 04:35:53 +00:00
dan 625459add6 Fix some overdraw (#6617) 2024-11-21 23:40:37 +00:00
dan 391e0c4c18 Fix embeder (#6616) 2024-11-21 22:52:28 +00:00
dan dc3a42edb1 Reduce <Text> nesting (#6615)
* Move isOnlyEmoji out of RichText

To fix Fast Refresh.

* Make renderChildrenWithEmoji work with any children

* Always go through UITextView for consistency

It already contains the `selectable` and `iOS` checks inside.

* Move `emoji` check into `renderChildrenWithEmoji`

* Remove unnecessary intermediate UITextView nodes

* Make childHasEmoji check recursive

It didn't handle nested arrays etc correctly before.

* Remove the "children must be string" limitation

Should not be necessary now that we correctly handle nested arrays etc.

* Fix unnecessary regex reallocation

This doesn't have a global flag so it's okay to reuse.

* Remove unnecessary <Text> wrapper in RichText
2024-11-21 22:52:16 +00:00
dan 84724bb940 [Android] Patch react-native-svg to cache parsed paths (#6583) 2024-11-21 22:32:49 +00:00
Samuel Newman 9f1e648686 Fix overflow issue on iOS autocomplete, among other things (#6611)
* stop using ref in render

* fix display name fallback on web

* use unicode ellipsis for useGrapheme

* fix overflow issue

* sanitize handle/displayname on web
2024-11-21 22:32:32 +00:00
Aditya Mathur f67e00a85c fix: reused getRkey function in embed component (#6591) 2024-11-21 21:10:19 +00:00
jviide 48cda1350e Dedup zod versions in bskyembed/yarn.lock (#6610) 2024-11-21 20:45:40 +00:00
dan cc60566cde Fix Fast Refresh in <Text> files (#6609)
* Separate non-components from components

* Mark old Text as deprecated

* Move emoji utilities to non-React file

* Fix type

* Fix import
2024-11-21 20:44:40 +00:00
dan ff23ddb556 Don't underline links on native hover (#6588) 2024-11-21 19:20:46 +00:00
lauren dff11fc076 Pin exact version of React Compiler packages (#6607)
This PR pins the various React Compiler packages to their exact
versions. While it is typically safe to upgrade the compiler, we
recommend manual upgrades because changes in memoization may or may not
be unexpected.

In the near future we will open source an upgrade script that automates
comparing the current compiled output with the upgraded output and
prints a diff for human confirmation prior to committing the upgrade.
2024-11-21 17:40:39 +00:00
dan ad83b8303e Remove unused RN.Animated code (#6584)
* Remove unused RN.Animated code

* Rm more dead code
2024-11-21 12:10:34 +00:00
dan 6438ec6d28 Disable vertical scrollbars on Android only (#6586) 2024-11-21 12:10:16 +00:00
Samuel Newman 97a4ff1a4c Bump 1.95.0 (#6530) 2024-11-21 01:25:25 +00:00
Horus Lugo 6605970fcf Prevent ProfileHeaderHandle from breaking out of its parent view (#6574) 2024-11-20 21:47:06 +00:00
Samuel Newman 202df845f3 rm unused type lib (#6546) 2024-11-20 16:21:18 +00:00
Samuel Newman 0e4f6717ce fix change handle dialog on android (#6563) 2024-11-20 13:26:23 +00:00
Trivikram Kamat f1c221ed28 Bump typescript-eslint to 7.x (#6525)
* chore(deps-dev): bump typescript-eslint to 6.x

* chore(deps-dev): switch to @react-native/eslint-config

* chore(deps-dev): bump typescript-eslint to 7.x
2024-11-20 02:23:26 +00:00
Andrew Aquino 78e34c8d35 preserve video's last known time when scrolling away (#6239) 2024-11-19 22:42:25 +00:00
axiand f882cf9737 Implement Polish translation (#6283)
* Initial polish translation sweep

* rewrite feeds page flavor text

* bring up to date with dev branch, revisit some strings

* redoing some more...

* bring up to date

* rm dead strings, try out a different phrasing for threaded mode str

prev translation was nonsensical - we're already calling threads "wątki". let's try "recursive tree" instead to describe what threaded mode does

* tweaks here and there

* more tweaks. sign translation file

TL note: trying a new space-saving translation for send via direct message, old one made the context menu stretch

* space-saving translations for profile tabs

* up to date

* tl latest changes

* some small fixes

* few more tweaks around profile editing strings

* i dislike gendered languages

* yet more tweaks

* Update dates.ts

* Review suggestions (1)

* tiny fixes

* Review suggestions (2)

* Review suggestions (3)

* Review suggestions (4)

* "Anuluj wyciszenie"

* A few new strings

* Remove dead strings again

* Review suggestions (5)

* te -> to

---------

Co-authored-by: Frudrax Cheng <i@cynosura.one>
2024-11-19 12:24:18 -08:00
smileyhead c0c8a192ce Update Hungarian translation for 1.94 (#6186) 2024-11-19 12:17:10 -08:00
Evan 3c33e42d35 Fix post interaction selectable text being cut off (#6527) 2024-11-19 19:07:27 +00:00
Paul Coroneos 5d7cfa0206 Remove aria-pressed attr from button. Closes #6325 (#6402) 2024-11-19 19:07:13 +00:00
Samuel Newman 103dfe2145 add back expo-system-ui (#6529) 2024-11-19 19:02:28 +00:00
dan 31b0d7cede Render post menu items lazily (take two) (#6473)
* Revert "Revert "Render dropdown menu items lazily (#6437)" (#6470)"

This reverts commit 0b38c77faa.

* Lazily initialize menu items

* Split in two files

* Make it work on mobile

This is wonky because our useMenuDialog abstraction only has `isOpen` on web. I couldn't figure out a way to make it work xplat so I'm just tracking it myself manually.

* Fix typo in comment
2024-11-19 18:27:56 +00:00
Trivikram Kamat 39c9f7248e Remove usused direct dependencies on 'webpack' (#6466)
* Remove usused direct dependencies on 'webpack'

* Remove direct dependency on 'babel-plugin-react-native-web'
2024-11-19 15:45:09 +00:00
Trivikram Kamat 2b30b0f7c6 Remove unused 'lodash' dependencies (#6472) 2024-11-19 15:35:16 +00:00
Paul Coroneos ff5ad38253 Leverage jsx transform to remove unnecessary react imports and update eslint config (#6516)
* update eslint config with jsx runtime

* leverage jsx transform to remove unnecessary react imports and update eslint config

* run yarn lint --fix to remove eslint disables related to react/prop-types that is now disabled
2024-11-19 15:33:07 +00:00
dan cfc653a5f4 Split FeedSlice into FlatList rows (#6507) 2024-11-19 15:24:08 +00:00
Samuel Newman 8d5e61e1e5 Use native modal for signup queue screen (#6392)
* fix top inset for signup queue

* use native modal for queue screen

* spacing for if text is super large

* don't allow scroll bounce
2024-11-19 14:43:40 +00:00
Samuel Newman ea3fe93be9 Attempted video perf improvements (#6519)
* remove layout animations from video

* only show button/spinner when necessary

* use native activityindicator rather than loader
2024-11-19 12:50:06 +00:00
dan 9a57d2a538 Add Profiler build for Android (#6510) 2024-11-19 02:20:58 +00:00
dan 7b6c182723 Add useHandleRef as a lighter alternative for useAnimatedRef (#6500) 2024-11-18 22:21:47 +00:00
dan 4f0f9eb413 Fix TagMenu initial render perf (#6483)
* Split tag menu dialog content

* Log the actual error please

* Fix it on native
2024-11-18 22:05:51 +00:00
Martin Pittenauer 1486e5cc5a Help Siri to correctly pronounce Blue Sky. (#6496)
* Add CFBundleSpokenName

Help Siri to correctly pronounce Blue Sky. Instead of pronouncing it like “Lebowski”.

* Format

---------

Co-authored-by: dan <dan.abramov@gmail.com>
2024-11-18 21:55:35 +00:00
dan ee49ca7a3d Hardcode Giphy embed height (#6475) 2024-11-18 21:55:20 +00:00
dan b87c94e6d9 Extract RepostButton inner dialog (#6498)
* Extract RepostButton inner dialog

* use `useDialogContext` instead of passing prop

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2024-11-18 21:51:23 +00:00
dan ec97352201 Fix layout jumps for images (#6474) 2024-11-18 21:42:32 +00:00
dan 924f1e0e7f Optimize ContentHider (#6501) 2024-11-18 17:44:45 +00:00
dan 0b38c77faa Revert "Render dropdown menu items lazily (#6437)" (#6470)
This reverts commit e9fe8d90ef.
2024-11-17 21:06:22 +00:00
dan 2f74a8dbe9 Upgrade React Compiler (take two) (#6460)
* Revert "Revert "React compiler beta and reenable rule (#5898)" (#6071)"

This reverts commit 7111e38cd4.

* Update the compiler

* Don't inline compiler runtime import
2024-11-17 18:50:28 +00:00
dan dd9a2f60dd Fix build (#6459) 2024-11-17 17:26:45 +00:00
dan 474c4eff29 Use compiler-safe Reanimated get/set APIs (#6391)
* Convert lightbox to get/set

* Work around software-mansion/react-native-reanimated#6613

* Use get/set in more places

* Port MainScrollProvider to get/set

* Port more to get/set

* Port composer to get/set

* Remove unnecessary thread hops in composer

* Port more things to get/set

* Convert more to get/set, remove redundant runOnJS

* Convert remaining cases to get/set
2024-11-17 15:06:28 +00:00
Trivikram Kamat d575a2fdaa Remove unused 'react-scripts' devDependency (#6441) 2024-11-17 14:57:01 +00:00
Samuel Newman 8d1c93efc8 fix video thumb cleanup (#6431) 2024-11-17 14:56:15 +00:00
dan e9fe8d90ef Render dropdown menu items lazily (#6437) 2024-11-17 03:15:03 +00:00
dan ddf2a64a72 [Web] Clicking root link twice refreshes the screen (#6434)
* [Web] Clicking root link twice refreshes the screen

* Scope it to navigation action

In practice this means -- just for the bottom mobile web tab bar.
2024-11-17 00:37:34 +00:00
Paul Frazee 3c30bb1d35 Release 1.94 (#6430)
* Fix errors in hindi .po

* Run intl extract

* Update join count
2024-11-16 15:12:06 -08:00
dan 965bcc44dc Sort pins last in thread view (#6426) 2024-11-16 22:02:33 +00:00
dan edaf230612 Remove typography NUX and consistently call upsertProfile (#6424)
* Make profile upsert unconditional

* Remove Typography NUX

* Early exit NUXes on unknown creation date

* Clarify via comment
2024-11-16 21:39:47 +00:00
Hailey 9bbea3f33a Email verification tweaks (date) (#6416) 2024-11-16 13:34:30 -08:00
Luan 43d6c15e88 Update PT-BR messages.po (#6284)
* Update PT-BR translation

* Update PT-BR translation

* Update messages.po

* Update PT-BR translation for consistent usage of "postagem"

* Update messages.po

* Update src/locale/locales/pt-BR/messages.po

Co-authored-by: Gildásio Filho <gildasiogx@gmail.com>

* Update src/locale/locales/pt-BR/messages.po

* Update src/locale/locales/pt-BR/messages.po

* Update src/locale/locales/pt-BR/messages.po

* Update src/locale/locales/pt-BR/messages.po

* Apply suggestions from code review

Co-authored-by: Gildásio Filho <gildasiogx@gmail.com>

---------

Co-authored-by: Gildásio Filho <gildasiogx@gmail.com>
Co-authored-by: Paul Frazee <pfrazee@gmail.com>
2024-11-16 12:44:11 -08:00
CanGiante 4e1ec0db86 Fix the translation of the adjective "open" (#6206) 2024-11-16 12:27:06 -08:00
Elena Torró d8e2a90dd7 Fix minor localization issues in Spanish: (#6094)
* Fix minor localization issues in Spanish:

- Use 'pack de inicio' instead of 'paquete de inicio'
- Use 'Inténtalo' instead of 'Intenta'
- Fix 'Por favor'
- Do not use 'una' as article for 'posts'
- Use 'Eliminado' instead of 'Removido'

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

Co-authored-by: oledfish <88390729+oledfish@users.noreply.github.com>

* Use 'paquete de inicio' and fix typos

---------

Co-authored-by: oledfish <88390729+oledfish@users.noreply.github.com>
2024-11-16 12:15:42 -08:00
Kevin Scannell 1f9c94a3cc Update Irish localization to 100% (#6387) 2024-11-16 12:13:27 -08:00
Stanislas Signoud 8e40b1147a Update French translations (November 9th, 2024) (#6190)
* Remove legacy strings in FR localisation file

* Update French translations

* Fix typos and inconsistent French translations

* Get rid of a few 'Veuillez' in French locale to appear less formal
2024-11-16 12:08:59 -08:00
Marco Maroni 0fd6eeb025 Update italian translation (#6188)
* Updated and added missing italian translation

* Resolved conflicts

* Fix conflict

* Fix some translation

* Update messages.po

Some other fixes
2024-11-16 12:08:22 -08:00
FalKoopa 330ed124d6 Big update of the Hindi translation (#6077)
* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Resolve conflicts

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po

* Update messages.po based on feedback

* Update messages.po

* Update messages.po
2024-11-16 12:08:02 -08:00
Ivan Beà 01469b092f Update catalan messages.po (#6030)
* Update catalan messages.po

back to 100%
Please have a look at it @jordimas @darccio @surfdude29 @rortan134

* Update catalan messages.po

aplying @jordimas corrections

* Update messages.po
2024-11-16 12:07:17 -08:00
Samuel Newman c81e149749 use IAB for translate (#6421) 2024-11-16 20:06:24 +00:00
Samuel Newman ebdbf55d14 manually implement drawertype logic (#6419) 2024-11-16 20:06:11 +00:00
Frudrax Cheng 6aa14f3203 Update Chinese & Cantonese localization (#6020)
* New translations messages.po (Chinese Simplified)

* New translations messages.po (Chinese Traditional)

* New translations messages.po (Chinese Traditional, Hong Kong)

* Update translates from crowdin

* Run intl:extract

* Update translates from crowdin

* Update translates from crowdin

* zh-CN: Fix typo

* zh-HK: Update translates

* Update translates by notlin4

* Update translates from crowdin

* Update translates from crowdin

* zh-CN: Clarify description text

* Update translates from crowdin

* zh-HK: Update translates

* zh-CN: Re-translate 'Feed'

* zh-HK: Update Translates

* Update translates from crowdin

* CN & TW: Consistency fixes

* Update translates from crowdin

* Update translates from crowdin

* Unify the translation of 'Yes'

* Update translates from crowdin

* Update translates from crowdin

* zh-CN: Optimize media tag translates

* CN & HK: Update translates from crowdin

* Improved 'starter pack' translations

* CN & HK: Improved onboard translations

* zh-CN: Improved translation

* Update translates from crowdin

* zh-HK: Improved translation

* Remove extra spaces before the time

* Update Translates from crowdin

* Update translates from crowdin

* zh-CN: Improved translation

* CN: Fix 'mute for everyone' translates

* Retranslate 'self-labels'

* Retranslate 'self-labels' again

* CN & HK: Improved translation

* CN & HK: Improved translation#2

* CN: Improved translation

* Rollback 'report' translation

* Hot fix

* CN: Clarify label annotations

* TW: Improved translation

* Update translates from crowdin

* Translated email verification dialog & Fix HK typo
2024-11-16 12:05:30 -08:00
Minseo Lee 0f1abfa9d4 Update Korean localization (#6014)
* 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-11-16 12:04:59 -08:00
Takayuki KUSANO 71d5dc0885 Update Japanese translation (#6013)
* Update translations

* Remove obsoleted messages

* Fix the msg pointed out by Hima-Zinn

* Update translation

* Update translation

* Applied suggested fixes by Hima-Zinn

* Translate 1 message and apply middlingphys' suggesstion

* Translate unread related msgs, etc.

* Update translation

* Translate 2 new msgs, remove obsolete msgs

* Translate 1 message

* translate backdated info

* Update translation
2024-11-16 12:04:36 -08:00
Hailey c0fb5245f1 Tweak email verification dialog (#6397) 2024-11-15 17:32:28 -08:00
Samuel Newman a9cacbf265 Better error message (+ translations!) (#6349)
* clean network errors for new common eerors

* translate errors
2024-11-15 22:16:57 +00:00
Hailey 3bd1437133 [Statsig] Remove client downsampling (#6153) 2024-11-15 11:40:43 -08:00
Eric Bailey 400c432283 Reduce calls to getLogs (#6381) 2024-11-15 11:43:21 -06:00
Eric Bailey db39f3e98a Reduce listConvos requests (#6378)
* Reduce page size for request

* Remove refetch interval entirely

* Add comment

* Optimistically mark as read

* Drop default active poll interval to 60s from 5min

* Only optimistically update unread count if success
2024-11-15 11:34:31 -06:00
dan 18aaa19b97 [Lightbox] Make the animation faster (#6382)
* [Lightbox] Make the animation faster

* Make it faster on Android too
2024-11-15 17:32:25 +00:00
Marc Rousavy 05312cce33 feat: Tweak shared element animation to make it much smoother (#6336) 2024-11-14 19:37:41 +00:00
Bartosz Kaszubowski 7d34f6bb5c NewChatDialog: remove extra padding on web (#6333) 2024-11-14 17:56:33 +00:00
Eric Bailey 5f7caf3010 Make ToggleButton full width everywhere (#6302)
* Fix 'Show badge' button label being cut off (#6215)

* Make em full width

---------

Co-authored-by: Khuddite <62555977+khuddite@users.noreply.github.com>
2024-11-13 21:55:19 +00:00
Paul Frazee 9640f08b8d Modify self labels for clarity 2024-11-13 21:54:24 +00:00
Paul Frazee ea0a1c3200 Test fixes 2024-11-13 21:54:24 +00:00
dan 4ca5267b98 [Lightbox] Small tweaks (#6301)
* Tweak lightbox springs

* Speed up dismiss speed

* Don't scroll to top while active lightbox

* Make Android a bit faster
2024-11-13 21:13:07 +00:00
dan 3bab7f7540 Patch RCTScrollView to fix centerContent (#6298) 2024-11-13 16:34:24 +00:00
Samuel Newman 017b7647b1 Add subtle web hover to treeview replies (#6142)
* add subtle web hover to treeview replies

* don't highlight indentation

* adjust position

* clean up some styles
2024-11-13 14:25:56 +00:00
Khuddite aa55798e55 Upgrade react-native-drawer-layout version to v4.0.1 (#6274) 2024-11-12 23:47:28 +00:00
Samuel Newman 6471e809aa Add backdated post indicator (#6156)
* add backdate indicator

* pill style

* add indexedAt

* update indicator - new copy, date in pill

* complete alf migration

* accidentally committed the missing indexedAt *again*!

* copy tweak
2024-11-12 21:02:41 +00:00
Samuel Newman f2916ce528 Slightly reduce rerenders in drawer (#6270)
* slightly reduce renders in drawer

* fix missing hook
2024-11-12 20:44:13 +00:00
307 changed files with 49124 additions and 34571 deletions
+4 -5
View File
@@ -1,8 +1,9 @@
module.exports = {
root: true,
extends: [
'@react-native-community',
'@react-native',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-native-a11y/ios',
'prettier',
],
@@ -16,9 +17,8 @@ module.exports = {
'eslint-plugin-react-compiler',
],
rules: {
// Temporary until https://github.com/facebook/react-native/pull/43756 gets into a release.
'prettier/prettier': 0,
'react/no-unescaped-entities': 0,
'react/prop-types': 0,
'react-native/no-inline-styles': 0,
'bsky-internal/avoid-unwrapped-text': [
'error',
@@ -79,8 +79,7 @@ module.exports = {
},
],
'simple-import-sort/exports': 'error',
// TODO: Reenable when we figure out why it gets stuck on CI.
// 'react-compiler/react-compiler': 'error',
'react-compiler/react-compiler': 'warn',
'no-restricted-imports': [
'error',
{
+2 -2
View File
@@ -17,8 +17,8 @@ appId: xyz.blueskyweb.app
- inputText: "Post with an image"
- tapOn:
id: "openGalleryBtn"
- tapOn:
id: "labelsBtn"
- tapOn: "Content warnings"
- tapOn: "Content warnings"
- tapOn: "Porn"
- tapOn:
label: "Tap on confirm"
-1
View File
@@ -29,7 +29,6 @@ appId: xyz.blueskyweb.app
- pressKey: Enter
- tapOn:
id: "nextBtn"
- tapOn: "Not now"
- tapOn:
id: "handleInput"
- inputText: "e2e-test"
+4
View File
@@ -23,6 +23,7 @@ appId: xyz.blueskyweb.app
id: "editDescriptionInput"
- inputText: "They good"
- tapOn: "Save"
- tapOn: "Save"
- assertNotVisible:
id: "createOrEditListModal"
- tapOn: "About"
@@ -45,6 +46,7 @@ appId: xyz.blueskyweb.app
- eraseText
- inputText: "They bad"
- tapOn: "Save"
- tapOn: "Save"
- assertNotVisible:
id: "createOrEditListModal"
- assertVisible: Bad Ppl
@@ -60,6 +62,7 @@ appId: xyz.blueskyweb.app
id: "editDescriptionInput"
- eraseText
- tapOn: "Save"
- tapOn: "Save"
- assertNotVisible:
id: "createOrEditListModal"
- assertNotVisible:
@@ -87,6 +90,7 @@ appId: xyz.blueskyweb.app
id: "editDescriptionInput"
- inputText: "They good"
- tapOn: "Save"
- tapOn: "Save"
- assertNotVisible:
id: "createOrEditListModal"
- tapOn: "About"
-1
View File
@@ -23,5 +23,4 @@ appId: xyz.blueskyweb.app
id: "loginPasswordInput"
- inputText: "hunter2"
- pressKey: Enter
- tapOn: "Not now"
- assertVisible: "Following"
+1
View File
@@ -22,6 +22,7 @@ appId: xyz.blueskyweb.app
id: "editDescriptionInput"
- inputText: "Shhh"
- tapOn: "Save"
- tapOn: "Save"
# view modlist
- assertVisible: "Muted Users"
+1 -1
View File
@@ -13,7 +13,7 @@ appId: xyz.blueskyweb.app
- tapOn: "Select an avatar"
- waitForAnimationToEnd
- tapOn:
point: "16%,22%"
point: "50%,22%"
- waitForAnimationToEnd
- tapOn: "Choose"
- waitForAnimationToEnd
+25
View File
@@ -92,6 +92,31 @@ module.exports = function (config) {
'Used to save images to your library.',
NSPhotoLibraryUsageDescription:
'Used for profile pictures, posts, and other kinds of content',
CFBundleSpokenName: 'Blue Sky',
CFBundleLocalizations: [
'en',
'ca',
'de',
'es',
'fi',
'fr',
'ga',
'hi',
'hu',
'id',
'it',
'ja',
'ko',
'pl',
'pt',
'ru',
'th',
'tr',
'uk',
'zh_CN',
'zh_HK',
'zh_TW',
],
},
associatedDomains: ASSOCIATED_DOMAINS,
splash: {
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M15.439 3.148a1 1 0 0 1 .41.645l.568 3.22a7 7 0 1 1-6.174 10.97L4.32 19.027a1 1 0 0 1-1.159-.811L1.078 6.398a1 1 0 0 1 .81-1.158l12.803-2.258a1 1 0 0 1 .748.166ZM9.325 16.114A7 7 0 0 1 9 14c0-1.56.51-3 1.372-4.164l-6.456 1.139 1.041 5.909 4.368-.77ZM3.568 9.005l10.833-1.91-.347-1.97L3.22 7.036l.347 1.97ZM16 9a5 5 0 1 0 0 10 5 5 0 0 0 0-10Zm0 2a1 1 0 0 1 1 1v1.586l1.374 1.374a1 1 0 0 1-1.414 1.414l-1.667-1.667A1 1 0 0 1 15 14v-2a1 1 0 0 1 1-1Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 589 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M18.535 5.465a5.003 5.003 0 0 0-7.076 0l-.005.005-.752.742a1 1 0 1 1-1.404-1.424l.749-.74a7.003 7.003 0 0 1 9.904 9.905l-.002.003-.737.746a1 1 0 1 1-1.424-1.404l.747-.757a5.003 5.003 0 0 0 0-7.076ZM6.202 9.288a1 1 0 0 1 .01 1.414l-.747.757a5.003 5.003 0 1 0 7.076 7.076l.005-.005.752-.742a1 1 0 1 1 1.404 1.424l-.746.737-.003.002a7.003 7.003 0 0 1-9.904-9.904l.74-.75a1 1 0 0 1 1.413-.009Zm8.505.005a1 1 0 0 1 0 1.414l-4 4a1 1 0 0 1-1.414-1.414l4-4a1 1 0 0 1 1.414 0Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 610 B

+1 -6
View File
@@ -17,12 +17,7 @@ module.exports = function (api) {
],
plugins: [
'macros',
[
'babel-plugin-react-compiler',
{
runtimeModule: 'react-compiler-runtime',
},
],
['babel-plugin-react-compiler', {target: '18'}],
[
'module:react-native-dotenv',
{
+2 -3
View File
@@ -9,7 +9,6 @@ import {
AppBskyGraphDefs,
AppBskyGraphStarterpack,
AppBskyLabelerDefs,
AtUri,
} from '@atproto/api'
import {ComponentChildren, h} from 'preact'
import {useMemo} from 'preact/hooks'
@@ -437,14 +436,14 @@ function StarterPackEmbed({
// from #/lib/strings/starter-pack.ts
function getStarterPackImage(starterPack: AppBskyGraphDefs.StarterPackView) {
const rkey = new AtUri(starterPack.uri).rkey
const rkey = getRkey({uri: starterPack.uri})
return `https://ogcard.cdn.bsky.app/start/${starterPack.creator.did}/${rkey}`
}
function getStarterPackHref(
starterPack: AppBskyGraphDefs.StarterPackViewBasic,
) {
const rkey = new AtUri(starterPack.uri).rkey
const rkey = getRkey({uri: starterPack.uri})
const handleOrDid = starterPack.creator.handle || starterPack.creator.did
return `/starter-pack/${handleOrDid}/${rkey}`
}
+1 -6
View File
@@ -4209,12 +4209,7 @@ yocto-queue@^0.1.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
zod@^3.21.4:
version "3.22.4"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff"
integrity sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==
zod@^3.23.8:
zod@^3.21.4, zod@^3.23.8:
version "3.23.8"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d"
integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==
-21
View File
@@ -1,21 +0,0 @@
const React = require('react')
const $empty = Symbol.for('react.memo_cache_sentinel')
/**
* DANGER: this hook is NEVER meant to be called directly!
*
* Note that this is a temporary userspace implementation of this function
* from React 19. It is not as efficient and may invalidate more frequently
* than the official API. Please upgrade to React 19 as soon as you can.
**/
export function c(size) {
// eslint-disable-next-line react-hooks/rules-of-hooks
return React.useState(() => {
const $ = new Array(size)
for (let ii = 0; ii < size; ii++) {
$[ii] = $empty
}
// @ts-ignore
$[$empty] = true
return $
})[0]
}
-9
View File
@@ -1,9 +0,0 @@
{
"name": "react-compiler-runtime",
"version": "0.0.1",
"license": "MIT",
"main": "index.js",
"peerDependencies": {
"react": "^18.2.0"
}
}
+1
View File
@@ -15,6 +15,7 @@ module.exports = {
'it',
'ja',
'ko',
'pl',
'pt-BR',
'ru',
'th',
+16 -1
View File
@@ -9,6 +9,11 @@ cfg.resolver.sourceExts = process.env.RN_SRC_EXT
if (cfg.resolver.resolveRequest) {
throw Error('Update this override because it is conflicting now.')
}
if (process.env.BSKY_PROFILE) {
cfg.cacheVersion += ':PROFILE'
}
cfg.resolver.resolveRequest = (context, moduleName, platform) => {
// HACK: manually resolve a few packages that use `exports` in `package.json`.
// A proper solution is to enable `unstable_enablePackageExports` but this needs careful testing.
@@ -29,6 +34,15 @@ cfg.resolver.resolveRequest = (context, moduleName, platform) => {
if (moduleName === '@ipld/dag-cbor') {
return context.resolveRequest(context, '@ipld/dag-cbor/src', platform)
}
if (process.env.BSKY_PROFILE) {
if (moduleName.endsWith('ReactNativeRenderer-prod')) {
return context.resolveRequest(
context,
moduleName.replace('-prod', '-profiling'),
platform,
)
}
}
return context.resolveRequest(context, moduleName, platform)
}
@@ -38,9 +52,10 @@ cfg.transformer.getTransformOptions = async () => ({
inlineRequires: true,
nonInlinedRequires: [
// We can remove this option and rely on the default after
// https://github.com/facebook/metro/pull/1126 is released.
// https://github.com/facebook/metro/pull/1390 is released.
'React',
'react',
'react-compiler-runtime',
'react/jsx-dev-runtime',
'react/jsx-runtime',
'react-native',
+14 -31
View File
@@ -1,6 +1,6 @@
{
"name": "bsky.app",
"version": "1.94.0",
"version": "1.95.0",
"private": true,
"engines": {
"node": ">=20"
@@ -11,6 +11,8 @@
"postinstall": "patch-package && yarn intl:compile",
"prebuild": "expo prebuild --clean",
"android": "expo run:android",
"android:prod": "expo run:android --variant release",
"android:profile": "BSKY_PROFILE=1 expo run:android --variant release",
"ios": "expo run:ios",
"web": "expo start --web",
"use-build-number": "./scripts/useBuildNumberEnv.sh",
@@ -23,7 +25,6 @@
"build-embed": "cd bskyembed && yarn build && yarn build-snippet && cd .. && node ./scripts/post-embed-build.js",
"start": "expo start --dev-client",
"start:prod": "expo start --dev-client --no-dev --minify",
"clean-cache": "rm -rf node_modules/.cache/babel-loader/*",
"test": "NODE_ENV=test jest --forceExit --testTimeout=20000 --bail",
"test-watch": "NODE_ENV=test jest --watchAll",
"test-ci": "NODE_ENV=test jest --ci --forceExit --reporters=default --reporters=jest-junit",
@@ -140,6 +141,7 @@
"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",
@@ -152,11 +154,6 @@
"lodash.chunk": "^4.2.0",
"lodash.debounce": "^4.0.8",
"lodash.isequal": "^4.5.0",
"lodash.omit": "^4.5.0",
"lodash.once": "^4.1.1",
"lodash.random": "^3.2.0",
"lodash.samplesize": "^4.2.0",
"lodash.set": "^4.3.2",
"lodash.shuffle": "^4.2.0",
"lodash.throttle": "^4.1.1",
"multiformats": "^13.1.0",
@@ -166,14 +163,14 @@
"postinstall-postinstall": "^2.1.0",
"psl": "^1.9.0",
"react": "18.2.0",
"react-compiler-runtime": "file:./lib/react-compiler-runtime",
"react-compiler-runtime": "19.0.0-beta-a7bf2bd-20241110",
"react-dom": "^18.2.0",
"react-image-crop": "^11.0.7",
"react-keyed-flatten-children": "^3.0.0",
"react-native": "0.74.1",
"react-native-compressor": "^1.8.24",
"react-native-date-picker": "^4.4.2",
"react-native-drawer-layout": "^4.0.0-alpha.3",
"react-native-drawer-layout": "^4.0.1",
"react-native-gesture-handler": "2.20.0",
"react-native-get-random-values": "~1.11.0",
"react-native-image-crop-picker": "0.41.2",
@@ -217,44 +214,35 @@
"@lingui/cli": "^4.5.0",
"@lingui/macro": "^4.5.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.11",
"@react-native-community/eslint-config": "^3.0.0",
"@react-native/eslint-config": "^0.76.2",
"@react-native/typescript-config": "^0.74.1",
"@testing-library/jest-native": "^5.4.1",
"@testing-library/react-native": "^11.5.2",
"@tsconfig/react-native": "^2.0.3",
"@types/he": "^1.1.2",
"@types/jest": "^29.4.0",
"@types/lodash.chunk": "^4.2.7",
"@types/lodash.debounce": "^4.0.7",
"@types/lodash.isequal": "^4.5.6",
"@types/lodash.omit": "^4.5.7",
"@types/lodash.once": "^4.1.7",
"@types/lodash.random": "^3.2.7",
"@types/lodash.samplesize": "^4.2.7",
"@types/lodash.set": "^4.3.7",
"@types/lodash.shuffle": "^4.2.7",
"@types/psl": "^1.1.1",
"@types/react-dom": "^18.2.18",
"@types/react-responsive": "^8.0.5",
"@types/react-test-renderer": "^17.0.1",
"@typescript-eslint/eslint-plugin": "^5.48.2",
"@typescript-eslint/parser": "^5.48.2",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"babel-jest": "^29.7.0",
"babel-loader": "^9.1.2",
"babel-plugin-macros": "^3.1.0",
"babel-plugin-module-resolver": "^5.0.0",
"babel-plugin-react-compiler": "^0.0.0-experimental-592953e-20240517",
"babel-plugin-react-native-web": "^0.18.12",
"babel-plugin-react-compiler": "19.0.0-beta-a7bf2bd-20241110",
"babel-preset-expo": "^10.0.0",
"eslint": "^8.19.0",
"eslint-plugin-bsky-internal": "link:./eslint",
"eslint-plugin-ft-flow": "^2.0.3",
"eslint-plugin-lingui": "^0.2.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-compiler": "^0.0.0-experimental-c8b3f72-20240517",
"eslint-plugin-react-compiler": "19.0.0-beta-a7bf2bd-20241110",
"eslint-plugin-react-native-a11y": "^3.3.0",
"eslint-plugin-simple-import-sort": "^12.0.0",
"html-webpack-plugin": "^5.5.0",
"file-loader": "6.2.0",
"husky": "^8.0.3",
"is-ci": "^3.0.1",
"jest": "^29.7.0",
@@ -265,16 +253,11 @@
"prettier": "^2.8.3",
"react-native-dotenv": "^3.3.1",
"react-refresh": "^0.14.0",
"react-scripts": "^5.0.1",
"react-test-renderer": "18.2.0",
"svgo": "^3.3.2",
"ts-node": "^10.9.1",
"typescript": "^5.5.4",
"url-loader": "^4.1.1",
"webpack": "^5.75.0",
"webpack-bundle-analyzer": "^4.10.1",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.11.1"
"typescript": "^5.7.2",
"webpack-bundle-analyzer": "^4.10.1"
},
"resolutions": {
"@types/react": "^18",
+125 -12
View File
@@ -9,7 +9,7 @@ index caa5540..c5d4e67 100644
- type != nil && [type length] > 0 ? type : @"application/octet-stream",
+ ![type isEqual:[NSNull null]] && [type length] > 0 ? type : @"application/octet-stream",
[data base64EncodedStringWithOptions:0]];
resolve(text);
diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm
index b0d71dc..41b9a0e 100644
@@ -18,7 +18,7 @@ index b0d71dc..41b9a0e 100644
@@ -377,10 +377,6 @@ - (void)textInputDidBeginEditing
self.backedTextInputView.attributedText = [NSAttributedString new];
}
- if (_selectTextOnFocus) {
- [self.backedTextInputView selectAll:nil];
- }
@@ -35,7 +35,7 @@ index b0d71dc..41b9a0e 100644
+ [self.backedTextInputView selectAll:nil];
+ }
}
- (void)reactBlur
diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h
index e9b330f..ec5f58c 100644
@@ -48,10 +48,10 @@ index e9b330f..ec5f58c 100644
+@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..288e60c 100644
index b09e653..d2b4e05 100644
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
+++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
@@ -22,6 +22,7 @@ @implementation RCTRefreshControl {
@@ -60,12 +60,12 @@ index b09e653..288e60c 100644
CGFloat _progressViewOffset;
+ UIColor *_customTintColor;
}
- (instancetype)init
@@ -56,6 +57,12 @@ - (void)layoutSubviews
_isInitialRender = false;
}
+- (void)didMoveToSuperview
+{
+ [super didMoveToSuperview];
@@ -78,7 +78,7 @@ index b09e653..288e60c 100644
@@ -203,4 +210,58 @@ - (void)refreshControlValueChanged
}
}
+- (void)setCustomTintColor:(UIColor *)customTintColor
+{
+ _customTintColor = customTintColor;
@@ -139,26 +139,139 @@ 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/React/Views/ScrollView/RCTScrollView.m b/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m
index 1aead51..39e6244 100644
--- a/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m
+++ b/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m
@@ -159,31 +159,6 @@ - (BOOL)touchesShouldCancelInContentView:(__unused UIView *)view
return !shouldDisableScrollInteraction;
}
-/*
- * Automatically centers the content such that if the content is smaller than the
- * ScrollView, we force it to be centered, but when you zoom or the content otherwise
- * becomes larger than the ScrollView, there is no padding around the content but it
- * can still fill the whole view.
- */
-- (void)setContentOffset:(CGPoint)contentOffset
-{
- UIView *contentView = [self contentView];
- if (contentView && _centerContent && !CGSizeEqualToSize(contentView.frame.size, CGSizeZero)) {
- CGSize subviewSize = contentView.frame.size;
- CGSize scrollViewSize = self.bounds.size;
- if (subviewSize.width <= scrollViewSize.width) {
- contentOffset.x = -(scrollViewSize.width - subviewSize.width) / 2.0;
- }
- if (subviewSize.height <= scrollViewSize.height) {
- contentOffset.y = -(scrollViewSize.height - subviewSize.height) / 2.0;
- }
- }
-
- super.contentOffset = CGPointMake(
- RCTSanitizeNaNValue(contentOffset.x, @"scrollView.contentOffset.x"),
- RCTSanitizeNaNValue(contentOffset.y, @"scrollView.contentOffset.y"));
-}
-
- (void)setFrame:(CGRect)frame
{
// Preserving and revalidating `contentOffset`.
@@ -427,6 +402,11 @@ - (void)setRemoveClippedSubviews:(__unused BOOL)removeClippedSubviews
// Does nothing
}
+- (void)setFrame:(CGRect)frame {
+ [super setFrame:frame];
+ [self centerContentIfNeeded];
+}
+
- (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex
{
[super insertReactSubview:view atIndex:atIndex];
@@ -444,6 +424,8 @@ - (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex
RCTApplyTransformationAccordingLayoutDirection(_contentView, self.reactLayoutDirection);
[_scrollView addSubview:view];
}
+
+ [self centerContentIfNeeded];
}
- (void)removeReactSubview:(UIView *)subview
@@ -652,9 +634,46 @@ -(void)delegateMethod : (UIScrollView *)scrollView \
}
RCT_SCROLL_EVENT_HANDLER(scrollViewWillBeginDecelerating, onMomentumScrollBegin)
-RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, onScroll)
RCT_SCROLL_EVENT_HANDLER(scrollViewDidScrollToTop, onScrollToTop)
+-(void)scrollViewDidZoom : (UIScrollView *)scrollView
+{
+ [self centerContentIfNeeded];
+
+ RCT_SEND_SCROLL_EVENT(onScroll, nil);
+ RCT_FORWARD_SCROLL_EVENT(scrollViewDidZoom : scrollView);
+}
+
+/*
+ * Automatically centers the content such that if the content is smaller than the
+ * ScrollView, we force it to be centered, but when you zoom or the content otherwise
+ * becomes larger than the ScrollView, there is no padding around the content but it
+ * can still fill the whole view.
+ *
+ * PATCHED: This deviates from the original React Native implementation to fix two issues:
+ *
+ * - The scroll view was swallowing any taps immediately after pinching
+ * - The scroll view was jittering when crossing the full screen threshold while pinching
+ *
+ * This implementation is based on https://petersteinberger.com/blog/2013/how-to-center-uiscrollview/.
+ */
+-(void)centerContentIfNeeded
+{
+ if (_scrollView.centerContent &&
+ !CGSizeEqualToSize(self.contentSize, CGSizeZero) &&
+ !CGSizeEqualToSize(self.bounds.size, CGSizeZero)
+ ) {
+ CGFloat top = 0, left = 0;
+ if (self.contentSize.width < self.bounds.size.width) {
+ left = (self.bounds.size.width - self.contentSize.width) * 0.5f;
+ }
+ if (self.contentSize.height < self.bounds.size.height) {
+ top = (self.bounds.size.height - self.contentSize.height) * 0.5f;
+ }
+ _scrollView.contentInset = UIEdgeInsetsMake(top, left, top, left);
+ }
+}
+
- (void)addScrollListener:(NSObject<UIScrollViewDelegate> *)scrollListener
{
[_scrollListeners addObject:scrollListener];
@@ -913,6 +932,7 @@ - (void)updateContentSizeIfNeeded
CGSize contentSize = self.contentSize;
if (!CGSizeEqualToSize(_scrollView.contentSize, contentSize)) {
_scrollView.contentSize = contentSize;
+ [self centerContentIfNeeded];
}
}
diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java
index 5f5e1ab..aac00b6 100644
--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java
+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java
@@ -99,8 +99,9 @@ public class JavaTimerManager {
}
// If the JS thread is busy for multiple frames we cancel any other pending runnable.
- if (mCurrentIdleCallbackRunnable != null) {
- mCurrentIdleCallbackRunnable.cancel();
@@ -166,5 +279,5 @@ index 5f5e1ab..aac00b6 100644
+ if (currentRunnable != null) {
+ currentRunnable.cancel();
}
mCurrentIdleCallbackRunnable = new IdleCallbackRunnable(frameTimeNanos);
+57
View File
@@ -0,0 +1,57 @@
diff --git a/node_modules/react-native-svg/android/src/main/java/com/horcrux/svg/PathView.java b/node_modules/react-native-svg/android/src/main/java/com/horcrux/svg/PathView.java
index 06829bd..1b15818 100644
--- a/node_modules/react-native-svg/android/src/main/java/com/horcrux/svg/PathView.java
+++ b/node_modules/react-native-svg/android/src/main/java/com/horcrux/svg/PathView.java
@@ -14,17 +14,33 @@ import android.graphics.Paint;
import android.graphics.Path;
import com.facebook.react.bridge.ReactContext;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+class ParsedPath {
+ final Path path;
+ final ArrayList<PathElement> elements;
+
+ ParsedPath(Path path, ArrayList<PathElement> elements) {
+ this.path = path;
+ this.elements = elements;
+ }
+}
+
@SuppressLint("ViewConstructor")
class PathView extends RenderableView {
private Path mPath;
+ // This grows forever but for our use case (static icons) it's ok.
+ private static final HashMap<String, ParsedPath> sPathCache = new HashMap<>();
+
public PathView(ReactContext reactContext) {
super(reactContext);
PathParser.mScale = mScale;
mPath = new Path();
}
- public void setD(String d) {
+ void setDByParsing(String d) {
mPath = PathParser.parse(d);
elements = PathParser.elements;
for (PathElement elem : elements) {
@@ -33,6 +49,17 @@ class PathView extends RenderableView {
point.y *= mScale;
}
}
+ }
+
+ public void setD(String d) {
+ ParsedPath cached = sPathCache.get(d);
+ if (cached != null) {
+ mPath = cached.path;
+ elements = cached.elements;
+ } else {
+ setDByParsing(d);
+ sPathCache.put(d, new ParsedPath(mPath, elements));
+ }
invalidate();
}
+37 -34
View File
@@ -69,6 +69,7 @@ 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'
import {AppProfiler} from './AppProfiler'
SplashScreen.preventAutoHideAsync()
@@ -184,40 +185,42 @@ function App() {
* that is set up in the InnerApp component above.
*/
return (
<GeolocationProvider>
<A11yProvider>
<KeyboardProvider enabled={false} statusBarTranslucent={true}>
<SessionProvider>
<PrefsStateProvider>
<I18nProvider>
<ShellStateProvider>
<InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<PortalProvider>
<BottomSheetProvider>
<StarterPackProvider>
<SafeAreaProvider
initialMetrics={initialWindowMetrics}>
<IntentDialogProvider>
<InnerApp />
</IntentDialogProvider>
</SafeAreaProvider>
</StarterPackProvider>
</BottomSheetProvider>
</PortalProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</ShellStateProvider>
</I18nProvider>
</PrefsStateProvider>
</SessionProvider>
</KeyboardProvider>
</A11yProvider>
</GeolocationProvider>
<AppProfiler>
<GeolocationProvider>
<A11yProvider>
<KeyboardProvider enabled={false} statusBarTranslucent={true}>
<SessionProvider>
<PrefsStateProvider>
<I18nProvider>
<ShellStateProvider>
<InvitesStateProvider>
<ModalStateProvider>
<DialogStateProvider>
<LightboxStateProvider>
<PortalProvider>
<BottomSheetProvider>
<StarterPackProvider>
<SafeAreaProvider
initialMetrics={initialWindowMetrics}>
<IntentDialogProvider>
<InnerApp />
</IntentDialogProvider>
</SafeAreaProvider>
</StarterPackProvider>
</BottomSheetProvider>
</PortalProvider>
</LightboxStateProvider>
</DialogStateProvider>
</ModalStateProvider>
</InvitesStateProvider>
</ShellStateProvider>
</I18nProvider>
</PrefsStateProvider>
</SessionProvider>
</KeyboardProvider>
</A11yProvider>
</GeolocationProvider>
</AppProfiler>
)
}
+29
View File
@@ -0,0 +1,29 @@
import React, {Profiler} from 'react'
// Don't let it get stripped out in profiling builds (which apply production Babel preset).
const log = (global as any)['con' + 'sole'].log
function onRender(id: string, phase: string, actualDuration: number) {
if (!__DEV__) {
// This block of code will exist in the production build of the app.
// However, only profiling builds of React call `onRender` so it's dead code in actual production.
const message = `<Profiler> ${id}:${phase} ${
actualDuration > 500
? '(╯°□°)╯ '
: actualDuration > 100
? '[!!] '
: actualDuration > 16
? '[!] '
: ''
}${Math.round(actualDuration)}ms`
log(message)
}
}
export function AppProfiler({children}: {children: React.ReactNode}) {
return (
<Profiler id="app" onRender={onRender}>
{children}
</Profiler>
)
}
+2 -8
View File
@@ -32,7 +32,7 @@ import {
import {RouteParams, State} from '#/lib/routes/types'
import {attachRouteToLogEvents, logEvent} from '#/lib/statsig/statsig'
import {bskyTitle} from '#/lib/strings/headings'
import {isAndroid, isNative, isWeb} from '#/platform/detection'
import {isNative, isWeb} from '#/platform/detection'
import {useModalControls} from '#/state/modals'
import {useUnreadNotifications} from '#/state/queries/notifications/unread'
import {useSession} from '#/state/session'
@@ -453,7 +453,6 @@ function HomeTabNavigator() {
return (
<HomeTab.Navigator
screenOptions={{
animation: isAndroid ? 'ios' : undefined,
animationDuration: 285,
gestureEnabled: true,
fullScreenGestureEnabled: true,
@@ -472,7 +471,6 @@ function SearchTabNavigator() {
return (
<SearchTab.Navigator
screenOptions={{
animation: isAndroid ? 'ios' : undefined,
animationDuration: 285,
gestureEnabled: true,
fullScreenGestureEnabled: true,
@@ -490,7 +488,6 @@ function NotificationsTabNavigator() {
return (
<NotificationsTab.Navigator
screenOptions={{
animation: isAndroid ? 'ios' : undefined,
animationDuration: 285,
gestureEnabled: true,
fullScreenGestureEnabled: true,
@@ -512,7 +509,6 @@ function MyProfileTabNavigator() {
return (
<MyProfileTab.Navigator
screenOptions={{
animation: isAndroid ? 'ios' : undefined,
animationDuration: 285,
gestureEnabled: true,
fullScreenGestureEnabled: true,
@@ -538,7 +534,6 @@ function MessagesTabNavigator() {
return (
<MessagesTab.Navigator
screenOptions={{
animation: isAndroid ? 'ios' : undefined,
animationDuration: 285,
gestureEnabled: true,
fullScreenGestureEnabled: true,
@@ -572,7 +567,6 @@ const FlatNavigator = () => {
<Flat.Navigator
screenListeners={screenListeners}
screenOptions={{
animation: isAndroid ? 'ios' : undefined,
animationDuration: 285,
gestureEnabled: true,
fullScreenGestureEnabled: true,
@@ -696,7 +690,7 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
onStateChange={() => {
const routeName = getCurrentRouteName()
if (routeName === 'Notifications') {
logEvent('router:navigate:notifications:sampled', {})
logEvent('router:navigate:notifications', {})
}
}}
onReady={() => {
+40 -32
View File
@@ -81,40 +81,40 @@ export function Splash(props: React.PropsWithChildren<Props>) {
return {
transform: [
{
scale: interpolate(intro.value, [0, 1], [0.8, 1], 'clamp'),
scale: interpolate(intro.get(), [0, 1], [0.8, 1], 'clamp'),
},
{
scale: interpolate(
outroLogo.value,
outroLogo.get(),
[0, 0.08, 1],
[1, 0.8, 500],
'clamp',
),
},
],
opacity: interpolate(intro.value, [0, 1], [0, 1], 'clamp'),
opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'),
}
})
const bottomLogoAnimation = useAnimatedStyle(() => {
return {
opacity: interpolate(intro.value, [0, 1], [0, 1], 'clamp'),
opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'),
}
})
const reducedLogoAnimation = useAnimatedStyle(() => {
return {
transform: [
{
scale: interpolate(intro.value, [0, 1], [0.8, 1], 'clamp'),
scale: interpolate(intro.get(), [0, 1], [0.8, 1], 'clamp'),
},
],
opacity: interpolate(intro.value, [0, 1], [0, 1], 'clamp'),
opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'),
}
})
const logoWrapperAnimation = useAnimatedStyle(() => {
return {
opacity: interpolate(
outroAppOpacity.value,
outroAppOpacity.get(),
[0, 0.1, 0.2, 1],
[1, 1, 0, 0],
'clamp',
@@ -126,11 +126,11 @@ export function Splash(props: React.PropsWithChildren<Props>) {
return {
transform: [
{
scale: interpolate(outroApp.value, [0, 1], [1.1, 1], 'clamp'),
scale: interpolate(outroApp.get(), [0, 1], [1.1, 1], 'clamp'),
},
],
opacity: interpolate(
outroAppOpacity.value,
outroAppOpacity.get(),
[0, 0.1, 0.2, 1],
[0, 0, 1, 1],
'clamp',
@@ -146,29 +146,37 @@ export function Splash(props: React.PropsWithChildren<Props>) {
if (isReady) {
SplashScreen.hideAsync()
.then(() => {
intro.value = withTiming(
1,
{duration: 400, easing: Easing.out(Easing.cubic)},
async () => {
// set these values to check animation at specific point
// outroLogo.value = 0.1
// outroApp.value = 0.1
outroLogo.value = withTiming(
1,
{duration: 1200, easing: Easing.in(Easing.cubic)},
() => {
runOnJS(onFinish)()
},
)
outroApp.value = withTiming(1, {
duration: 1200,
easing: Easing.inOut(Easing.cubic),
})
outroAppOpacity.value = withTiming(1, {
duration: 1200,
easing: Easing.in(Easing.cubic),
})
},
intro.set(() =>
withTiming(
1,
{duration: 400, easing: Easing.out(Easing.cubic)},
async () => {
// set these values to check animation at specific point
// outroLogo.set(0.1)
// outroApp.set(0.1)
outroLogo.set(() =>
withTiming(
1,
{duration: 1200, easing: Easing.in(Easing.cubic)},
() => {
runOnJS(onFinish)()
},
),
)
outroApp.set(() =>
withTiming(1, {
duration: 1200,
easing: Easing.inOut(Easing.cubic),
}),
)
outroAppOpacity.set(() =>
withTiming(1, {
duration: 1200,
easing: Easing.in(Easing.cubic),
}),
)
},
),
)
})
.catch(() => {})
+126
View File
@@ -0,0 +1,126 @@
import React, {Children} from 'react'
import {TextProps as RNTextProps} from 'react-native'
import {StyleProp, TextStyle} from 'react-native'
import {UITextView} from 'react-native-uitextview'
import createEmojiRegex from 'emoji-regex'
import {isNative} from '#/platform/detection'
import {isIOS} from '#/platform/detection'
import {Alf, applyFonts, atoms, flatten} from '#/alf'
/**
* Util to calculate lineHeight from a text size atom and a leading atom
*
* Example:
* `leading(atoms.text_md, atoms.leading_normal)` // => 24
*/
export function leading<
Size extends {fontSize?: number},
Leading extends {lineHeight?: number},
>(textSize: Size, leading: Leading) {
const size = textSize?.fontSize || atoms.text_md.fontSize
const lineHeight = leading?.lineHeight || atoms.leading_normal.lineHeight
return Math.round(size * lineHeight)
}
/**
* Ensures that `lineHeight` defaults to a relative value of `1`, or applies
* other relative leading atoms.
*
* If the `lineHeight` value is > 2, we assume it's an absolute value and
* returns it as-is.
*/
export function normalizeTextStyles(
styles: StyleProp<TextStyle>,
{
fontScale,
fontFamily,
}: {
fontScale: number
fontFamily: Alf['fonts']['family']
} & Pick<Alf, 'flags'>,
) {
const s = flatten(styles)
// should always be defined on these components
s.fontSize = (s.fontSize || atoms.text_md.fontSize) * fontScale
if (s?.lineHeight) {
if (s.lineHeight !== 0 && s.lineHeight <= 2) {
s.lineHeight = Math.round(s.fontSize * s.lineHeight)
}
} else if (!isNative) {
s.lineHeight = s.fontSize
}
applyFonts(s, fontFamily)
return s
}
export type StringChild = string | (string | null)[]
export type TextProps = RNTextProps & {
/**
* Lets the user select text, to use the native copy and paste functionality.
*/
selectable?: boolean
/**
* Provides `data-*` attributes to the underlying `UITextView` component on
* web only.
*/
dataSet?: Record<string, string | number | undefined>
/**
* Appears as a small tooltip on web hover.
*/
title?: string
/**
* Whether the children could possibly contain emoji.
*/
emoji?: boolean
}
const EMOJI = createEmojiRegex()
export function childHasEmoji(children: React.ReactNode) {
let hasEmoji = false
Children.forEach(children, child => {
if (typeof child === 'string' && createEmojiRegex().test(child)) {
hasEmoji = true
}
})
return hasEmoji
}
export function renderChildrenWithEmoji(
children: React.ReactNode,
props: Omit<TextProps, 'children'> = {},
emoji: boolean,
) {
if (!isIOS || !emoji) {
return children
}
return Children.map(children, child => {
if (typeof child !== 'string') return child
const emojis = child.match(EMOJI)
if (emojis === null) {
return child
}
return child.split(EMOJI).map((stringPart, index) => [
stringPart,
emojis[index] ? (
<UITextView
{...props}
style={[props?.style, {color: 'black', fontFamily: 'System'}]}>
{emojis[index]}
</UITextView>
) : null,
])
})
}
const SINGLE_EMOJI_RE = /^[\p{Emoji_Presentation}\p{Extended_Pictographic}]+$/u
export function isOnlyEmoji(text: string) {
return text.length <= 15 && SINGLE_EMOJI_RE.test(text)
}
-1
View File
@@ -1,4 +1,3 @@
import React from 'react'
import {View} from 'react-native'
import {moderateProfile} from '@atproto/api'
-1
View File
@@ -458,7 +458,6 @@ export const Button = React.forwardRef<View, ButtonProps>(
// @ts-ignore - this will always be a pressable
ref={ref}
aria-label={label}
aria-pressed={state.pressed}
accessibilityLabel={label}
disabled={disabled || false}
accessibilityState={{
+1 -1
View File
@@ -75,7 +75,7 @@ export function Outer({
try {
cb()
} catch (e: any) {
logger.error('Error running close callback', e)
logger.error(e || 'Error running close callback')
}
}
+3 -5
View File
@@ -1,7 +1,7 @@
import React from 'react'
import {StyleProp, TextStyle, View, ViewStyle} from 'react-native'
import {atoms as a, useTheme, web} from '#/alf'
import {atoms as a, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
export function Header({
@@ -29,10 +29,8 @@ export function Header({
a.border_b,
t.atoms.border_contrast_medium,
t.atoms.bg,
web([
{borderRadiusTopLeft: a.rounded_md.borderRadius},
{borderRadiusTopRight: a.rounded_md.borderRadius},
]),
{borderTopLeftRadius: a.rounded_md.borderRadius},
{borderTopRightRadius: a.rounded_md.borderRadius},
style,
]}>
{renderLeft && (
-1
View File
@@ -1,4 +1,3 @@
import React from 'react'
import {View} from 'react-native'
import {atoms as a, flatten, useTheme, ViewStyleProp} from '#/alf'
-1
View File
@@ -1,4 +1,3 @@
import React from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-1
View File
@@ -1,4 +1,3 @@
import React from 'react'
import {LinearGradient} from 'expo-linear-gradient'
import {atoms as a, tokens} from '#/alf'
-1
View File
@@ -1,4 +1,3 @@
import React from 'react'
import {View} from 'react-native'
import {
+1 -1
View File
@@ -1,4 +1,4 @@
import React, {useCallback, useMemo} from 'react'
import {useCallback, useMemo} from 'react'
import {ActivityIndicator, FlatList, View} from 'react-native'
import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
+9 -13
View File
@@ -223,7 +223,7 @@ export function Link({
{...web({
hrefAttrs: {
target: download ? undefined : isExternal ? 'blank' : undefined,
rel: isExternal ? 'noopener noreferrer' : undefined,
rel: isExternal ? 'noopener' : undefined,
download,
},
dataSet: {
@@ -274,11 +274,6 @@ export function InlineLinkText({
onOut: onHoverOut,
} = useInteractionState()
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
const {
state: pressed,
onIn: onPressIn,
onOut: onPressOut,
} = useInteractionState()
const flattenedStyle = flatten(style) || {}
return (
@@ -289,19 +284,20 @@ export function InlineLinkText({
{...rest}
style={[
{color: t.palette.primary_500},
(hovered || focused || pressed) &&
(hovered || focused) &&
!disableUnderline && {
...web({outline: 0}),
textDecorationLine: 'underline',
textDecorationColor: flattenedStyle.color ?? t.palette.primary_500,
...web({
outline: 0,
textDecorationLine: 'underline',
textDecorationColor:
flattenedStyle.color ?? t.palette.primary_500,
}),
},
flattenedStyle,
]}
role="link"
onPress={download ? undefined : onPress}
onLongPress={onLongPress}
onPressIn={onPressIn}
onPressOut={onPressOut}
onFocus={onFocus}
onBlur={onBlur}
onMouseEnter={onHoverIn}
@@ -311,7 +307,7 @@ export function InlineLinkText({
{...web({
hrefAttrs: {
target: download ? undefined : isExternal ? 'blank' : undefined,
rel: isExternal ? 'noopener noreferrer' : undefined,
rel: isExternal ? 'noopener' : undefined,
download,
},
dataSet: {
+3 -4
View File
@@ -17,13 +17,12 @@ export function Loader(props: Props) {
const rotation = useSharedValue(0)
const animatedStyles = useAnimatedStyle(() => ({
transform: [{rotate: rotation.value + 'deg'}],
transform: [{rotate: rotation.get() + 'deg'}],
}))
React.useEffect(() => {
rotation.value = withRepeat(
withTiming(360, {duration: 500, easing: Easing.linear}),
-1,
rotation.set(() =>
withRepeat(withTiming(360, {duration: 500, easing: Easing.linear}), -1),
)
}, [rotation])
-1
View File
@@ -1,4 +1,3 @@
import React from 'react'
import {View} from 'react-native'
import {atoms as a, flatten, useTheme} from '#/alf'
-2
View File
@@ -1,5 +1,3 @@
/* eslint-disable react/prop-types */
import React from 'react'
import {Pressable, StyleProp, View, ViewStyle} from 'react-native'
import {msg} from '@lingui/macro'
+2 -2
View File
@@ -283,8 +283,8 @@ export function DescriptionPlaceholder({
export type FollowButtonProps = {
profile: AppBskyActorDefs.ProfileViewBasic
moderationOpts: ModerationOpts
logContext: LogEvents['profile:follow:sampled']['logContext'] &
LogEvents['profile:unfollow:sampled']['logContext']
logContext: LogEvents['profile:follow']['logContext'] &
LogEvents['profile:unfollow']['logContext']
} & Partial<ButtonProps>
export function FollowButton(props: FollowButtonProps) {
-1
View File
@@ -1,4 +1,3 @@
import React from 'react'
import {StyleProp, View, ViewStyle} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-1
View File
@@ -1,4 +1,3 @@
import React from 'react'
import {View} from 'react-native'
import * as Progress from 'react-native-progress'
+28 -22
View File
@@ -55,13 +55,15 @@ export const ProgressGuideToast = React.forwardRef<
// animate the opacity then set isOpen to false when done
const setIsntOpen = () => setIsOpen(false)
opacity.value = withTiming(
0,
{
duration: 400,
easing: Easing.out(Easing.cubic),
},
() => runOnJS(setIsntOpen)(),
opacity.set(() =>
withTiming(
0,
{
duration: 400,
easing: Easing.out(Easing.cubic),
},
() => runOnJS(setIsntOpen)(),
),
)
}, [setIsOpen, opacity])
@@ -71,20 +73,24 @@ export const ProgressGuideToast = React.forwardRef<
// animate the vertical translation, the opacity, and the checkmark
const playCheckmark = () => animatedCheckRef.current?.play()
opacity.value = 0
opacity.value = withTiming(
1,
{
duration: 100,
easing: Easing.out(Easing.cubic),
},
() => runOnJS(playCheckmark)(),
opacity.set(0)
opacity.set(() =>
withTiming(
1,
{
duration: 100,
easing: Easing.out(Easing.cubic),
},
() => runOnJS(playCheckmark)(),
),
)
translateY.set(0)
translateY.set(() =>
withTiming(insets.top + 10, {
duration: 500,
easing: Easing.out(Easing.cubic),
}),
)
translateY.value = 0
translateY.value = withTiming(insets.top + 10, {
duration: 500,
easing: Easing.out(Easing.cubic),
})
// start the countdown timer to autoclose
timeoutRef.current = setTimeout(close, visibleDuration || 5e3)
@@ -114,8 +120,8 @@ export const ProgressGuideToast = React.forwardRef<
}, [winDim.width])
const animatedStyle = useAnimatedStyle(() => ({
transform: [{translateY: translateY.value}],
opacity: opacity.value,
transform: [{translateY: translateY.get()}],
opacity: opacity.get(),
}))
return (
@@ -1,4 +1,3 @@
import React from 'react'
import {View} from 'react-native'
import {AppBskyLabelerDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
+9 -24
View File
@@ -9,6 +9,7 @@ import {NavigationProp} from '#/lib/routes/types'
import {toShortUrl} from '#/lib/strings/url-helpers'
import {isNative} from '#/platform/detection'
import {atoms as a, flatten, native, TextStyleProp, useTheme, web} from '#/alf'
import {isOnlyEmoji} from '#/alf/typography'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {InlineLinkText, LinkProps} from '#/components/Link'
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
@@ -53,7 +54,6 @@ export function RichText({
const plainStyles = [a.leading_snug, flattenedStyle]
const interactiveStyles = [
a.leading_snug,
a.pointer_events_auto,
flatten(interactiveStyle),
flattenedStyle,
]
@@ -151,17 +151,14 @@ export function RichText({
/>,
)
} else {
els.push(
<Text key={key} emoji style={plainStyles}>
{segment.text}
</Text>,
)
els.push(segment.text)
}
key++
}
return (
<Text
emoji
selectable={selectable}
testID={testID}
style={plainStyles}
@@ -194,11 +191,6 @@ function RichTextTag({
onOut: onHoverOut,
} = useInteractionState()
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
const {
state: pressed,
onIn: onPressIn,
onOut: onPressOut,
} = useInteractionState()
const navigation = useNavigation<NavigationProp>()
const navigateToPage = React.useCallback(() => {
@@ -228,8 +220,6 @@ function RichTextTag({
accessibilityRole: isNative ? 'button' : undefined,
onPress: navigateToPage,
onLongPress: openDialog,
onPressIn: onPressIn,
onPressOut: onPressOut,
})}
{...web({
onMouseEnter: onHoverIn,
@@ -243,10 +233,12 @@ function RichTextTag({
cursor: 'pointer',
}),
{color: t.palette.primary_500},
(hovered || focused || pressed) && {
...web({outline: 0}),
textDecorationLine: 'underline',
textDecorationColor: t.palette.primary_500,
(hovered || focused) && {
...web({
outline: 0,
textDecorationLine: 'underline',
textDecorationColor: t.palette.primary_500,
}),
},
style,
]}>
@@ -256,10 +248,3 @@ function RichTextTag({
</React.Fragment>
)
}
export function isOnlyEmoji(text: string) {
return (
text.length <= 15 &&
/^[\p{Emoji_Presentation}\p{Extended_Pictographic}]+$/u.test(text)
)
}
@@ -1,4 +1,3 @@
import React from 'react'
import {View} from 'react-native'
import {Image} from 'expo-image'
import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker'
@@ -1,4 +1,4 @@
import React, {useRef} from 'react'
import {useRef} from 'react'
import type {ListRenderItemInfo} from 'react-native'
import {View} from 'react-native'
import {AppBskyActorDefs, ModerationOpts} from '@atproto/api'
@@ -1,4 +1,3 @@
import React from 'react'
import {Keyboard, View} from 'react-native'
import {
AppBskyActorDefs,
-1
View File
@@ -1,4 +1,3 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import {isTouchDevice} from '#/lib/browser'
+164 -150
View File
@@ -40,9 +40,37 @@ export function TagMenu({
tag: string
authorHandle?: string
}>) {
const navigation = useNavigation<NavigationProp>()
return (
<>
{children}
<Dialog.Outer control={control}>
<Dialog.Handle />
<TagMenuInner
control={control}
tag={tag}
authorHandle={authorHandle}
navigation={navigation}
/>
</Dialog.Outer>
</>
)
}
function TagMenuInner({
control,
tag,
authorHandle,
navigation,
}: {
control: Dialog.DialogOuterProps['control']
tag: string
authorHandle?: string
// Passed down because on native, we don't use real portals (and context would be wrong).
navigation: NavigationProp
}) {
const {_} = useLingui()
const t = useTheme()
const navigation = useNavigation<NavigationProp>()
const {isLoading: isPreferencesLoading, data: preferences} =
usePreferencesQuery()
const {
@@ -79,32 +107,75 @@ export function TagMenu({
}, [tag, preferences?.moderationPrefs?.mutedWords])
return (
<>
{children}
<Dialog.Outer control={control}>
<Dialog.Handle />
<Dialog.Inner label={_(msg`Tag menu: ${displayTag}`)}>
{isPreferencesLoading ? (
<View style={[a.w_full, a.align_center]}>
<Loader size="lg" />
</View>
) : (
<>
<Dialog.Inner label={_(msg`Tag menu: ${displayTag}`)}>
{isPreferencesLoading ? (
<View style={[a.w_full, a.align_center]}>
<Loader size="lg" />
</View>
) : (
<>
<View
style={[
a.rounded_md,
a.border,
a.mb_md,
t.atoms.border_contrast_low,
t.atoms.bg_contrast_25,
]}>
<Link
label={_(msg`View all posts with tag ${displayTag}`)}
{...createStaticClick(() => {
control.close(() => {
navigation.push('Hashtag', {
tag: encodeURIComponent(tag),
})
})
})}>
<View
style={[
a.rounded_md,
a.border,
a.mb_md,
t.atoms.border_contrast_low,
t.atoms.bg_contrast_25,
a.w_full,
a.flex_row,
a.align_center,
a.justify_start,
a.gap_md,
a.px_lg,
a.py_md,
]}>
<Search size="lg" style={[t.atoms.text_contrast_medium]} />
<Text
numberOfLines={1}
ellipsizeMode="middle"
style={[
a.flex_1,
a.text_md,
a.font_bold,
native({top: 2}),
t.atoms.text_contrast_medium,
]}>
<Trans>
See{' '}
<Text style={[a.text_md, a.font_bold, t.atoms.text]}>
{displayTag}
</Text>{' '}
posts
</Trans>
</Text>
</View>
</Link>
{authorHandle && !isInvalidHandle(authorHandle) && (
<>
<Divider />
<Link
label={_(msg`View all posts with tag ${displayTag}`)}
label={_(
msg`View all posts by @${authorHandle} with tag ${displayTag}`,
)}
{...createStaticClick(() => {
control.close(() => {
navigation.push('Hashtag', {
tag: encodeURIComponent(tag),
author: authorHandle,
})
})
})}>
@@ -118,7 +189,7 @@ export function TagMenu({
a.px_lg,
a.py_md,
]}>
<Search size="lg" style={[t.atoms.text_contrast_medium]} />
<Person size="lg" style={[t.atoms.text_contrast_medium]} />
<Text
numberOfLines={1}
ellipsizeMode="middle"
@@ -134,143 +205,86 @@ export function TagMenu({
<Text style={[a.text_md, a.font_bold, t.atoms.text]}>
{displayTag}
</Text>{' '}
posts
posts by this user
</Trans>
</Text>
</View>
</Link>
</>
)}
{authorHandle && !isInvalidHandle(authorHandle) && (
<>
<Divider />
{preferences ? (
<>
<Divider />
<Link
label={_(
msg`View all posts by @${authorHandle} with tag ${displayTag}`,
)}
{...createStaticClick(() => {
control.close(() => {
navigation.push('Hashtag', {
tag: encodeURIComponent(tag),
author: authorHandle,
})
})
})}>
<View
style={[
a.w_full,
a.flex_row,
a.align_center,
a.justify_start,
a.gap_md,
a.px_lg,
a.py_md,
]}>
<Person
size="lg"
style={[t.atoms.text_contrast_medium]}
/>
<Text
numberOfLines={1}
ellipsizeMode="middle"
style={[
a.flex_1,
a.text_md,
a.font_bold,
native({top: 2}),
t.atoms.text_contrast_medium,
]}>
<Trans>
See{' '}
<Text
style={[a.text_md, a.font_bold, t.atoms.text]}>
{displayTag}
</Text>{' '}
posts by this user
</Trans>
</Text>
</View>
</Link>
</>
)}
{preferences ? (
<>
<Divider />
<Button
label={
isMuted
? _(msg`Unmute all ${displayTag} posts`)
: _(msg`Mute all ${displayTag} posts`)
<Button
label={
isMuted
? _(msg`Unmute all ${displayTag} posts`)
: _(msg`Mute all ${displayTag} posts`)
}
onPress={() => {
control.close(() => {
if (isMuted) {
resetUpsert()
removeMutedWords(removeableMuteWords)
} else {
resetRemove()
upsertMutedWord([
{
value: tag,
targets: ['tag'],
actorTarget: 'all',
},
])
}
onPress={() => {
control.close(() => {
if (isMuted) {
resetUpsert()
removeMutedWords(removeableMuteWords)
} else {
resetRemove()
upsertMutedWord([
{
value: tag,
targets: ['tag'],
actorTarget: 'all',
},
])
}
})
}}>
<View
style={[
a.w_full,
a.flex_row,
a.align_center,
a.justify_start,
a.gap_md,
a.px_lg,
a.py_md,
]}>
<Mute
size="lg"
style={[t.atoms.text_contrast_medium]}
/>
<Text
numberOfLines={1}
ellipsizeMode="middle"
style={[
a.flex_1,
a.text_md,
a.font_bold,
native({top: 2}),
t.atoms.text_contrast_medium,
]}>
{isMuted ? _(msg`Unmute`) : _(msg`Mute`)}{' '}
<Text style={[a.text_md, a.font_bold, t.atoms.text]}>
{displayTag}
</Text>{' '}
<Trans>posts</Trans>
</Text>
</View>
</Button>
</>
) : null}
</View>
})
}}>
<View
style={[
a.w_full,
a.flex_row,
a.align_center,
a.justify_start,
a.gap_md,
a.px_lg,
a.py_md,
]}>
<Mute size="lg" style={[t.atoms.text_contrast_medium]} />
<Text
numberOfLines={1}
ellipsizeMode="middle"
style={[
a.flex_1,
a.text_md,
a.font_bold,
native({top: 2}),
t.atoms.text_contrast_medium,
]}>
{isMuted ? _(msg`Unmute`) : _(msg`Mute`)}{' '}
<Text style={[a.text_md, a.font_bold, t.atoms.text]}>
{displayTag}
</Text>{' '}
<Trans>posts</Trans>
</Text>
</View>
</Button>
</>
) : null}
</View>
<Button
label={_(msg`Close this dialog`)}
size="small"
variant="ghost"
color="secondary"
onPress={() => control.close()}>
<ButtonText>
<Trans>Cancel</Trans>
</ButtonText>
</Button>
</>
)}
</Dialog.Inner>
</Dialog.Outer>
</>
<Button
label={_(msg`Close this dialog`)}
size="small"
variant="ghost"
color="secondary"
onPress={() => control.close()}>
<ButtonText>
<Trans>Cancel</Trans>
</ButtonText>
</Button>
</>
)}
</Dialog.Inner>
)
}
+10 -139
View File
@@ -1,140 +1,15 @@
import React from 'react'
import {StyleProp, TextProps as RNTextProps, TextStyle} from 'react-native'
import {UITextView} from 'react-native-uitextview'
import createEmojiRegex from 'emoji-regex'
import {logger} from '#/logger'
import {isIOS, isNative} from '#/platform/detection'
import {Alf, applyFonts, atoms, flatten, useAlf, useTheme, web} from '#/alf'
import {atoms, flatten, useAlf, useTheme, web} from '#/alf'
import {
childHasEmoji,
normalizeTextStyles,
renderChildrenWithEmoji,
TextProps,
} from '#/alf/typography'
import {IS_DEV} from '#/env'
export type StringChild = string | (string | null)[]
export type TextProps = Omit<RNTextProps, 'children'> & {
/**
* Lets the user select text, to use the native copy and paste functionality.
*/
selectable?: boolean
/**
* Provides `data-*` attributes to the underlying `UITextView` component on
* web only.
*/
dataSet?: Record<string, string | number | undefined>
/**
* Appears as a small tooltip on web hover.
*/
title?: string
} & (
| {
emoji?: true
children: StringChild
}
| {
emoji?: false
children: RNTextProps['children']
}
)
const EMOJI = createEmojiRegex()
export function childHasEmoji(children: React.ReactNode) {
return (Array.isArray(children) ? children : [children]).some(
child => typeof child === 'string' && createEmojiRegex().test(child),
)
}
export function childIsString(
children: React.ReactNode,
): children is StringChild {
return (
typeof children === 'string' ||
(Array.isArray(children) &&
children.every(child => typeof child === 'string' || child === null))
)
}
export function renderChildrenWithEmoji(
children: StringChild,
props: Omit<TextProps, 'children'> = {},
) {
const normalized = Array.isArray(children) ? children : [children]
return (
<UITextView {...props}>
{normalized.map(child => {
if (typeof child !== 'string') return child
const emojis = child.match(EMOJI)
if (emojis === null) {
return child
}
return child.split(EMOJI).map((stringPart, index) => (
<UITextView key={index} {...props}>
{stringPart}
{emojis[index] ? (
<UITextView
{...props}
style={[props?.style, {color: 'black', fontFamily: 'System'}]}>
{emojis[index]}
</UITextView>
) : null}
</UITextView>
))
})}
</UITextView>
)
}
/**
* Util to calculate lineHeight from a text size atom and a leading atom
*
* Example:
* `leading(atoms.text_md, atoms.leading_normal)` // => 24
*/
export function leading<
Size extends {fontSize?: number},
Leading extends {lineHeight?: number},
>(textSize: Size, leading: Leading) {
const size = textSize?.fontSize || atoms.text_md.fontSize
const lineHeight = leading?.lineHeight || atoms.leading_normal.lineHeight
return Math.round(size * lineHeight)
}
/**
* Ensures that `lineHeight` defaults to a relative value of `1`, or applies
* other relative leading atoms.
*
* If the `lineHeight` value is > 2, we assume it's an absolute value and
* returns it as-is.
*/
export function normalizeTextStyles(
styles: StyleProp<TextStyle>,
{
fontScale,
fontFamily,
}: {
fontScale: number
fontFamily: Alf['fonts']['family']
} & Pick<Alf, 'flags'>,
) {
const s = flatten(styles)
// should always be defined on these components
s.fontSize = (s.fontSize || atoms.text_md.fontSize) * fontScale
if (s?.lineHeight) {
if (s.lineHeight !== 0 && s.lineHeight <= 2) {
s.lineHeight = Math.round(s.fontSize * s.lineHeight)
}
} else if (!isNative) {
s.lineHeight = s.fontSize
}
applyFonts(s, fontFamily)
return s
}
export type {TextProps}
/**
* Our main text component. Use this most of the time.
@@ -162,10 +37,6 @@ export function Text({
`Text: emoji detected but emoji not enabled: "${children}"\n\nPlease add <Text emoji />'`,
)
}
if (emoji && !childIsString(children)) {
logger.error('Text: when <Text emoji />, children can only be strings.')
}
}
const shared = {
@@ -178,12 +49,12 @@ export function Text({
return (
<UITextView {...shared}>
{isIOS && emoji ? renderChildrenWithEmoji(children, shared) : children}
{renderChildrenWithEmoji(children, shared, emoji ?? false)}
</UITextView>
)
}
export function createHeadingElement({level}: {level: number}) {
function createHeadingElement({level}: {level: number}) {
return function HeadingElement({style, ...rest}: TextProps) {
const attr =
web({
+12 -8
View File
@@ -32,21 +32,25 @@ export const AnimatedCheck = React.forwardRef<
const checkAnim = useSharedValue(0)
const circleAnimatedProps = useAnimatedProps(() => ({
strokeDashoffset: 166 - circleAnim.value * 166,
strokeDashoffset: 166 - circleAnim.get() * 166,
}))
const checkAnimatedProps = useAnimatedProps(() => ({
strokeDashoffset: 48 - 48 * checkAnim.value,
strokeDashoffset: 48 - 48 * checkAnim.get(),
}))
const play = React.useCallback(
(cb?: () => void) => {
circleAnim.value = 0
checkAnim.value = 0
circleAnim.set(0)
checkAnim.set(0)
circleAnim.value = withTiming(1, {duration: 500, easing: Easing.linear})
checkAnim.value = withDelay(
500,
withTiming(1, {duration: 300, easing: Easing.linear}, cb),
circleAnim.set(() =>
withTiming(1, {duration: 500, easing: Easing.linear}),
)
checkAnim.set(() =>
withDelay(
500,
withTiming(1, {duration: 300, easing: Easing.linear}, cb),
),
)
},
[circleAnim, checkAnim],
+1 -1
View File
@@ -1,4 +1,4 @@
import React, {useCallback} from 'react'
import {useCallback} from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -306,7 +306,7 @@ export function PostInteractionSettingsForm({
}
value={quotesEnabled}
onChange={onChangeQuotesEnabled}
style={[, a.justify_between, a.pt_xs]}>
style={[a.justify_between, a.pt_xs]}>
<Text style={[t.atoms.text_contrast_medium]}>
{quotesEnabled ? (
<Trans>Quote posts enabled</Trans>
@@ -483,7 +483,7 @@ function Selectable({
a.justify_between,
a.rounded_sm,
a.p_md,
{height: 40}, // for consistency with checkmark icon visible or not
{minHeight: 40}, // for consistency with checkmark icon visible or not
t.atoms.bg_contrast_50,
(hovered || focused) && t.atoms.bg_contrast_100,
isSelected && {
+1 -1
View File
@@ -1,4 +1,4 @@
import React, {useCallback} from 'react'
import {useCallback} from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
+45 -22
View File
@@ -146,18 +146,17 @@ export function Inner({
<ErrorMessage message={error} />
</View>
) : null}
<Text style={[a.text_md, a.leading_snug]}>
{currentStep === 'StepOne' ? (
<>
{!reasonText ? (
<>
<Trans>
You'll receive an email at{' '}
<Text style={[a.text_md, a.leading_snug, a.font_bold]}>
{currentAccount?.email}
</Text>{' '}
to verify it's you.
</Trans>{' '}
{currentStep === 'StepOne' ? (
<View>
{reasonText ? (
<View style={[a.gap_sm]}>
<Text style={[a.text_md, a.leading_snug]}>{reasonText}</Text>
<Text style={[a.text_md, a.leading_snug]}>
Don't have access to{' '}
<Text style={[a.text_md, a.leading_snug, a.font_bold]}>
{currentAccount?.email}
</Text>
?{' '}
<InlineLinkText
to="#"
label={_(msg`Change email address`)}
@@ -169,17 +168,41 @@ export function Inner({
})
return false
}}>
<Trans>Need to change it?</Trans>
<Trans>Change your email address</Trans>
</InlineLinkText>
</>
) : (
reasonText
)}
</>
) : (
uiStrings[currentStep].message
)}
</Text>
.
</Text>
</View>
) : (
<Text style={[a.text_md, a.leading_snug]}>
<Trans>
You'll receive an email at{' '}
<Text style={[a.text_md, a.leading_snug, a.font_bold]}>
{currentAccount?.email}
</Text>{' '}
to verify it's you.
</Trans>{' '}
<InlineLinkText
to="#"
label={_(msg`Change email address`)}
style={[a.text_md, a.leading_snug]}
onPress={e => {
e.preventDefault()
control.close(() => {
openModal({name: 'change-email'})
})
return false
}}>
<Trans>Need to change it?</Trans>
</InlineLinkText>
</Text>
)}
</View>
) : (
<Text style={[a.text_md, a.leading_snug]}>
{uiStrings[currentStep].message}
</Text>
)}
</View>
{currentStep === 'StepTwo' ? (
<View>
@@ -1,117 +0,0 @@
import React from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {AppearanceToggleButtonGroup} from '#/screens/Settings/AppearanceSettings'
import {atoms as a, useAlf, useTheme} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {useNuxDialogContext} from '#/components/dialogs/nuxs'
import {Divider} from '#/components/Divider'
import {TextSize_Stroke2_Corner0_Rounded as TextSize} from '#/components/icons/TextSize'
import {TitleCase_Stroke2_Corner0_Rounded as Aa} from '#/components/icons/TitleCase'
import {Text} from '#/components/Typography'
export function NeueTypography() {
const t = useTheme()
const {_} = useLingui()
const nuxDialogs = useNuxDialogContext()
const control = Dialog.useDialogControl()
const {fonts} = useAlf()
Dialog.useAutoOpen(control, 3e3)
const onClose = React.useCallback(() => {
nuxDialogs.dismissActiveNux()
}, [nuxDialogs])
const onChangeFontFamily = React.useCallback(
(values: string[]) => {
const next = values[0] === 'system' ? 'system' : 'theme'
fonts.setFontFamily(next)
},
[fonts],
)
const onChangeFontScale = React.useCallback(
(values: string[]) => {
const next = values[0] || ('0' as any)
fonts.setFontScale(next)
},
[fonts],
)
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]}>
<Text style={[a.text_3xl, a.font_heavy]}>
<Trans>New font settings </Trans>
</Text>
<Text style={[a.text_lg, a.leading_snug, {maxWidth: 400}]}>
<Trans>
We're introducing a new theme font, along with adjustable font
sizing.
</Trans>
</Text>
<Text
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
<Trans>
You can adjust these in your Appearance Settings later.
</Trans>
</Text>
</View>
<Divider />
<View style={[a.gap_lg]}>
<AppearanceToggleButtonGroup
title={_(msg`Font`)}
description={_(
msg`For the best experience, we recommend using the theme font.`,
)}
icon={Aa}
items={[
{
label: _(msg`System`),
name: 'system',
},
{
label: _(msg`Theme`),
name: 'theme',
},
]}
values={[fonts.family]}
onChange={onChangeFontFamily}
/>
<AppearanceToggleButtonGroup
title={_(msg`Font size`)}
icon={TextSize}
items={[
{
label: _(msg`Smaller`),
name: '-1',
},
{
label: _(msg`Default`),
name: '0',
},
{
label: _(msg`Larger`),
name: '1',
},
]}
values={[fonts.scale]}
onChange={onChangeFontScale}
/>
</View>
</View>
<Dialog.Close />
</Dialog.ScrollableInner>
</Dialog.Outer>
)
}
+10 -16
View File
@@ -19,7 +19,6 @@ import {useOnboardingState} from '#/state/shell'
/*
* NUXs
*/
import {NeueTypography} from '#/components/dialogs/nuxs/NeueTypography'
import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing'
import {IS_DEV} from '#/env'
@@ -36,19 +35,7 @@ const queuedNuxs: {
currentProfile: AppBskyActorDefs.ProfileViewDetailed
preferences: UsePreferencesQueryResponse
}) => boolean
}[] = [
{
id: Nux.NeueTypography,
enabled(props) {
if (props.currentProfile.createdAt) {
if (new Date(props.currentProfile.createdAt) < new Date('2024-10-09')) {
return true
}
}
return false
},
},
]
}[] = []
const Context = React.createContext<Context>({
activeNux: undefined,
@@ -66,7 +53,14 @@ export function NuxDialogs() {
const onboardingActive = useOnboardingState().isActive
const isLoading =
!currentAccount || !preferences || !profile || onboardingActive
onboardingActive ||
!currentAccount ||
!preferences ||
!profile ||
// Profile isn't legit ready until createdAt is a real date.
!profile.createdAt ||
profile.createdAt === '0001-01-01T00:00:00.000Z' // TODO: Fix this in AppView.
return !isLoading ? (
<Inner
currentAccount={currentAccount}
@@ -174,7 +168,7 @@ function Inner({
return (
<Context.Provider value={ctx}>
{activeNux === Nux.NeueTypography && <NeueTypography />}
{/*For example, activeNux === Nux.NeueTypography && <NeueTypography />*/}
</Context.Provider>
)
}
+9 -7
View File
@@ -34,7 +34,7 @@ export function ActionsWrapper({
const scale = useSharedValue(1)
const animatedStyle = useAnimatedStyle(() => ({
transform: [{scale: scale.value}],
transform: [{scale: scale.get()}],
}))
const open = React.useCallback(() => {
@@ -46,7 +46,7 @@ export function ActionsWrapper({
const shrink = React.useCallback(() => {
'worklet'
cancelAnimation(scale)
scale.value = withTiming(1, {duration: 200})
scale.set(() => withTiming(1, {duration: 200}))
}, [scale])
const doubleTapGesture = Gesture.Tap()
@@ -58,11 +58,13 @@ export function ActionsWrapper({
const pressAndHoldGesture = Gesture.LongPress()
.onStart(() => {
'worklet'
scale.value = withTiming(1.05, {duration: 200}, finished => {
if (!finished) return
runOnJS(open)()
shrink()
})
scale.set(() =>
withTiming(1.05, {duration: 200}, finished => {
if (!finished) return
runOnJS(open)()
shrink()
}),
)
})
.onTouchesUp(shrink)
.onTouchesMove(shrink)
+3 -3
View File
@@ -42,12 +42,12 @@ export function ChatEmptyPill() {
const onPressIn = React.useCallback(() => {
if (isWeb) return
scale.value = withTiming(1.075, {duration: 100})
scale.set(() => withTiming(1.075, {duration: 100}))
}, [scale])
const onPressOut = React.useCallback(() => {
if (isWeb) return
scale.value = withTiming(1, {duration: 100})
scale.set(() => withTiming(1, {duration: 100}))
}, [scale])
const onPress = React.useCallback(() => {
@@ -61,7 +61,7 @@ export function ChatEmptyPill() {
}, [playHaptic, prompts.length])
const animatedStyle = useAnimatedStyle(() => ({
transform: [{scale: scale.value}],
transform: [{scale: scale.get()}],
}))
return (
+1 -1
View File
@@ -115,7 +115,7 @@ let ConvoMenu = ({
{...props}
onPress={() => {
Keyboard.dismiss()
// eslint-disable-next-line react/prop-types -- eslint is confused by the name `props`
props.onPress()
}}
style={[
-1
View File
@@ -1,4 +1,3 @@
import React from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
+2 -1
View File
@@ -19,10 +19,11 @@ import {ConvoItem} from '#/state/messages/convo/types'
import {useSession} from '#/state/session'
import {TimeElapsed} from '#/view/com/util/TimeElapsed'
import {atoms as a, useTheme} from '#/alf'
import {isOnlyEmoji} from '#/alf/typography'
import {ActionsWrapper} from '#/components/dms/ActionsWrapper'
import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography'
import {isOnlyEmoji, RichText} from '../RichText'
import {RichText} from '../RichText'
import {DateDivider} from './DateDivider'
import {MessageItemEmbed} from './MessageItemEmbed'
import {localDateString} from './util'
+1 -1
View File
@@ -62,7 +62,7 @@ export let MessageMenu = ({
message.text,
langPrefs.primaryLanguage,
)
openLink(translatorUrl)
openLink(translatorUrl, true)
}, [langPrefs.primaryLanguage, message.text, openLink])
const onDelete = React.useCallback(() => {
+3 -3
View File
@@ -35,12 +35,12 @@ export function NewMessagesPill({
const onPressIn = React.useCallback(() => {
if (isWeb) return
scale.value = withTiming(1.075, {duration: 100})
scale.set(() => withTiming(1.075, {duration: 100}))
}, [scale])
const onPressOut = React.useCallback(() => {
if (isWeb) return
scale.value = withTiming(1, {duration: 100})
scale.set(() => withTiming(1, {duration: 100}))
}, [scale])
const onPress = React.useCallback(() => {
@@ -49,7 +49,7 @@ export function NewMessagesPill({
}, [onPressInner, playHaptic])
const animatedStyle = useAnimatedStyle(() => ({
transform: [{scale: scale.value}],
transform: [{scale: scale.get()}],
}))
return (
@@ -1,4 +1,3 @@
import React from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
+1 -1
View File
@@ -1,4 +1,4 @@
import React, {useCallback} from 'react'
import {useCallback} from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -278,7 +278,7 @@ export function SearchablePeopleList({
) : null}
</View>
<View style={[, web([a.pt_xs])]}>
<View style={web([a.pt_xs])}>
<SearchInput
inputRef={inputRef}
value={searchText}
@@ -313,6 +313,7 @@ export function SearchablePeopleList({
web([a.py_0, {height: '100vh', maxHeight: 600}, a.px_0]),
native({height: '100%'}),
]}
webInnerContentContainerStyle={a.py_0}
webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]}
keyboardDismissMode="on-drag"
/>
@@ -1,4 +1,4 @@
import React, {useCallback} from 'react'
import {useCallback} from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -1,4 +1,3 @@
import React from 'react'
import {Pressable, View} from 'react-native'
import {useLingui} from '@lingui/react'
-1
View File
@@ -1,4 +1,3 @@
import React from 'react'
import {View} from 'react-native'
import {atoms as a, useTheme} from '#/alf'
+2
View File
@@ -22,6 +22,7 @@ import {
it,
ja,
ko,
pl,
ptBR,
ru,
th,
@@ -53,6 +54,7 @@ const locales: Record<AppLanguage, Locale | undefined> = {
it,
ja,
ko,
pl,
['pt-BR']: ptBR,
ru,
th,
+2 -2
View File
@@ -15,8 +15,8 @@ export function useFollowMethods({
logContext,
}: {
profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
logContext: LogEvents['profile:follow:sampled']['logContext'] &
LogEvents['profile:unfollow:sampled']['logContext']
logContext: LogEvents['profile:follow']['logContext'] &
LogEvents['profile:unfollow']['logContext']
}) {
const {_} = useLingui()
const requireAuth = useRequireAuth()
+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const CalendarClock_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M15.439 3.148a1 1 0 0 1 .41.645l.568 3.22a7 7 0 1 1-6.174 10.97L4.32 19.027a1 1 0 0 1-1.159-.811L1.078 6.398a1 1 0 0 1 .81-1.158l12.803-2.258a1 1 0 0 1 .748.166ZM9.325 16.114A7 7 0 0 1 9 14c0-1.56.51-3 1.372-4.164l-6.456 1.139 1.041 5.909 4.368-.77ZM3.568 9.005l10.833-1.91-.347-1.97L3.22 7.036l.347 1.97ZM16 9a5 5 0 1 0 0 10 5 5 0 0 0 0-10Zm0 2a1 1 0 0 1 1 1v1.586l1.374 1.374a1 1 0 0 1-1.414 1.414l-1.667-1.667A1 1 0 0 1 15 14v-2a1 1 0 0 1 1-1Z',
})
+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const ChainLink3_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M18.535 5.465a5.003 5.003 0 0 0-7.076 0l-.005.005-.752.742a1 1 0 1 1-1.404-1.424l.749-.74a7.003 7.003 0 0 1 9.904 9.905l-.002.003-.737.746a1 1 0 1 1-1.424-1.404l.747-.757a5.003 5.003 0 0 0 0-7.076ZM6.202 9.288a1 1 0 0 1 .01 1.414l-.747.757a5.003 5.003 0 1 0 7.076 7.076l.005-.005.752-.742a1 1 0 1 1 1.404 1.424l-.746.737-.003.002a7.003 7.003 0 0 1-9.904-9.904l.74-.75a1 1 0 0 1 1.413-.009Zm8.505.005a1 1 0 0 1 0 1.414l-4 4a1 1 0 0 1-1.414-1.414l4-4a1 1 0 0 1 1.414 0Z',
})
-1
View File
@@ -1,4 +1,3 @@
import React from 'react'
import {StyleSheet, TextProps} from 'react-native'
import type {PathProps, SvgProps} from 'react-native-svg'
import {Defs, LinearGradient, Stop} from 'react-native-svg'
+31 -9
View File
@@ -31,6 +31,37 @@ export function ContentHider({
ignoreMute?: boolean
style?: StyleProp<ViewStyle>
childContainerStyle?: StyleProp<ViewStyle>
}>) {
const blur = modui?.blurs[0]
if (!blur || (ignoreMute && isJustAMute(modui))) {
return (
<View testID={testID} style={style}>
{children}
</View>
)
}
return (
<ContentHiderActive
testID={testID}
modui={modui}
style={style}
childContainerStyle={childContainerStyle}>
{children}
</ContentHiderActive>
)
}
function ContentHiderActive({
testID,
modui,
style,
childContainerStyle,
children,
}: React.PropsWithChildren<{
testID?: string
modui: ModerationUI
style?: StyleProp<ViewStyle>
childContainerStyle?: StyleProp<ViewStyle>
}>) {
const t = useTheme()
const {_} = useLingui()
@@ -40,7 +71,6 @@ export function ContentHider({
const {labelDefs} = useLabelDefinitions()
const globalLabelStrings = useGlobalLabelStrings()
const {i18n} = useLingui()
const blur = modui?.blurs[0]
const desc = useModerationCauseDescription(blur)
@@ -99,14 +129,6 @@ export function ContentHider({
globalLabelStrings,
])
if (!blur || (ignoreMute && isJustAMute(modui))) {
return (
<View testID={testID} style={style}>
{children}
</View>
)
}
return (
<View testID={testID} style={[a.overflow_hidden, style]}>
<ModerationDetailsDialog control={control} modcause={blur} />
@@ -22,7 +22,7 @@ export function Outer({children}: React.PropsWithChildren<{}>) {
<View
style={[
a.flex_row,
a.gap_md,
a.gap_sm,
a.px_lg,
a.py_lg,
a.justify_between,
@@ -74,10 +74,9 @@ export function Buttons({
hideLabel?: string
}) {
const {_} = useLingui()
const {gtPhone} = useBreakpoints()
return (
<View style={[{minHeight: 35}, gtPhone ? undefined : a.w_full]}>
<View style={[{minHeight: 35}, a.w_full]}>
<ToggleButton.Group
label={_(
msg`Configure content filtering setting for category: ${name}`,
@@ -259,7 +258,7 @@ export function LabelerLabelPreference({
</Content>
{showConfig && (
<View style={[gtPhone ? undefined : a.w_full]}>
<>
{cantConfigure ? (
<View
style={[
@@ -290,7 +289,7 @@ export function LabelerLabelPreference({
hideLabel={hideLabel}
/>
)}
</View>
</>
)}
</Outer>
)
-1
View File
@@ -1,4 +1,3 @@
import React from 'react'
import {StyleProp, View, ViewStyle} from 'react-native'
import {AppBskyFeedDefs, ComAtprotoLabelDefs} from '@atproto/api'
import {msg, Plural} from '@lingui/macro'
@@ -1,4 +1,3 @@
import React from 'react'
import {View} from 'react-native'
import {ModerationCause} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
-1
View File
@@ -1,4 +1,3 @@
import React from 'react'
import {StyleProp, ViewStyle} from 'react-native'
import {ModerationCause, ModerationUI} from '@atproto/api'
@@ -1,4 +1,3 @@
import React from 'react'
import {StyleProp, ViewStyle} from 'react-native'
import {ModerationDecision} from '@atproto/api'
+10 -27
View File
@@ -1,4 +1,3 @@
import React from 'react'
import {View} from 'react-native'
import {atoms as a, useTheme} from '#/alf'
@@ -10,39 +9,23 @@ export function PlayButtonIcon({size = 32}: {size?: number}) {
const fg = t.name === 'light' ? t.palette.contrast_975 : t.palette.contrast_25
return (
<View
style={[
a.rounded_full,
a.overflow_hidden,
a.align_center,
a.justify_center,
t.atoms.shadow_lg,
{
width: size + size / 1.5,
height: size + size / 1.5,
},
]}>
<>
<View
style={[
a.absolute,
a.inset_0,
a.rounded_full,
{
backgroundColor: bg,
shadowColor: 'black',
shadowRadius: 32,
shadowOpacity: 0.5,
elevation: 24,
width: size + size / 1.5,
height: size + size / 1.5,
opacity: 0.7,
},
]}
/>
<PlayIcon
width={size}
fill={fg}
style={[
a.relative,
a.z_10,
{
left: size / 50,
},
]}
/>
</View>
<PlayIcon width={size} fill={fg} style={a.absolute} />
</>
)
}
+2 -1
View File
@@ -21,7 +21,7 @@ export const STARTER_PACK_MAX_SIZE = 150
// code and update this number with each release until we can get the
// server route done.
// -prf
export const JOINED_THIS_WEEK = 650000 // estimate as of 10/28/24
export const JOINED_THIS_WEEK = 3500000 // estimate as of 11/16/24
export const DISCOVER_DEBUG_DIDS: Record<string, true> = {
'did:plc:oisofpd7lj26yvgiivf3lxsi': true, // hailey.at
@@ -154,6 +154,7 @@ export const SUPPORTED_MIME_TYPES = [
'video/mpeg',
'video/webm',
'video/quicktime',
'image/gif',
] as const
export type SupportedMimeTypes = (typeof SUPPORTED_MIME_TYPES)[number]
+43 -41
View File
@@ -61,7 +61,7 @@ export function GestureActionView({
const clampedTransX = useDerivedValue(() => {
const min = actions.leftFirst ? -MAX_WIDTH : 0
const max = actions.rightFirst ? MAX_WIDTH : 0
return clamp(transX.value, min, max)
return clamp(transX.get(), min, max)
})
const iconScale = useSharedValue(1)
@@ -75,21 +75,23 @@ export function GestureActionView({
return
}
iconScale.value = withSequence(
withTiming(1.2, {duration: 175}),
withTiming(1, {duration: 100}),
iconScale.set(() =>
withSequence(
withTiming(1.2, {duration: 175}),
withTiming(1, {duration: 100}),
),
)
}
useAnimatedReaction(
() => transX,
() => {
if (transX.value === 0) {
if (transX.get() === 0) {
runOnJS(setActiveAction)(null)
} else if (transX.value < 0) {
} else if (transX.get() < 0) {
if (
actions.leftSecond &&
transX.value <= -actions.leftSecond.threshold
transX.get() <= -actions.leftSecond.threshold
) {
if (activeAction !== 'leftSecond') {
runOnJS(setActiveAction)('leftSecond')
@@ -97,10 +99,10 @@ export function GestureActionView({
} else if (activeAction !== 'leftFirst') {
runOnJS(setActiveAction)('leftFirst')
}
} else if (transX.value > 0) {
} else if (transX.get() > 0) {
if (
actions.rightSecond &&
transX.value > actions.rightSecond.threshold
transX.get() > actions.rightSecond.threshold
) {
if (activeAction !== 'rightSecond') {
runOnJS(setActiveAction)('rightSecond')
@@ -119,44 +121,44 @@ export function GestureActionView({
.activeOffsetY([-200, 200])
.onStart(() => {
'worklet'
isActive.value = true
isActive.set(true)
})
.onChange(e => {
'worklet'
transX.value = e.translationX
transX.set(e.translationX)
if (e.translationX < 0) {
// Left side
if (actions.leftSecond) {
if (
e.translationX <= -actions.leftSecond.threshold &&
!hitSecond.value
!hitSecond.get()
) {
runPopAnimation()
runOnJS(haptic)()
hitSecond.value = true
hitSecond.set(true)
} else if (
hitSecond.value &&
hitSecond.get() &&
e.translationX > -actions.leftSecond.threshold
) {
runPopAnimation()
hitSecond.value = false
hitSecond.set(false)
}
}
if (!hitSecond.value && actions.leftFirst) {
if (!hitSecond.get() && actions.leftFirst) {
if (
e.translationX <= -actions.leftFirst.threshold &&
!hitFirst.value
!hitFirst.get()
) {
runPopAnimation()
runOnJS(haptic)()
hitFirst.value = true
hitFirst.set(true)
} else if (
hitFirst.value &&
hitFirst.get() &&
e.translationX > -actions.leftFirst.threshold
) {
hitFirst.value = false
hitFirst.set(false)
}
}
} else if (e.translationX > 0) {
@@ -164,33 +166,33 @@ export function GestureActionView({
if (actions.rightSecond) {
if (
e.translationX >= actions.rightSecond.threshold &&
!hitSecond.value
!hitSecond.get()
) {
runPopAnimation()
runOnJS(haptic)()
hitSecond.value = true
hitSecond.set(true)
} else if (
hitSecond.value &&
hitSecond.get() &&
e.translationX < actions.rightSecond.threshold
) {
runPopAnimation()
hitSecond.value = false
hitSecond.set(false)
}
}
if (!hitSecond.value && actions.rightFirst) {
if (!hitSecond.get() && actions.rightFirst) {
if (
e.translationX >= actions.rightFirst.threshold &&
!hitFirst.value
!hitFirst.get()
) {
runPopAnimation()
runOnJS(haptic)()
hitFirst.value = true
hitFirst.set(true)
} else if (
hitFirst.value &&
hitFirst.get() &&
e.translationX < actions.rightFirst.threshold
) {
hitFirst.value = false
hitFirst.set(false)
}
}
}
@@ -198,29 +200,29 @@ export function GestureActionView({
.onEnd(e => {
'worklet'
if (e.translationX < 0) {
if (hitSecond.value && actions.leftSecond) {
if (hitSecond.get() && actions.leftSecond) {
runOnJS(actions.leftSecond.action)()
} else if (hitFirst.value && actions.leftFirst) {
} else if (hitFirst.get() && actions.leftFirst) {
runOnJS(actions.leftFirst.action)()
}
} else if (e.translationX > 0) {
if (hitSecond.value && actions.rightSecond) {
if (hitSecond.get() && actions.rightSecond) {
runOnJS(actions.rightSecond.action)()
} else if (hitSecond.value && actions.rightFirst) {
} else if (hitSecond.get() && actions.rightFirst) {
runOnJS(actions.rightFirst.action)()
}
}
transX.value = withTiming(0, {duration: 200})
hitFirst.value = false
hitSecond.value = false
isActive.value = false
transX.set(() => withTiming(0, {duration: 200}))
hitFirst.set(false)
hitSecond.set(false)
isActive.set(false)
})
const composedGesture = Gesture.Simultaneous(panGesture)
const animatedSliderStyle = useAnimatedStyle(() => {
return {
transform: [{translateX: clampedTransX.value}],
transform: [{translateX: clampedTransX.get()}],
}
})
@@ -274,7 +276,7 @@ export function GestureActionView({
const animatedBackgroundStyle = useAnimatedStyle(() => {
return {
backgroundColor: interpolateColor(
clampedTransX.value,
clampedTransX.get(),
interpolation.inputRange,
// @ts-expect-error - Weird type expected by reanimated, but this is okay
interpolation.outputRange,
@@ -283,10 +285,10 @@ export function GestureActionView({
})
const animatedIconStyle = useAnimatedStyle(() => {
const absTransX = Math.abs(clampedTransX.value)
const absTransX = Math.abs(clampedTransX.get())
return {
opacity: interpolate(absTransX, [0, 75], [0.15, 1]),
transform: [{scale: iconScale.value}],
transform: [{scale: iconScale.get()}],
}
})
-1
View File
@@ -1,4 +1,3 @@
import React from 'react'
import {View} from 'react-native'
import Animated, {
Keyframe,
+5 -9
View File
@@ -1,8 +1,6 @@
import React from 'react'
import {Pressable, PressableProps, StyleProp, ViewStyle} from 'react-native'
import Animated, {
cancelAnimation,
runOnJS,
useAnimatedStyle,
useReducedMotion,
useSharedValue,
@@ -32,27 +30,25 @@ export function PressableScale({
const scale = useSharedValue(1)
const animatedStyle = useAnimatedStyle(() => ({
transform: [{scale: scale.value}],
transform: [{scale: scale.get()}],
}))
return (
<AnimatedPressable
accessibilityRole="button"
onPressIn={e => {
'worklet'
if (onPressIn) {
runOnJS(onPressIn)(e)
onPressIn(e)
}
cancelAnimation(scale)
scale.value = withTiming(targetScale, {duration: 100})
scale.set(() => withTiming(targetScale, {duration: 100}))
}}
onPressOut={e => {
'worklet'
if (onPressOut) {
runOnJS(onPressOut)(e)
onPressOut(e)
}
cancelAnimation(scale)
scale.value = withTiming(1, {duration: 100})
scale.set(() => withTiming(1, {duration: 100}))
}}
style={[!reducedMotion && animatedStyle, style]}
{...rest}>
+15 -2
View File
@@ -1,4 +1,5 @@
import {useServiceConfigQuery} from '#/state/queries/email-verification-required'
import {useProfileQuery} from '#/state/queries/profile'
import {useSession} from '#/state/session'
import {BSKY_SERVICE} from '../constants'
import {getHostnameFromUrl} from '../strings/url-helpers'
@@ -7,13 +8,25 @@ export function useEmail() {
const {currentAccount} = useSession()
const {data: serviceConfig} = useServiceConfigQuery()
const {data: profile} = useProfileQuery({did: currentAccount?.did})
const checkEmailConfirmed = !!serviceConfig?.checkEmailConfirmed
// Date set for 11 AM PST on the 18th of November
const isNewEnough =
!!profile?.createdAt &&
Date.parse(profile.createdAt) >= Date.parse('2024-11-18T19:00:00.000Z')
const isSelfHost =
serviceConfig?.checkEmailConfirmed &&
currentAccount &&
getHostnameFromUrl(currentAccount.service) !==
getHostnameFromUrl(BSKY_SERVICE)
const needsEmailVerification = !isSelfHost && !currentAccount?.emailConfirmed
const needsEmailVerification =
!isSelfHost &&
checkEmailConfirmed &&
!currentAccount?.emailConfirmed &&
isNewEnough
return {needsEmailVerification}
}
+39
View File
@@ -0,0 +1,39 @@
import {useState} from 'react'
import {AnimatedRef, measure, MeasuredDimensions} from 'react-native-reanimated'
export type HandleRef = {
(node: any): void
current: null | number
}
// This is a lighterweight alternative to `useAnimatedRef()` for imperative UI thread actions.
// Render it like <View ref={ref} />, then pass `ref.current` to `measureHandle()` and such.
export function useHandleRef(): HandleRef {
return useState(() => {
const ref = (node: any) => {
if (node) {
ref.current =
node._nativeTag ??
node.__nativeTag ??
node.canonical?.nativeTag ??
null
} else {
ref.current = null
}
}
ref.current = null
return ref
})[0] as HandleRef
}
// When using this version, you need to read ref.current on the JS thread, and pass it to UI.
export function measureHandle(
current: number | null,
): MeasuredDimensions | null {
'worklet'
if (current !== null) {
return measure((() => current) as AnimatedRef<any>)
} else {
return null
}
}
+11 -9
View File
@@ -10,15 +10,16 @@ export function useMinimalShellHeaderTransform() {
const {headerHeight} = useShellLayout()
const headerTransform = useAnimatedStyle(() => {
const headerModeValue = headerMode.get()
return {
pointerEvents: headerMode.value === 0 ? 'auto' : 'none',
opacity: Math.pow(1 - headerMode.value, 2),
pointerEvents: headerModeValue === 0 ? 'auto' : 'none',
opacity: Math.pow(1 - headerModeValue, 2),
transform: [
{
translateY: interpolate(
headerMode.value,
headerModeValue,
[0, 1],
[0, -headerHeight.value],
[0, -headerHeight.get()],
),
},
],
@@ -33,15 +34,16 @@ export function useMinimalShellFooterTransform() {
const {footerHeight} = useShellLayout()
const footerTransform = useAnimatedStyle(() => {
const footerModeValue = footerMode.get()
return {
pointerEvents: footerMode.value === 0 ? 'auto' : 'none',
opacity: Math.pow(1 - footerMode.value, 2),
pointerEvents: footerModeValue === 0 ? 'auto' : 'none',
opacity: Math.pow(1 - footerModeValue, 2),
transform: [
{
translateY: interpolate(
footerMode.value,
footerModeValue,
[0, 1],
[0, footerHeight.value],
[0, footerHeight.get()],
),
},
],
@@ -58,7 +60,7 @@ export function useMinimalShellFabTransform() {
return {
transform: [
{
translateY: interpolate(footerMode.value, [0, 1], [-44, 0]),
translateY: interpolate(footerMode.get(), [0, 1], [-44, 0]),
},
],
}
+23 -1
View File
@@ -4,12 +4,14 @@ import * as WebBrowser from 'expo-web-browser'
import {
createBskyAppAbsoluteUrl,
isBskyAppUrl,
isBskyRSSUrl,
isRelativeUrl,
} from '#/lib/strings/url-helpers'
import {isNative} from '#/platform/detection'
import {useModalControls} from '#/state/modals'
import {useInAppBrowser} from '#/state/preferences/in-app-browser'
import {useOptOutOfUtm} from '#/state/preferences/opt-out-of-utm'
import {useTheme} from '#/alf'
import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
@@ -18,6 +20,7 @@ export function useOpenLink() {
const enabled = useInAppBrowser()
const t = useTheme()
const sheetWrapper = useSheetWrapper()
const optOutOfUtm = useOptOutOfUtm()
const openLink = useCallback(
async (url: string, override?: boolean) => {
@@ -26,6 +29,9 @@ export function useOpenLink() {
}
if (isNative && !url.startsWith('mailto:')) {
if (!optOutOfUtm && !isBskyAppUrl(url) && url.startsWith('http')) {
url = addUtmSource(url)
}
if (override === undefined && enabled === undefined) {
openModal({
name: 'in-app-browser-consent',
@@ -47,8 +53,24 @@ export function useOpenLink() {
}
Linking.openURL(url)
},
[enabled, openModal, t, sheetWrapper],
[enabled, openModal, t, sheetWrapper, optOutOfUtm],
)
return openLink
}
function addUtmSource(url: string): string {
let parsedUrl
try {
parsedUrl = new URL(url)
} catch (e) {
return url
}
if (!parsedUrl.searchParams.has('utm_source')) {
parsedUrl.searchParams.set('utm_source', 'bluesky')
if (!parsedUrl.searchParams.has('utm_medium')) {
parsedUrl.searchParams.set('utm_medium', 'social')
}
}
return parsedUrl.toString()
}
-1
View File
@@ -1,4 +1,3 @@
import React from 'react'
import {StyleProp, TextStyle, ViewStyle} from 'react-native'
import Svg, {Ellipse, Line, Path, Rect} from 'react-native-svg'
+4
View File
@@ -32,6 +32,8 @@ export function mimeToExt(mimeType: SupportedMimeTypes | (string & {})) {
return 'mpeg'
case 'video/quicktime':
return 'mov'
case 'image/gif':
return 'gif'
default:
throw new Error(`Unsupported mime type: ${mimeType}`)
}
@@ -47,6 +49,8 @@ export function extToMime(ext: string) {
return 'video/mpeg'
case 'mov':
return 'video/quicktime'
case 'gif':
return 'image/gif'
default:
throw new Error(`Unsupported file extension: ${ext}`)
}
+15 -15
View File
@@ -21,11 +21,11 @@ export type LogEvents = {
context: 'StartOnboarding' | 'AfterOnboarding' | 'Login' | 'Home'
status: 'granted' | 'denied' | 'undetermined'
}
'state:background:sampled': {
'state:background': {
secondsActive: number
}
'state:foreground:sampled': {}
'router:navigate:notifications:sampled': {}
'state:foreground': {}
'router:navigate:notifications': {}
'deepLink:referrerReceived': {
to: string
referrer: string
@@ -76,7 +76,7 @@ export type LogEvents = {
'onboarding:finished:avatarResult': {
avatarResult: 'default' | 'created' | 'uploaded'
}
'home:feedDisplayed:sampled': {
'home:feedDisplayed': {
feedUrl: string
feedType: string
index: number
@@ -87,12 +87,12 @@ export type LogEvents = {
| 'desktop-sidebar-click'
| 'starter-pack-initial-feed'
}
'feed:endReached:sampled': {
'feed:endReached': {
feedUrl: string
feedType: string
itemCount: number
}
'feed:refresh:sampled': {
'feed:refresh': {
feedUrl: string
feedType: string
reason: 'pull-to-refresh' | 'soft-reset' | 'load-latest'
@@ -103,13 +103,13 @@ export type LogEvents = {
'discover:showLess': {
feedContext: string
}
'discover:clickthrough:sampled': {
'discover:clickthrough': {
count: number
}
'discover:engaged:sampled': {
'discover:engaged': {
count: number
}
'discover:seen:sampled': {
'discover:seen': {
count: number
}
@@ -132,27 +132,27 @@ export type LogEvents = {
postCount: number
isReply: boolean
}
'post:like:sampled': {
'post:like': {
doesLikerFollowPoster: boolean | undefined
doesPosterFollowLiker: boolean | undefined
likerClout: number | undefined
postClout: number | undefined
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
}
'post:repost:sampled': {
'post:repost': {
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
}
'post:unlike:sampled': {
'post:unlike': {
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
}
'post:unrepost:sampled': {
'post:unrepost': {
logContext: 'FeedItem' | 'PostThreadItem' | 'Post'
}
'post:mute': {}
'post:unmute': {}
'post:pin': {}
'post:unpin': {}
'profile:follow:sampled': {
'profile:follow': {
didBecomeMutual: boolean | undefined
followeeClout: number | undefined
followerClout: number | undefined
@@ -169,7 +169,7 @@ export type LogEvents = {
| 'FeedInterstitial'
| 'ProfileHeaderSuggestedFollows'
}
'profile:unfollow:sampled': {
'profile:unfollow': {
logContext:
| 'RecommendedFollowsItem'
| 'PostThreadItem'
+3 -39
View File
@@ -59,6 +59,7 @@ function createStatsigOptions(prefetchUsers: StatsigUser[]) {
initTimeoutMs: 1,
// Get fresh flags for other accounts as well, if any.
prefetchUsers,
api: 'https://events.bsky.app/v2',
}
}
@@ -89,51 +90,14 @@ export function toClout(n: number | null | undefined): number | undefined {
}
}
const DOWNSAMPLE_RATE = 0.99 // 99% likely
const DOWNSAMPLED_EVENTS: Set<keyof LogEvents> = new Set([
'router:navigate:notifications:sampled',
'state:background:sampled',
'state:foreground:sampled',
'home:feedDisplayed:sampled',
'feed:endReached:sampled',
'feed:refresh:sampled',
'discover:clickthrough:sampled',
'discover:engaged:sampled',
'discover:seen:sampled',
'post:like:sampled',
'post:unlike:sampled',
'post:repost:sampled',
'post:unrepost:sampled',
'profile:follow:sampled',
'profile:unfollow:sampled',
])
const isDownsampledSession = Math.random() < DOWNSAMPLE_RATE
export function logEvent<E extends keyof LogEvents>(
eventName: E & string,
rawMetadata: LogEvents[E] & FlatJSONRecord,
) {
try {
if (
process.env.NODE_ENV === 'development' &&
eventName.endsWith(':sampled') &&
!DOWNSAMPLED_EVENTS.has(eventName)
) {
logger.error(
'Did you forget to add ' + eventName + ' to DOWNSAMPLED_EVENTS?',
)
}
const isDownsampledEvent = DOWNSAMPLED_EVENTS.has(eventName)
if (isDownsampledSession && isDownsampledEvent) {
return
}
const fullMetadata = {
...rawMetadata,
} as Record<string, string> // Statsig typings are unnecessarily strict here.
if (isDownsampledEvent) {
fullMetadata.downsampleRate = DOWNSAMPLE_RATE.toString()
}
fullMetadata.routeName = getCurrentRouteName() ?? '(Uninitialized)'
if (Statsig.initializeCalled()) {
Statsig.logEvent(eventName, null, fullMetadata)
@@ -232,13 +196,13 @@ AppState.addEventListener('change', (state: AppStateStatus) => {
lastState = state
if (state === 'active') {
lastActive = performance.now()
logEvent('state:foreground:sampled', {})
logEvent('state:foreground', {})
} else {
let secondsActive = 0
if (lastActive != null) {
secondsActive = Math.round((performance.now() - lastActive) / 1e3)
lastActive = null
logEvent('state:background:sampled', {
logEvent('state:background', {
secondsActive,
})
}
+10 -4
View File
@@ -1,3 +1,5 @@
import {t} from '@lingui/macro'
export function cleanError(str: any): string {
if (!str) {
return ''
@@ -6,13 +8,17 @@ export function cleanError(str: any): string {
str = str.toString()
}
if (isNetworkError(str)) {
return 'Unable to connect. Please check your internet connection and try again.'
return t`Unable to connect. Please check your internet connection and try again.`
}
if (str.includes('Upstream Failure')) {
return 'The server appears to be experiencing issues. Please try again in a few moments.'
if (
str.includes('Upstream Failure') ||
str.includes('NotEnoughResources') ||
str.includes('pipethrough network error')
) {
return t`The server appears to be experiencing issues. Please try again in a few moments.`
}
if (str.includes('Bad token scope')) {
return 'This feature is not available while using an App Password. Please sign in with your main password.'
return t`This feature is not available while using an App Password. Please sign in with your main password.`
}
if (str.startsWith('Error: ')) {
return str.slice('Error: '.length)
+24 -3
View File
@@ -13,6 +13,12 @@ type LocalWithLanguageCode = Locale & {
*
* {@link https://github.com/bluesky-social/social-app/pull/4461}
* {@link https://xml.coverpages.org/iso639a.html}
*
* Convert Chinese language tags for Native.
*
* {@link https://datatracker.ietf.org/doc/html/rfc5646#appendix-A}
* {@link https://developer.apple.com/documentation/packagedescription/languagetag}
* {@link https://gist.github.com/amake/0ac7724681ac1c178c6f95a5b09f03ce#new-locales-vs-old-locales-chinese}
*/
export function getLocales() {
const locales = defaultGetLocales?.() ?? []
@@ -32,10 +38,25 @@ export function getLocales() {
// yiddish
locale.languageCode = 'yi'
}
// @ts-ignore checked above
output.push(locale)
}
if (typeof locale.languageTag === 'string') {
if (locale.languageTag.startsWith('zh-Hans')) {
// Simplified Chinese to zh-CN
locale.languageTag = 'zh-CN'
}
if (locale.languageTag.startsWith('zh-Hant')) {
// Traditional Chinese to zh-TW
locale.languageTag = 'zh-TW'
}
if (locale.languageTag.startsWith('yue')) {
// Cantonese (Yue) to zh-HK
locale.languageTag = 'zh-HK'
}
}
// @ts-ignore checked above
output.push(locale)
}
return output
+2
View File
@@ -145,6 +145,8 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
return AppLanguage.ja
case 'ko':
return AppLanguage.ko
case 'pl':
return AppLanguage.pl
case 'pt-BR':
return AppLanguage.pt_BR
case 'ru':
+10 -1
View File
@@ -25,6 +25,7 @@ import {messages as messagesId} from '#/locale/locales/id/messages'
import {messages as messagesIt} from '#/locale/locales/it/messages'
import {messages as messagesJa} from '#/locale/locales/ja/messages'
import {messages as messagesKo} from '#/locale/locales/ko/messages'
import {messages as messagesPl} from '#/locale/locales/pl/messages'
import {messages as messagesPt_BR} from '#/locale/locales/pt-BR/messages'
import {messages as messagesRu} from '#/locale/locales/ru/messages'
import {messages as messagesTh} from '#/locale/locales/th/messages'
@@ -144,6 +145,14 @@ export async function dynamicActivate(locale: AppLanguage) {
])
break
}
case AppLanguage.pl: {
i18n.loadAndActivate({locale, messages: messagesPl})
await Promise.all([
import('@formatjs/intl-pluralrules/locale-data/pl'),
import('@formatjs/intl-numberformat/locale-data/pl'),
])
break
}
case AppLanguage.pt_BR: {
i18n.loadAndActivate({locale, messages: messagesPt_BR})
await Promise.all([
@@ -215,7 +224,7 @@ export async function dynamicActivate(locale: AppLanguage) {
}
}
export async function useLocaleLanguage() {
export function useLocaleLanguage() {
const {appLanguage} = useLanguagePrefs()
useEffect(() => {
dynamicActivate(sanitizeAppLanguageSetting(appLanguage))
+4
View File
@@ -64,6 +64,10 @@ export async function dynamicActivate(locale: AppLanguage) {
mod = await import(`./locales/ko/messages`)
break
}
case AppLanguage.pl: {
mod = await import(`./locales/pl/messages`)
break
}
case AppLanguage.pt_BR: {
mod = await import(`./locales/pt-BR/messages`)
break
+2
View File
@@ -19,6 +19,7 @@ export enum AppLanguage {
it = 'it',
ja = 'ja',
ko = 'ko',
pl = 'pl',
pt_BR = 'pt-BR',
ru = 'ru',
th = 'th',
@@ -49,6 +50,7 @@ export const APP_LANGUAGES: AppLanguageConfig[] = [
{code2: AppLanguage.it, name: 'Italiano Italian'},
{code2: AppLanguage.ja, name: '日本語 Japanese'},
{code2: AppLanguage.ko, name: '한국어 Korean'},
{code2: AppLanguage.pl, name: 'Polski Polish'},
{code2: AppLanguage.pt_BR, name: 'Português (BR) Portuguese (BR)'},
{code2: AppLanguage.ru, name: 'Русский Russian'},
{code2: AppLanguage.th, name: 'ภาษาไทย Thai'},

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