Compare commits

..

77 Commits

Author SHA1 Message Date
Ducky fa997fa7c1 upstream:main → zio/dev 2024-09-19 06:18:51 +01:00
Eric Bailey bda355fd58
Fix font loading on web (#5412)
* Copy font files during build

* Fall back if error loading fonts
2024-09-18 20:22:03 -05:00
Eric Bailey cbc7cd0808
[Neue] Base (#5395)
* Add fontScale, gate it, fix some computes

* Add inter, integrate

* Clean up

* Apply to old Text component

* Use numeric weight

* Cleanup

* Clean up appearance settings

* Global tracking

* Fix regular italic variant

* Refactor settings and fontScale values

* Remove flags

* Get rid of lower weight font usage

* Remove gate from settings

* Refactor appearance settings for reuse

* Add neue type nux

* Update defaults

* Load fonts, add fallback families

* Load fonts via plugin in production

* Fixes

* Fix for web

* Nits

---------

Co-authored-by: Hailey <me@haileyok.com>
2024-09-18 19:35:34 -05:00
Hailey fb3be79820
Update sentry sourcemaps upload (#5409) 2024-09-18 14:51:14 -07:00
Eric Bailey 6c8ef69654
Fix for undefined ref on hot reload on web (#5407) 2024-09-18 15:45:20 -05:00
Hailey 41d4b2c7ef
remove expo-sentry (#5405) 2024-09-18 09:34:29 -07:00
Hailey f45f7148ee
Revert unneeded changes to `expo-modules-core` patch (#5393) 2024-09-18 08:42:42 -07:00
Hailey 8d560de44b
Temporary Sentry hack patch for iOS 18 (#5400) 2024-09-18 11:25:32 +01:00
Samuel Newman 67895f7f99
Make it work with Xcode 16 (#5386) 2024-09-17 12:27:22 -07:00
Eric Bailey b2b5be542a
Fix border radius on avatars (#5392) 2024-09-17 12:07:19 -07:00
Roscoe Rubin-Rottenberg 751375ce3c
Make like animation on web same speed as mobile (#5391) 2024-09-17 12:07:09 -07:00
Eric Bailey 2745cba3ea
Pre-fill alt text with 10-million card post (#5389)
* Pre-fill alt text with 10-million card post (#5377)

* Clean up type

* Tweak alt copy

* Add pt translation, fix typo

---------

Co-authored-by: Calvin <clavin@users.noreply.github.com>
2024-09-17 13:55:19 -05:00
surfdude29 8f98d6b12f
Tweak `pt-BR` string (#5372) 2024-09-17 11:34:48 -07:00
Hailey 342919bc4e
Bump 1.91.2 (#5387) 2024-09-17 10:00:31 -07:00
Hailey 0414b95c58
[Video] Additional android fixes (#5373)
* rm unused code

* bump lib

* invert bool
2024-09-17 14:04:59 +09:00
Eric Bailey b69fd23456
Milly tweaks (#5365)
Co-authored-by: Hailey <me@haileyok.com>
2024-09-16 14:52:28 -07:00
Samuel Newman 8daf6b7868
[Video] Fix safari showing spinner (#5364) 2024-09-16 14:22:22 -07:00
Samuel Newman 8241747fc2
[Video] Volume controls on web (#5363)
* split up VideoWebControls

* add basic slider

* logarithmic volume

* integrate mute state

* fix typo

* shared video volume

* rm log

* animate in/out

* disable for touch devices

* remove flicker on touch devices

* more detailed comment

* move into correct context provider

* add minHeight

* hack

* bettern umber

---------

Co-authored-by: Hailey <me@haileyok.com>
2024-09-16 21:37:33 +01:00
Kevin Scannell 38c8f01594
Updates to Irish translation, back to 100% (#5345) 2024-09-16 12:08:26 -07:00
Ivan Beà b5d8ce114d
Update catalan messages.po (#5223)
Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
Co-authored-by: gsmt <samaritanojr006@gmail.com>
2024-09-16 12:00:26 -07:00
Takayuki KUSANO 4a0e2e3a05
Update Japanese translation (#5217) 2024-09-16 11:59:40 -07:00
Minseo Lee 7dfbd5d7d3
Update Korean localization (#5254) 2024-09-16 11:47:26 -07:00
Frudrax Cheng 08003a09ec
Update Chinese localization (#5291)
Co-authored-by: cirx <133132480+cirx1e@users.noreply.github.com>
Co-authored-by: Kuwa Lee <kuwalee1069@gmail.com>
2024-09-16 11:46:54 -07:00
surfdude29 c371b70722
Update French localization (#5227)
Co-authored-by: Stanislas Signoud <signez@stanisoft.net>
2024-09-16 11:44:42 -07:00
surfdude29 06f0785a8c
Update Portuguese localization (#5256)
Co-authored-by: Henrique Marques <hfvmarques@gmail.com>
Co-authored-by: Arthur Tavares <arthurabreu00@gmail.com>
2024-09-16 11:36:39 -07:00
Samuel Newman 75e3f51c10
[Video] Fix scrubber tap target (#5360)
* put padding on correct element

* clear timeout on down
2024-09-16 18:15:07 +01:00
Samuel Newman d62e14eca1
loosen checks on vtt file (#5359) 2024-09-16 17:29:34 +01:00
Eric Bailey 0681727b64
Remove overflow hidden from external link outer el (#5356)
* Remove overflow hidden

* Borders when no thumb

* Fix overflow, add bg to no-thumb state

* Cleanup
2024-09-15 15:42:45 -05:00
Eric Bailey 42b28fec9d Disallow some font scaling 2024-09-15 15:11:04 -05:00
Eric Bailey 61deab7051
Nux after onboarding (#5357)
* Don't show nux dialogs until post-onboarding

* Don't show if over 10M
2024-09-15 14:58:10 -05:00
dan d6c11a7231
Fix wrong empty state for liked by (#5343) 2024-09-15 12:14:46 -07:00
Hailey 55da2704d8
[UITextView] Add background color support to iOS selectable text (#5335) 2024-09-15 12:11:55 -07:00
surfdude29 2a344d8f12
Add context to `Unmute` and `Mute` strings (#5340)
Co-authored-by: Hailey <me@haileyok.com>
2024-09-15 12:11:44 -07:00
Hailey f8658f0795
bump (#5348) 2024-09-14 19:38:51 +01:00
Paul Frazee 701ddfb171
Release 1.91.1 prep (#5339)
* Fixes to tests

* intl extract
2024-09-13 16:55:01 -07:00
Hailey d8b80309bd
[Video] Fix regression on audio session at launch (#5338) 2024-09-13 16:48:17 -07:00
Eric Bailey e767c50f65
Don't open composer via hotkey if other dialog is already open (#5334)
* Don't open composer via hotkey if other dialog is already open

* Check for lightbox also

* Check for drawer
2024-09-13 18:14:46 -05:00
Eric Bailey d76f9abdd7
"N" keyboard shortcut to open a new post modal (#5197)
* feat: Add hook on web app to open composer with 'N' keyboard shortcut

* Extract, don't fire open composer if already open

* Ignore interactive elements

---------

Co-authored-by: João Gabriel <joaog@nocorp.io>
Co-authored-by: Hailey <me@haileyok.com>
2024-09-13 16:48:28 -05:00
Hailey cac43127f0
[Video] Bump video (#5333) 2024-09-13 14:46:02 -07:00
dan ce3893d816
Apply Following settings to Lists (#5313)
* Apply Following settings to Lists

* Remove dead code
2024-09-13 22:30:09 +01:00
Eric Bailey 88813f57c9
Always display next button on login page (#5326)
Co-authored-by: Vinícius Souza <39967235+vinifsouza@users.noreply.github.com>
Co-authored-by: Hailey <me@haileyok.com>
2024-09-13 14:21:45 -07:00
Hailey 533382173c
[Video] Don't require email verification on self-host (#5332) 2024-09-13 14:08:45 -07:00
Hailey 843f9925f5
[Video] Remember mute state while scrolling (#5331) 2024-09-13 14:07:13 -07:00
dan 791bc7afbe
Fix lexicon validation in PWI Discover (#5329) 2024-09-13 21:11:17 +01:00
Hailey 26508cfe6a
[Video] Remove `expo-video`, use `bluesky-video` (#5282)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2024-09-13 12:44:42 -07:00
Eric Bailey 78a531f5ff
Disable pointer events on media border (#5327) 2024-09-13 19:02:47 +01:00
Paul Frazee 9163f676f5 Merge branch 'events' into main 2024-09-13 10:30:55 -07:00
Eric Bailey 08ac3a27c2 Add events 2024-09-13 12:05:15 -05:00
Eric Bailey b3381da1c1
Image/video border + tweaks (#5324)
* Image/video border (#5253)

* Update AutoSizedImage.tsx

* Update AutoSizedImage.tsx

* Update Gallery.tsx

* Update ExternalLinkEmbed.tsx

* Update MediaPreview.tsx

* Update UserAvatar.tsx

* Update ExternalLinkEmbed.tsx

* Update ExternalPlayerEmbed.tsx

* Update ExternalGifEmbed.tsx

* Update GifEmbed.tsx

* Update ExternalGifEmbed.tsx

* Update GifEmbed.tsx

* Update UserAvatar.tsx

* Update ExternalPlayerEmbed.tsx

* Update ExternalPlayerEmbed.tsx

* video

* Update QuoteEmbed.tsx

* Tweaks, abstract components

---------

Co-authored-by: Minseo Lee <itoupluk427@gmail.com>
2024-09-13 18:02:58 +01:00
Paul Frazee c7231537f1 Merge branch 'ten-milly' into main 2024-09-13 08:57:41 -07:00
dan 1dc7ef137c
Fix notification->post jump for real (#5314)
* Revert "Fix notification scroll jump (#5297)"

This reverts commit e0d9e75407.

* Query notifications first
2024-09-13 15:01:09 +01:00
Eric Bailey 0315814edd
Separate alt/crop, use new icon (#5321) 2024-09-13 14:56:54 +01:00
Eric Bailey b47bac965f Merge remote-tracking branch 'origin/ten-milly-android-save' into ten-milly
* origin/ten-milly-android-save:
  On android, change ten milly nux secondary action to save instead of share
2024-09-12 21:24:57 -05:00
Eric Bailey 4637c66390 Let display name wrap 2024-09-12 21:24:32 -05:00
Eric Bailey 003f9e06d4 Fallback snoozing 2024-09-12 21:24:32 -05:00
Paul Frazee 36ac551347 Add ja and pt-BR translations 2024-09-12 13:11:30 -07:00
Paul Frazee 7acf0e1284 On android, change ten milly nux secondary action to save instead of share 2024-09-12 12:57:08 -07:00
Eric Bailey cc8e7b5ae5 Handle display name 2024-09-12 13:56:25 -05:00
Eric Bailey 1f2e4b26c0 Handle overflow of bottom text 2024-09-12 13:54:20 -05:00
Eric Bailey 7bba213e1a Add retry 2024-09-12 13:45:58 -05:00
Eric Bailey bd79ce7ea0 Ensure dialog shows for all accounts without snoozing 2024-09-12 13:45:06 -05:00
Eric Bailey 46402fd010 Add gate 2024-09-12 13:14:51 -05:00
Eric Bailey 63f85f6493 Copy 2024-09-12 08:03:58 -05:00
Eric Bailey c99e43d6c6 Protect against other exit methods, protect against multiple fetches 2024-09-11 21:56:20 -05:00
Eric Bailey 45c8d89d92 Protect against 3p PDSs and bad responses 2024-09-11 21:52:26 -05:00
Eric Bailey 6e78ce53d7 Dev helpers, string cleanup 2024-09-11 21:47:25 -05:00
Eric Bailey c8b133863d Fix some nux types 2024-09-11 21:28:34 -05:00
Eric Bailey 9bb385a4dd Refactor, integrate nux, snoozing 2024-09-11 21:20:39 -05:00
Eric Bailey 63444052e8 Rename 2024-09-11 20:01:27 -05:00
Eric Bailey 77d60a5b80 Hook up data 2024-09-11 19:58:17 -05:00
Eric Bailey f8edd11bc5 Don't open for logged out users 2024-09-11 19:58:17 -05:00
Eric Bailey 2ee68e4f4d Copy 2024-09-11 19:58:16 -05:00
Eric Bailey 11ecea22d4 Add badges, clean up spacing 2024-09-11 19:58:16 -05:00
Eric Bailey f7db14f32f Disable avi 2024-09-11 19:58:16 -05:00
Eric Bailey eaf0081623 WIP, avi not working on web 2024-09-11 19:58:16 -05:00
Eric Bailey 3c8b3b4782 Progress on desktoip 2024-09-11 19:58:16 -05:00
Eric Bailey 76c584d981 WIP 2024-09-11 19:58:16 -05:00
142 changed files with 11153 additions and 8548 deletions

View File

@ -74,8 +74,7 @@ appId: xyz.blueskyweb.app
- tapOn: "Delete List" - tapOn: "Delete List"
- tapOn: - tapOn:
id: "confirmBtn" id: "confirmBtn"
- assertVisible: - assertVisible: "This list is empty!"
id: "listsEmpty"
- tapOn: - tapOn:
label: "Create a new curatelist" label: "Create a new curatelist"
@ -161,17 +160,6 @@ appId: xyz.blueskyweb.app
- assertNotVisible: - assertNotVisible:
id: "userAddRemoveListsModal" id: "userAddRemoveListsModal"
- tapOn:
label: "Shows the curatelist on my profile"
id: "bottomBarProfileBtn"
- swipe:
from:
id: "profilePager-selector"
direction: LEFT
- tapOn:
id: "profilePager-selector-6"
- tapOn: "Good Ppl"
- tapOn: - tapOn:
label: "Adds and removes users on curatelists from the profile" label: "Adds and removes users on curatelists from the profile"
id: "bottomBarSearchBtn" id: "bottomBarSearchBtn"

View File

@ -21,14 +21,12 @@ appId: xyz.blueskyweb.app
id: "likeBtn" id: "likeBtn"
childOf: childOf:
id: "postThreadItem-by-bob.test" id: "postThreadItem-by-bob.test"
- assertVisible: - assertVisible: "1 like"
id: "likeCount-expanded"
- tapOn: - tapOn:
id: "likeBtn" id: "likeBtn"
childOf: childOf:
id: "postThreadItem-by-bob.test" id: "postThreadItem-by-bob.test"
- assertNotVisible: - assertNotVisible: "1 like"
id: "likeCount-expanded"
# Can like a reply post # Can like a reply post
- tapOn: - tapOn:

View File

@ -55,6 +55,7 @@ module.exports = function (config) {
: undefined : undefined
const UPDATES_ENABLED = !!UPDATES_CHANNEL const UPDATES_ENABLED = !!UPDATES_CHANNEL
const USE_SENTRY = Boolean(process.env.SENTRY_AUTH_TOKEN)
const SENTRY_DIST = `${PLATFORM}.${VERSION}.${IS_TESTFLIGHT ? 'tf' : ''}${ const SENTRY_DIST = `${PLATFORM}.${VERSION}.${IS_TESTFLIGHT ? 'tf' : ''}${
IS_DEV ? 'dev' : '' IS_DEV ? 'dev' : ''
}` }`
@ -186,7 +187,15 @@ module.exports = function (config) {
}, },
plugins: [ plugins: [
'expo-localization', 'expo-localization',
Boolean(process.env.SENTRY_AUTH_TOKEN) && 'sentry-expo', USE_SENTRY && [
'@sentry/react-native/expo',
{
organization: 'blueskyweb',
project: 'react-native',
release: VERSION,
dist: SENTRY_DIST,
},
],
[ [
'expo-build-properties', 'expo-build-properties',
{ {
@ -211,7 +220,6 @@ module.exports = function (config) {
sounds: PLATFORM === 'ios' ? ['assets/dm.aiff'] : ['assets/dm.mp3'], sounds: PLATFORM === 'ios' ? ['assets/dm.aiff'] : ['assets/dm.mp3'],
}, },
], ],
'expo-video',
'react-native-compressor', 'react-native-compressor',
'./plugins/starterPackAppClipExtension/withStarterPackAppClip.js', './plugins/starterPackAppClipExtension/withStarterPackAppClip.js',
'./plugins/withAndroidManifestPlugin.js', './plugins/withAndroidManifestPlugin.js',
@ -222,6 +230,31 @@ module.exports = function (config) {
'./plugins/shareExtension/withShareExtensions.js', './plugins/shareExtension/withShareExtensions.js',
'./plugins/notificationsExtension/withNotificationsExtension.js', './plugins/notificationsExtension/withNotificationsExtension.js',
'./plugins/withAppDelegateReferrer.js', './plugins/withAppDelegateReferrer.js',
[
'expo-font',
{
fonts: [
// './assets/fonts/inter/Inter-Thin.otf',
// './assets/fonts/inter/Inter-ThinItalic.otf',
// './assets/fonts/inter/Inter-ExtraLight.otf',
// './assets/fonts/inter/Inter-ExtraLightItalic.otf',
// './assets/fonts/inter/Inter-Light.otf',
// './assets/fonts/inter/Inter-LightItalic.otf',
'./assets/fonts/inter/Inter-Regular.otf',
'./assets/fonts/inter/Inter-Italic.otf',
'./assets/fonts/inter/Inter-Medium.otf',
'./assets/fonts/inter/Inter-MediumItalic.otf',
'./assets/fonts/inter/Inter-SemiBold.otf',
'./assets/fonts/inter/Inter-SemiBoldItalic.otf',
'./assets/fonts/inter/Inter-Bold.otf',
'./assets/fonts/inter/Inter-BoldItalic.otf',
'./assets/fonts/inter/Inter-ExtraBold.otf',
'./assets/fonts/inter/Inter-ExtraBoldItalic.otf',
'./assets/fonts/inter/Inter-Black.otf',
'./assets/fonts/inter/Inter-BlackItalic.otf',
],
},
],
].filter(Boolean), ].filter(Boolean),
extra: { extra: {
eas: { eas: {
@ -264,7 +297,7 @@ module.exports = function (config) {
* @see https://docs.expo.dev/guides/using-sentry/#app-configuration * @see https://docs.expo.dev/guides/using-sentry/#app-configuration
*/ */
{ {
file: 'sentry-expo/upload-sourcemaps', file: './postHooks/uploadSentrySourcemapsPostHook',
config: { config: {
organization: 'blueskyweb', organization: 'blueskyweb',
project: 'react-native', project: 'react-native',

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 3a1 1 0 0 1 1 1v8.086l1.793-1.793a1 1 0 1 1 1.414 1.414l-3.5 3.5a1 1 0 0 1-1.414 0l-3.5-3.5a1 1 0 1 1 1.414-1.414L11 12.086V4a1 1 0 0 1 1-1ZM4 14a1 1 0 0 1 1 1v4h14v-4a1 1 0 1 1 2 0v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-5a1 1 0 0 1 1-1Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 378 B

View File

@ -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="M9 5a1 1 0 0 1 1-1h12a1 1 0 1 1 0 2h-5v14a1 1 0 1 1-2 0V6h-5a1 1 0 0 1-1-1Zm-3.073 7v8a1 1 0 1 0 2 0v-8H12a1 1 0 1 0 0-2H6.971a1.015 1.015 0 0 0-.089 0H2a1 1 0 1 0 0 2h3.927Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 317 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M3.65 17.247c-.242.832-.632 1.178-1.325 1.178-.814 0-1.325-.476-1.325-1.23 0-.216.06-.51.173-.831L4.586 7.07c.364-1.014.979-1.482 1.966-1.482 1.022 0 1.629.45 2.001 1.473l3.43 9.303c.121.337.165.571.165.831 0 .72-.546 1.23-1.308 1.23-.736 0-1.126-.338-1.36-1.152l-.658-1.975H4.309l-.658 1.95ZM6.5 8.152l-1.62 5.12h3.335l-1.654-5.12H6.5Zm13.005 8.688c-.52.988-1.68 1.568-2.84 1.568-1.768 0-3.11-1.144-3.11-2.815 0-1.69 1.299-2.668 3.62-2.807l2.34-.138v-.615c0-.867-.607-1.369-1.56-1.369-.771 0-1.239.251-1.802.979-.277.312-.597.468-1.004.468-.615 0-1.057-.399-1.057-.97 0-.2.043-.382.13-.572.433-1.109 1.923-1.793 3.845-1.793 2.383 0 3.933 1.23 3.933 3.1v5.293c0 .84-.511 1.273-1.23 1.273-.684 0-1.16-.38-1.213-1.126v-.476h-.052Zm-3.43-1.386c0 .693.572 1.126 1.42 1.126 1.11 0 2.02-.719 2.02-1.723v-.676l-1.959.121c-.944.07-1.48.494-1.48 1.152Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 986 B

View File

@ -258,6 +258,51 @@
.force-no-clicks * { .force-no-clicks * {
pointer-events: none !important; pointer-events: none !important;
} }
input[type=range][orient=vertical] {
writing-mode: vertical-lr;
direction: rtl;
appearance: slider-vertical;
width: 16px;
vertical-align: bottom;
-webkit-appearance: none;
appearance: none;
background: transparent;
cursor: pointer;
}
input[type="range"][orient=vertical]::-webkit-slider-runnable-track {
background: white;
height: 100%;
width: 4px;
border-radius: 4px;
}
input[type="range"][orient=vertical]::-moz-range-track {
background: white;
height: 100%;
width: 4px;
border-radius: 4px;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
border-radius: 50%;
background-color: white;
height: 16px;
width: 16px;
margin-left: -6px;
}
input[type="range"][orient=vertical]::-moz-range-thumb {
border: none;
border-radius: 50%;
background-color: white;
height: 16px;
width: 16px;
margin-left: -6px;
}
</style> </style>
{% include "scripts.html" %} {% include "scripts.html" %}
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png"> <link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">

View File

@ -0,0 +1,36 @@
diff --git a/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js b/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js
index 7e0b4cd..177454c 100644
--- a/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js
+++ b/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js
@@ -3,6 +3,8 @@ import { LogBox } from 'react-native';
* This is a workaround for using fetch on RN, this is a known issue in react-native and only generates a warning.
*/
export function ignoreRequireCycleLogs() {
- LogBox.ignoreLogs(['Require cycle:']);
+ try {
+ LogBox.ignoreLogs(['Require cycle:']);
+ } catch (e) {}
}
//# sourceMappingURL=ignorerequirecyclelogs.js.map
\ No newline at end of file
diff --git a/node_modules/@sentry/react-native/scripts/expo-upload-sourcemaps.js b/node_modules/@sentry/react-native/scripts/expo-upload-sourcemaps.js
index 0f244f2..ae7dfb3 100755
--- a/node_modules/@sentry/react-native/scripts/expo-upload-sourcemaps.js
+++ b/node_modules/@sentry/react-native/scripts/expo-upload-sourcemaps.js
@@ -174,6 +174,7 @@ if (!outputDir) {
process.exit(1);
}
+const otherArgs = process.argv.slice(3);
const files = getAssetPathsSync(outputDir);
const groupedAssets = groupAssets(files);
@@ -195,7 +196,7 @@ for (const [assetGroupName, assets] of Object.entries(groupedAssets)) {
const isHermes = assets.find(asset => asset.endsWith('.hbc'));
const windowsCallback = process.platform === "win32" ? 'node ' : '';
- execSync(`${windowsCallback}${sentryCliBin} sourcemaps upload ${isHermes ? '--debug-id-reference' : ''} ${assets.join(' ')}`, {
+ execSync(`${windowsCallback}${sentryCliBin} sourcemaps upload ${isHermes ? '--debug-id-reference' : ''} ${assets.join(' ')} ${otherArgs.join(' ')}`, {
env: {
...process.env,
[SENTRY_PROJECT]: sentryProject,

View File

@ -1,15 +0,0 @@
diff --git a/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js b/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js
index 7e0b4cd..177454c 100644
--- a/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js
+++ b/node_modules/@sentry/react-native/dist/js/utils/ignorerequirecyclelogs.js
@@ -3,6 +3,8 @@ import { LogBox } from 'react-native';
* This is a workaround for using fetch on RN, this is a known issue in react-native and only generates a warning.
*/
export function ignoreRequireCycleLogs() {
- LogBox.ignoreLogs(['Require cycle:']);
+ try {
+ LogBox.ignoreLogs(['Require cycle:']);
+ } catch (e) {}
}
//# sourceMappingURL=ignorerequirecyclelogs.js.map
\ No newline at end of file

View File

@ -12,28 +12,3 @@ index bb74e80..0aa0202 100644
Map<String, Object> constants = new HashMap<>(3); Map<String, Object> constants = new HashMap<>(3);
constants.put(MODULES_CONSTANTS_KEY, new HashMap<>()); constants.put(MODULES_CONSTANTS_KEY, new HashMap<>());
diff --git a/node_modules/expo-modules-core/build/uuid/uuid.js b/node_modules/expo-modules-core/build/uuid/uuid.js
index 109d3fe..c421931 100644
--- a/node_modules/expo-modules-core/build/uuid/uuid.js
+++ b/node_modules/expo-modules-core/build/uuid/uuid.js
@@ -1,5 +1,7 @@
import bytesToUuid from './lib/bytesToUuid';
import { Uuidv5Namespace } from './uuid.types';
+import { ensureNativeModulesAreInstalled } from '../ensureNativeModulesAreInstalled';
+ensureNativeModulesAreInstalled();
const nativeUuidv4 = globalThis?.expo?.uuidv4;
const nativeUuidv5 = globalThis?.expo?.uuidv5;
function uuidv4() {
diff --git a/node_modules/expo-modules-core/ios/Core/SharedObjects/SharedObjectRegistry.swift b/node_modules/expo-modules-core/ios/Core/SharedObjects/SharedObjectRegistry.swift
index ee2268a..4851b67 100644
--- a/node_modules/expo-modules-core/ios/Core/SharedObjects/SharedObjectRegistry.swift
+++ b/node_modules/expo-modules-core/ios/Core/SharedObjects/SharedObjectRegistry.swift
@@ -173,7 +173,7 @@ public final class SharedObjectRegistry {
}
internal func clear() {
- Self.lockQueue.async {
+ Self.lockQueue.sync {
self.pairs.removeAll()
}
}

View File

@ -0,0 +1,34 @@
const exec = require('child_process').execSync
const SENTRY_AUTH_TOKEN = process.env.SENTRY_AUTH_TOKEN
module.exports = ({config}) => {
if (!SENTRY_AUTH_TOKEN) {
console.log(
'SENTRY_AUTH_TOKEN environment variable must be set to upload sourcemaps. Skipping.',
)
return
}
const org = config.organization
const project = config.project
const release = config.release
const dist = config.dist
if (!org || !project || !release || !dist) {
console.log(
'"organization", "project", "release", and "dist" must be set in the hook config to upload sourcemaps. Skipping.',
)
return
}
try {
console.log('Uploading sourcemaps to Sentry...')
exec(
`node node_modules/@sentry/react-native/scripts/expo-upload-sourcemaps dist --url https://sentry.io/ -o ${org} -p ${project} -r ${release} -d ${dist}`,
)
console.log('Sourcemaps uploaded to Sentry.')
} catch (e) {
console.error('Error uploading sourcemaps to Sentry:', e)
}
}

View File

@ -52,17 +52,17 @@ import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies' import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
import {TestCtrls} from '#/view/com/testing/TestCtrls' import {TestCtrls} from '#/view/com/testing/TestCtrls'
import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoNativeContext' import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext'
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
import {Shell} from '#/view/shell' import {Shell} from '#/view/shell'
import {ThemeProvider as Alf} from '#/alf' import {ThemeProvider as Alf, useFonts} from '#/alf'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme' import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
import {NuxDialogs} from '#/components/dialogs/nuxs'
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs' import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
import {Provider as PortalProvider} from '#/components/Portal' import {Provider as PortalProvider} from '#/components/Portal'
import {Splash} from '#/Splash' import {Splash} from '#/Splash'
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
import {AudioCategory, PlatformInfo} from '../modules/expo-bluesky-swiss-army'
SplashScreen.preventAutoHideAsync() SplashScreen.preventAutoHideAsync()
@ -106,16 +106,15 @@ function InnerApp() {
}, [_]) }, [_])
return ( return (
<StatsigProvider
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<Alf theme={theme}> <Alf theme={theme}>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<Splash isReady={isReady && hasCheckedReferrer}> <Splash isReady={isReady && hasCheckedReferrer}>
<ActiveVideoProvider>
<RootSiblingParent> <RootSiblingParent>
<React.Fragment <VideoVolumeProvider>
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<QueryProvider currentDid={currentAccount?.did}> <QueryProvider currentDid={currentAccount?.did}>
<StatsigProvider>
<MessagesProvider> <MessagesProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */} {/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<LabelDefsProvider> <LabelDefsProvider>
@ -127,10 +126,10 @@ function InnerApp() {
<BackgroundNotificationPreferencesProvider> <BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider> <MutedThreadsProvider>
<ProgressGuideProvider> <ProgressGuideProvider>
<GestureHandlerRootView <GestureHandlerRootView style={s.h100pct}>
style={s.h100pct}>
<TestCtrls /> <TestCtrls />
<Shell /> <Shell />
<NuxDialogs />
</GestureHandlerRootView> </GestureHandlerRootView>
</ProgressGuideProvider> </ProgressGuideProvider>
</MutedThreadsProvider> </MutedThreadsProvider>
@ -142,27 +141,25 @@ function InnerApp() {
</ModerationOptsProvider> </ModerationOptsProvider>
</LabelDefsProvider> </LabelDefsProvider>
</MessagesProvider> </MessagesProvider>
</StatsigProvider>
</QueryProvider> </QueryProvider>
</React.Fragment> </VideoVolumeProvider>
</RootSiblingParent> </RootSiblingParent>
</ActiveVideoProvider>
</Splash> </Splash>
</ThemeProvider> </ThemeProvider>
</Alf> </Alf>
</StatsigProvider>
) )
} }
function App() { function App() {
const [isReady, setReady] = useState(false) const [isReady, setReady] = useState(false)
const [loaded] = useFonts()
React.useEffect(() => { React.useEffect(() => {
PlatformInfo.setAudioCategory(AudioCategory.Ambient)
PlatformInfo.setAudioActive(false)
initPersistedState().then(() => setReady(true)) initPersistedState().then(() => setReady(true))
}, []) }, [])
if (!isReady) { if (!isReady || !loaded) {
return null return null
} }

View File

@ -35,17 +35,20 @@ import {
} from '#/state/session' } from '#/state/session'
import {readLastActiveAccount} from '#/state/session/util' import {readLastActiveAccount} from '#/state/session/util'
import {Provider as ShellStateProvider} from '#/state/shell' import {Provider as ShellStateProvider} from '#/state/shell'
import {useComposerKeyboardShortcut} from '#/state/shell/composer/useComposerKeyboardShortcut'
import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out' import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out'
import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide' import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies' import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoWebContext' import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoWebContext'
import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext'
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
import {ToastContainer} from '#/view/com/util/Toast.web' import {ToastContainer} from '#/view/com/util/Toast.web'
import {Shell} from '#/view/shell/index' import {Shell} from '#/view/shell/index'
import {ThemeProvider as Alf} from '#/alf' import {ThemeProvider as Alf, useFonts} from '#/alf'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme' import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
import {NuxDialogs} from '#/components/dialogs/nuxs'
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs' import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
import {Provider as PortalProvider} from '#/components/Portal' import {Provider as PortalProvider} from '#/components/Portal'
@ -60,6 +63,8 @@ function InnerApp() {
useIntentHandler() useIntentHandler()
const hasCheckedReferrer = useStarterPackEntry() const hasCheckedReferrer = useStarterPackEntry()
useComposerKeyboardShortcut()
// init // init
useEffect(() => { useEffect(() => {
async function onLaunch(account?: SessionAccount) { async function onLaunch(account?: SessionAccount) {
@ -91,15 +96,15 @@ function InnerApp() {
return ( return (
<KeyboardProvider enabled={false}> <KeyboardProvider enabled={false}>
<StatsigProvider
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<Alf theme={theme}> <Alf theme={theme}>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<RootSiblingParent> <RootSiblingParent>
<VideoVolumeProvider>
<ActiveVideoProvider> <ActiveVideoProvider>
<React.Fragment
// Resets the entire tree below when it changes:
key={currentAccount?.did}>
<QueryProvider currentDid={currentAccount?.did}> <QueryProvider currentDid={currentAccount?.did}>
<StatsigProvider>
<MessagesProvider> <MessagesProvider>
{/* LabelDefsProvider MUST come before ModerationOptsProvider */} {/* LabelDefsProvider MUST come before ModerationOptsProvider */}
<LabelDefsProvider> <LabelDefsProvider>
@ -113,6 +118,7 @@ function InnerApp() {
<SafeAreaProvider> <SafeAreaProvider>
<ProgressGuideProvider> <ProgressGuideProvider>
<Shell /> <Shell />
<NuxDialogs />
</ProgressGuideProvider> </ProgressGuideProvider>
</SafeAreaProvider> </SafeAreaProvider>
</MutedThreadsProvider> </MutedThreadsProvider>
@ -124,26 +130,27 @@ function InnerApp() {
</ModerationOptsProvider> </ModerationOptsProvider>
</LabelDefsProvider> </LabelDefsProvider>
</MessagesProvider> </MessagesProvider>
</StatsigProvider>
</QueryProvider> </QueryProvider>
</React.Fragment>
<ToastContainer /> <ToastContainer />
</ActiveVideoProvider> </ActiveVideoProvider>
</VideoVolumeProvider>
</RootSiblingParent> </RootSiblingParent>
</ThemeProvider> </ThemeProvider>
</Alf> </Alf>
</StatsigProvider>
</KeyboardProvider> </KeyboardProvider>
) )
} }
function App() { function App() {
const [isReady, setReady] = useState(false) const [isReady, setReady] = useState(false)
const [loaded, error] = useFonts()
React.useEffect(() => { React.useEffect(() => {
initPersistedState().then(() => setReady(true)) initPersistedState().then(() => setReady(true))
}, []) }, [])
if (!isReady) { if (!isReady || (!loaded && !error)) {
return null return null
} }

View File

@ -225,43 +225,43 @@ export const atoms = {
}, },
text_2xs: { text_2xs: {
fontSize: tokens.fontSize._2xs, fontSize: tokens.fontSize._2xs,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
}, },
text_xs: { text_xs: {
fontSize: tokens.fontSize.xs, fontSize: tokens.fontSize.xs,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
}, },
text_sm: { text_sm: {
fontSize: tokens.fontSize.sm, fontSize: tokens.fontSize.sm,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
}, },
text_md: { text_md: {
fontSize: tokens.fontSize.md, fontSize: tokens.fontSize.md,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
}, },
text_lg: { text_lg: {
fontSize: tokens.fontSize.lg, fontSize: tokens.fontSize.lg,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
}, },
text_xl: { text_xl: {
fontSize: tokens.fontSize.xl, fontSize: tokens.fontSize.xl,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
}, },
text_2xl: { text_2xl: {
fontSize: tokens.fontSize._2xl, fontSize: tokens.fontSize._2xl,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
}, },
text_3xl: { text_3xl: {
fontSize: tokens.fontSize._3xl, fontSize: tokens.fontSize._3xl,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
}, },
text_4xl: { text_4xl: {
fontSize: tokens.fontSize._4xl, fontSize: tokens.fontSize._4xl,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
}, },
text_5xl: { text_5xl: {
fontSize: tokens.fontSize._5xl, fontSize: tokens.fontSize._5xl,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
}, },
leading_tight: { leading_tight: {
lineHeight: 1.15, lineHeight: 1.15,
@ -273,10 +273,7 @@ export const atoms = {
lineHeight: 1.5, lineHeight: 1.5,
}, },
tracking_normal: { tracking_normal: {
letterSpacing: 0, letterSpacing: tokens.TRACKING,
},
tracking_wide: {
letterSpacing: 0.25,
}, },
font_normal: { font_normal: {
fontWeight: tokens.fontWeight.normal, fontWeight: tokens.fontWeight.normal,

111
src/alf/fonts.ts 100644
View File

@ -0,0 +1,111 @@
import {useFonts as defaultUseFonts} from 'expo-font'
import {isNative, isWeb} from '#/platform/detection'
import {Device, device} from '#/storage'
const FAMILIES = `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Helvetica, Arial, sans-serif`
const factor = 0.0625 // 1 - (15/16)
const fontScaleMultipliers: Record<Device['fontScale'], number> = {
'-2': 1 - factor * 3,
'-1': 1 - factor * 2,
'0': 1 - factor * 1, // default
'1': 1,
'2': 1 + factor * 1,
}
export function computeFontScaleMultiplier(scale: Device['fontScale']) {
return fontScaleMultipliers[scale]
}
export function getFontScale() {
return device.get(['fontScale']) ?? '0'
}
export function setFontScale(fontScale: Device['fontScale']) {
device.set(['fontScale'], fontScale)
}
export function getFontFamily() {
return device.get(['fontFamily']) || 'theme'
}
export function setFontFamily(fontFamily: Device['fontFamily']) {
device.set(['fontFamily'], fontFamily)
}
/*
* Unused fonts are commented out, but the files are there if we need them.
*/
export function useFonts() {
/**
* For native, the `expo-font` config plugin embeds the fonts in the
* application binary. But `expo-font` isn't supported on web, so we fall
* back to async loading here.
*/
if (isNative) return [true, null]
return defaultUseFonts({
// 'Inter-Thin': require('../../assets/fonts/inter/Inter-Thin.otf'),
// 'Inter-ThinItalic': require('../../assets/fonts/inter/Inter-ThinItalic.otf'),
// 'Inter-ExtraLight': require('../../assets/fonts/inter/Inter-ExtraLight.otf'),
// 'Inter-ExtraLightItalic': require('../../assets/fonts/inter/Inter-ExtraLightItalic.otf'),
// 'Inter-Light': require('../../assets/fonts/inter/Inter-Light.otf'),
// 'Inter-LightItalic': require('../../assets/fonts/inter/Inter-LightItalic.otf'),
'Inter-Regular': require('../../assets/fonts/inter/Inter-Regular.otf'),
'Inter-Italic': require('../../assets/fonts/inter/Inter-Italic.otf'),
'Inter-Medium': require('../../assets/fonts/inter/Inter-Medium.otf'),
'Inter-MediumItalic': require('../../assets/fonts/inter/Inter-MediumItalic.otf'),
'Inter-SemiBold': require('../../assets/fonts/inter/Inter-SemiBold.otf'),
'Inter-SemiBoldItalic': require('../../assets/fonts/inter/Inter-SemiBoldItalic.otf'),
'Inter-Bold': require('../../assets/fonts/inter/Inter-Bold.otf'),
'Inter-BoldItalic': require('../../assets/fonts/inter/Inter-BoldItalic.otf'),
'Inter-ExtraBold': require('../../assets/fonts/inter/Inter-ExtraBold.otf'),
'Inter-ExtraBoldItalic': require('../../assets/fonts/inter/Inter-ExtraBoldItalic.otf'),
'Inter-Black': require('../../assets/fonts/inter/Inter-Black.otf'),
'Inter-BlackItalic': require('../../assets/fonts/inter/Inter-BlackItalic.otf'),
})
}
/*
* Unused fonts are commented out, but the files are there if we need them.
*/
export function applyFonts(
style: Record<string, any>,
fontFamily: 'system' | 'theme',
) {
if (fontFamily === 'theme') {
style.fontFamily =
{
// '100': 'Inter-Thin',
// '200': 'Inter-ExtraLight',
// '300': 'Inter-Light',
'100': 'Inter-Regular',
'200': 'Inter-Regular',
'300': 'Inter-Regular',
'400': 'Inter-Regular',
'500': 'Inter-Medium',
'600': 'Inter-SemiBold',
'700': 'Inter-Bold',
'800': 'Inter-ExtraBold',
'900': 'Inter-Black',
}[style.fontWeight as string] || 'Inter-Regular'
if (style.fontStyle === 'italic') {
if (style.fontFamily === 'Inter-Regular') {
style.fontFamily = 'Inter-Italic'
} else {
style.fontFamily += 'Italic'
}
}
// fallback families only supported on web
if (isWeb) {
style.fontFamily += `, ${FAMILIES}`
}
} else {
// fallback families only supported on web
if (isWeb) {
style.fontFamily = style.fontFamily || FAMILIES
}
}
}

View File

@ -1,32 +1,98 @@
import React from 'react' import React from 'react'
import {useMediaQuery} from 'react-responsive' import {useMediaQuery} from 'react-responsive'
import {
computeFontScaleMultiplier,
getFontFamily,
getFontScale,
setFontFamily as persistFontFamily,
setFontScale as persistFontScale,
} from '#/alf/fonts'
import {createThemes, defaultTheme} from '#/alf/themes' import {createThemes, defaultTheme} from '#/alf/themes'
import {Theme, ThemeName} from '#/alf/types' import {Theme, ThemeName} from '#/alf/types'
import {BLUE_HUE, GREEN_HUE, RED_HUE} from '#/alf/util/colorGeneration' import {BLUE_HUE, GREEN_HUE, RED_HUE} from '#/alf/util/colorGeneration'
import {Device} from '#/storage'
export {atoms} from '#/alf/atoms' export {atoms} from '#/alf/atoms'
export * from '#/alf/fonts'
export * as tokens from '#/alf/tokens' export * as tokens from '#/alf/tokens'
export * from '#/alf/types' export * from '#/alf/types'
export * from '#/alf/util/flatten' export * from '#/alf/util/flatten'
export * from '#/alf/util/platform' export * from '#/alf/util/platform'
export * from '#/alf/util/themeSelector' export * from '#/alf/util/themeSelector'
export type Alf = {
themeName: ThemeName
theme: Theme
themes: ReturnType<typeof createThemes>
fonts: {
scale: Exclude<Device['fontScale'], undefined>
scaleMultiplier: number
family: Device['fontFamily']
setFontScale: (fontScale: Exclude<Device['fontScale'], undefined>) => void
setFontFamily: (fontFamily: Device['fontFamily']) => void
}
/**
* Feature flags or other gated options
*/
flags: {}
}
/* /*
* Context * Context
*/ */
export const Context = React.createContext<{ export const Context = React.createContext<Alf>({
themeName: ThemeName
theme: Theme
}>({
themeName: 'light', themeName: 'light',
theme: defaultTheme, theme: defaultTheme,
themes: createThemes({
hues: {
primary: BLUE_HUE,
negative: RED_HUE,
positive: GREEN_HUE,
},
}),
fonts: {
scale: getFontScale(),
scaleMultiplier: computeFontScaleMultiplier(getFontScale()),
family: getFontFamily(),
setFontScale: () => {},
setFontFamily: () => {},
},
flags: {},
}) })
export function ThemeProvider({ export function ThemeProvider({
children, children,
theme: themeName, theme: themeName,
}: React.PropsWithChildren<{theme: ThemeName}>) { }: React.PropsWithChildren<{theme: ThemeName}>) {
const [fontScale, setFontScale] = React.useState<Alf['fonts']['scale']>(() =>
getFontScale(),
)
const [fontScaleMultiplier, setFontScaleMultiplier] = React.useState(() =>
computeFontScaleMultiplier(fontScale),
)
const setFontScaleAndPersist = React.useCallback<
Alf['fonts']['setFontScale']
>(
fontScale => {
setFontScale(fontScale)
persistFontScale(fontScale)
setFontScaleMultiplier(computeFontScaleMultiplier(fontScale))
},
[setFontScale],
)
const [fontFamily, setFontFamily] = React.useState<Alf['fonts']['family']>(
() => getFontFamily(),
)
const setFontFamilyAndPersist = React.useCallback<
Alf['fonts']['setFontFamily']
>(
fontFamily => {
setFontFamily(fontFamily)
persistFontFamily(fontFamily)
},
[setFontFamily],
)
const themes = React.useMemo(() => { const themes = React.useMemo(() => {
return createThemes({ return createThemes({
hues: { hues: {
@ -36,24 +102,47 @@ export function ThemeProvider({
}, },
}) })
}, []) }, [])
const theme = themes[themeName]
return ( return (
<Context.Provider <Context.Provider
value={React.useMemo( value={React.useMemo<Alf>(
() => ({ () => ({
themes,
themeName: themeName, themeName: themeName,
theme: theme, theme: themes[themeName],
fonts: {
scale: fontScale,
scaleMultiplier: fontScaleMultiplier,
family: fontFamily,
setFontScale: setFontScaleAndPersist,
setFontFamily: setFontFamilyAndPersist,
},
flags: {},
}), }),
[theme, themeName], [
themeName,
themes,
fontScale,
setFontScaleAndPersist,
fontFamily,
setFontFamilyAndPersist,
fontScaleMultiplier,
],
)}> )}>
{children} {children}
</Context.Provider> </Context.Provider>
) )
} }
export function useTheme() { export function useAlf() {
return React.useContext(Context).theme return React.useContext(Context)
}
export function useTheme(theme?: ThemeName) {
const alf = useAlf()
return React.useMemo(() => {
return theme ? alf.themes[theme] : alf.theme
}, [theme, alf])
} }
export function useBreakpoints() { export function useBreakpoints() {

View File

@ -1,3 +1,7 @@
import {Platform} from 'react-native'
export const TRACKING = Platform.OS === 'android' ? 0.1 : 0
export const color = { export const color = {
temp_purple: 'rgb(105 0 255)', temp_purple: 'rgb(105 0 255)',
temp_purple_dark: 'rgb(83 0 202)', temp_purple_dark: 'rgb(83 0 202)',

View File

@ -7,7 +7,6 @@ import {
PressableProps, PressableProps,
StyleProp, StyleProp,
StyleSheet, StyleSheet,
Text,
TextProps, TextProps,
TextStyle, TextStyle,
View, View,
@ -17,7 +16,7 @@ import {LinearGradient} from 'expo-linear-gradient'
import {android, atoms as a, flatten, select, tokens, useTheme} from '#/alf' import {android, atoms as a, flatten, select, tokens, useTheme} from '#/alf'
import {Props as SVGIconProps} from '#/components/icons/common' import {Props as SVGIconProps} from '#/components/icons/common'
import {normalizeTextStyles} from '#/components/Typography' import {Text} from '#/components/Typography'
export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'gradient' export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'gradient'
export type ButtonColor = export type ButtonColor =
@ -635,14 +634,7 @@ export function ButtonText({children, style, ...rest}: ButtonTextProps) {
const textStyles = useSharedButtonTextStyles() const textStyles = useSharedButtonTextStyles()
return ( return (
<Text <Text {...rest} style={[a.font_bold, a.text_center, textStyles, style]}>
{...rest}
style={normalizeTextStyles([
a.font_bold,
a.text_center,
textStyles,
style,
])}>
{children} {children}
</Text> </Text>
) )

View File

@ -37,6 +37,7 @@ import {Portal} from '#/components/Portal'
export {useDialogContext, useDialogControl} from '#/components/Dialog/context' export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
export * from '#/components/Dialog/types' export * from '#/components/Dialog/types'
export * from '#/components/Dialog/utils'
// @ts-ignore // @ts-ignore
export const Input = createInput(BottomSheetTextInput) export const Input = createInput(BottomSheetTextInput)
@ -256,7 +257,7 @@ export const ScrollableInner = React.forwardRef<
borderTopLeftRadius: 40, borderTopLeftRadius: 40,
borderTopRightRadius: 40, borderTopRightRadius: 40,
}, },
flatten(style), style,
]} ]}
contentContainerStyle={a.pb_4xl} contentContainerStyle={a.pb_4xl}
ref={ref}> ref={ref}>

View File

@ -27,6 +27,7 @@ import {Portal} from '#/components/Portal'
export {useDialogContext, useDialogControl} from '#/components/Dialog/context' export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
export * from '#/components/Dialog/types' export * from '#/components/Dialog/types'
export * from '#/components/Dialog/utils'
export {Input} from '#/components/forms/TextField' export {Input} from '#/components/forms/TextField'
const stopPropagation = (e: any) => e.stopPropagation() const stopPropagation = (e: any) => e.stopPropagation()

View File

@ -0,0 +1,18 @@
import React from 'react'
import {DialogControlProps} from '#/components/Dialog/types'
export function useAutoOpen(control: DialogControlProps, showTimeout?: number) {
React.useEffect(() => {
if (showTimeout) {
const timeout = setTimeout(() => {
control.open()
}, showTimeout)
return () => {
clearTimeout(timeout)
}
} else {
control.open()
}
}, [control, showTimeout])
}

View File

@ -0,0 +1,11 @@
import React from 'react'
import {View} from 'react-native'
import {atoms as a, ViewStyleProp} from '#/alf'
export function Fill({
children,
style,
}: {children?: React.ReactNode} & ViewStyleProp) {
return <View style={[a.absolute, a.inset_0, style]}>{children}</View>
}

View File

@ -75,6 +75,7 @@ export function LikedByList({uri}: {uri: string}) {
isLoading={isUriLoading || isLikedByLoading} isLoading={isUriLoading || isLikedByLoading}
isError={isError} isError={isError}
emptyType="results" emptyType="results"
emptyTitle={_(msg`No likes yet`)}
emptyMessage={_( emptyMessage={_(
msg`Nobody has liked this yet. Maybe you should be the first!`, msg`Nobody has liked this yet. Maybe you should be the first!`,
)} )}

View File

@ -0,0 +1,45 @@
import React from 'react'
import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
import {Fill} from '#/components/Fill'
/**
* Applies and thin border within a bounding box. Used to contrast media from
* bg of the container.
*/
export function MediaInsetBorder({
children,
style,
opaque,
}: {
children?: React.ReactNode
/**
* Used where this border needs to match adjacent borders, such as in
* external link previews
*/
opaque?: boolean
} & ViewStyleProp) {
const t = useTheme()
const isLight = t.name === 'light'
return (
<Fill
style={[
a.rounded_sm,
a.border,
opaque
? [t.atoms.border_contrast_low]
: [
isLight
? t.atoms.border_contrast_low
: t.atoms.border_contrast_high,
{opacity: 0.6},
],
{
pointerEvents: 'none',
},
style,
]}>
{children}
</Fill>
)
}

View File

@ -11,6 +11,7 @@ import {Trans} from '@lingui/macro'
import {parseTenorGif} from '#/lib/strings/embed-player' import {parseTenorGif} from '#/lib/strings/embed-player'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
@ -104,6 +105,7 @@ export function ImageItem({
accessibilityHint={alt} accessibilityHint={alt}
accessibilityLabel="" accessibilityLabel=""
/> />
<MediaInsetBorder style={[a.rounded_xs]} />
{children} {children}
</View> </View>
) )

View File

@ -59,7 +59,9 @@ export function Outer({
export function TitleText({children}: React.PropsWithChildren<{}>) { export function TitleText({children}: React.PropsWithChildren<{}>) {
const {titleId} = React.useContext(Context) const {titleId} = React.useContext(Context)
return ( return (
<Text nativeID={titleId} style={[a.text_2xl, a.font_bold, a.pb_sm]}> <Text
nativeID={titleId}
style={[a.text_2xl, a.font_bold, a.pb_sm, a.leading_snug]}>
{children} {children}
</Text> </Text>
) )

View File

@ -18,7 +18,7 @@ interface ProfilesListProps {
export const PostsList = React.forwardRef<SectionRef, ProfilesListProps>( export const PostsList = React.forwardRef<SectionRef, ProfilesListProps>(
function PostsListImpl({listUri, headerHeight, scrollElRef}, ref) { function PostsListImpl({listUri, headerHeight, scrollElRef}, ref) {
const feed: FeedDescriptor = `list|${listUri}|as_following` const feed: FeedDescriptor = `list|${listUri}`
const {_} = useLingui() const {_} = useLingui()
const onScrollToTop = useCallback(() => { const onScrollToTop = useCallback(() => {

View File

@ -3,7 +3,7 @@ import {StyleProp, TextProps as RNTextProps, TextStyle} from 'react-native'
import {UITextView} from 'react-native-uitextview' import {UITextView} from 'react-native-uitextview'
import {isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
import {atoms, flatten, useTheme, web} from '#/alf' import {Alf, applyFonts, atoms, flatten, useAlf, useTheme, web} from '#/alf'
export type TextProps = RNTextProps & { export type TextProps = RNTextProps & {
/** /**
@ -34,19 +34,30 @@ export function leading<
* If the `lineHeight` value is > 2, we assume it's an absolute value and * If the `lineHeight` value is > 2, we assume it's an absolute value and
* returns it as-is. * returns it as-is.
*/ */
export function normalizeTextStyles(styles: StyleProp<TextStyle>) { export function normalizeTextStyles(
styles: StyleProp<TextStyle>,
{
fontScale,
fontFamily,
}: {
fontScale: number
fontFamily: Alf['fonts']['family']
} & Pick<Alf, 'flags'>,
) {
const s = flatten(styles) const s = flatten(styles)
// should always be defined on these components // should always be defined on these components
const fontSize = s.fontSize || atoms.text_md.fontSize s.fontSize = (s.fontSize || atoms.text_md.fontSize) * fontScale
if (s?.lineHeight) { if (s?.lineHeight) {
if (s.lineHeight !== 0 && s.lineHeight <= 2) { if (s.lineHeight !== 0 && s.lineHeight <= 2) {
s.lineHeight = Math.round(fontSize * s.lineHeight) s.lineHeight = Math.round(s.fontSize * s.lineHeight)
} }
} else if (!isNative) { } else if (!isNative) {
s.lineHeight = s.fontSize s.lineHeight = s.fontSize
} }
applyFonts(s, fontFamily)
return s return s
} }
@ -54,8 +65,13 @@ export function normalizeTextStyles(styles: StyleProp<TextStyle>) {
* Our main text component. Use this most of the time. * Our main text component. Use this most of the time.
*/ */
export function Text({style, selectable, ...rest}: TextProps) { export function Text({style, selectable, ...rest}: TextProps) {
const {fonts, flags} = useAlf()
const t = useTheme() const t = useTheme()
const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, flatten(style)]) const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, flatten(style)], {
fontScale: fonts.scaleMultiplier,
fontFamily: fonts.family,
flags,
})
return <UITextView selectable={selectable} uiTextView style={s} {...rest} /> return <UITextView selectable={selectable} uiTextView style={s} {...rest} />
} }

View File

@ -0,0 +1,119 @@
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, {fontWeight: '900'}]}>
<Trans>Introducing new font settings </Trans>
</Text>
<Text style={[a.text_lg, a.leading_snug]}>
<Trans>
To the ensure the best possible experience, we're introducing a
new theme font, along with adjustable font sizing settings.
</Trans>
</Text>
<Text
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
<Trans>
Defaults are shown below. You can edit 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>
)
}

View File

@ -0,0 +1,129 @@
import React from 'react'
import {View} from 'react-native'
import Svg, {Circle, Path} from 'react-native-svg'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {Nux, useUpsertNuxMutation} from '#/state/queries/nuxs'
import {atoms as a, ViewStyleProp} from '#/alf'
import {Button, ButtonProps} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {InlineLinkText} from '#/components/Link'
import * as Prompt from '#/components/Prompt'
import {TenMillion} from './'
export function Trigger({children}: {children: ButtonProps['children']}) {
const {_} = useLingui()
const {mutate: upsertNux} = useUpsertNuxMutation()
const [show, setShow] = React.useState(false)
const [fallback, setFallback] = React.useState(false)
const control = Prompt.usePromptControl()
const handleOnPress = () => {
if (!fallback) {
setShow(true)
upsertNux({
id: Nux.TenMillionDialog,
completed: true,
data: undefined,
})
} else {
control.open()
}
}
const onHandleFallback = () => {
setFallback(true)
control.open()
}
return (
<>
<Button
label={_(msg`Bluesky is celebrating 10 million users!`)}
onPress={handleOnPress}>
{children}
</Button>
{show && !fallback && (
<TenMillion
showTimeout={0}
onClose={() => setShow(false)}
onFallback={onHandleFallback}
/>
)}
<Prompt.Outer control={control}>
<View style={{maxWidth: 300}}>
<Prompt.TitleText>
<Trans>Bluesky is celebrating 10 million users!</Trans>
</Prompt.TitleText>
</View>
<Prompt.DescriptionText>
<Trans>
Together, we're rebuilding the social internet. We're glad you're
here.
</Trans>
</Prompt.DescriptionText>
<Prompt.DescriptionText>
<Trans>
To learn more,{' '}
<InlineLinkText
label={_(msg`View our post`)}
to="/profile/bsky.app/post/3l47prg3wgy23"
onPress={() => {
control.close()
}}
style={[a.text_md, a.leading_snug]}>
<Trans>check out our post.</Trans>
</InlineLinkText>
</Trans>
</Prompt.DescriptionText>
<Dialog.Close />
</Prompt.Outer>
</>
)
}
export function Icon({width, style}: {width: number} & ViewStyleProp) {
return (
<Svg width={width} height={width} viewBox="0 0 36 36" style={style}>
<Path
fill="#dd2e44"
d="M11.626 7.488a1.4 1.4 0 0 0-.268.395l-.008-.008L.134 33.141l.011.011c-.208.403.14 1.223.853 1.937c.713.713 1.533 1.061 1.936.853l.01.01L28.21 24.735l-.008-.009c.147-.07.282-.155.395-.269c1.562-1.562-.971-6.627-5.656-11.313c-4.687-4.686-9.752-7.218-11.315-5.656"
/>
<Path
fill="#ea596e"
d="M13 12L.416 32.506l-.282.635l.011.011c-.208.403.14 1.223.853 1.937c.232.232.473.408.709.557L17 17z"
/>
<Path
fill="#a0041e"
d="M23.012 13.066c4.67 4.672 7.263 9.652 5.789 11.124c-1.473 1.474-6.453-1.118-11.126-5.788c-4.671-4.672-7.263-9.654-5.79-11.127c1.474-1.473 6.454 1.119 11.127 5.791"
/>
<Path
fill="#aa8dd8"
d="M18.59 13.609a1 1 0 0 1-.734.215c-.868-.094-1.598-.396-2.109-.873c-.541-.505-.808-1.183-.735-1.862c.128-1.192 1.324-2.286 3.363-2.066c.793.085 1.147-.17 1.159-.292c.014-.121-.277-.446-1.07-.532c-.868-.094-1.598-.396-2.11-.873c-.541-.505-.809-1.183-.735-1.862c.13-1.192 1.325-2.286 3.362-2.065c.578.062.883-.057 1.012-.134c.103-.063.144-.123.148-.158c.012-.121-.275-.446-1.07-.532a1 1 0 0 1-.886-1.102a.997.997 0 0 1 1.101-.886c2.037.219 2.973 1.542 2.844 2.735c-.13 1.194-1.325 2.286-3.364 2.067c-.578-.063-.88.057-1.01.134c-.103.062-.145.123-.149.157c-.013.122.276.446 1.071.532c2.037.22 2.973 1.542 2.844 2.735s-1.324 2.286-3.362 2.065c-.578-.062-.882.058-1.012.134c-.104.064-.144.124-.148.158c-.013.121.276.446 1.07.532a1 1 0 0 1 .52 1.773"
/>
<Path
fill="#77b255"
d="M30.661 22.857c1.973-.557 3.334.323 3.658 1.478c.324 1.154-.378 2.615-2.35 3.17c-.77.216-1.001.584-.97.701c.034.118.425.312 1.193.095c1.972-.555 3.333.325 3.657 1.479c.326 1.155-.378 2.614-2.351 3.17c-.769.216-1.001.585-.967.702s.423.311 1.192.095a1 1 0 1 1 .54 1.925c-1.971.555-3.333-.323-3.659-1.479c-.324-1.154.379-2.613 2.353-3.169c.77-.217 1.001-.584.967-.702c-.032-.117-.422-.312-1.19-.096c-1.974.556-3.334-.322-3.659-1.479c-.325-1.154.378-2.613 2.351-3.17c.768-.215.999-.585.967-.701c-.034-.118-.423-.312-1.192-.096a1 1 0 1 1-.54-1.923"
/>
<Path
fill="#aa8dd8"
d="M23.001 20.16a1.001 1.001 0 0 1-.626-1.781c.218-.175 5.418-4.259 12.767-3.208a1 1 0 1 1-.283 1.979c-6.493-.922-11.187 2.754-11.233 2.791a1 1 0 0 1-.625.219"
/>
<Path
fill="#77b255"
d="M5.754 16a1 1 0 0 1-.958-1.287c1.133-3.773 2.16-9.794.898-11.364c-.141-.178-.354-.353-.842-.316c-.938.072-.849 2.051-.848 2.071a1 1 0 1 1-1.994.149c-.103-1.379.326-4.035 2.692-4.214c1.056-.08 1.933.287 2.552 1.057c2.371 2.951-.036 11.506-.542 13.192a1 1 0 0 1-.958.712"
/>
<Circle cx="25.5" cy="9.5" r="1.5" fill="#5c913b" />
<Circle cx="2" cy="18" r="2" fill="#9266cc" />
<Circle cx="32.5" cy="19.5" r="1.5" fill="#5c913b" />
<Circle cx="23.5" cy="31.5" r="1.5" fill="#5c913b" />
<Circle cx="28" cy="4" r="2" fill="#ffcc4d" />
<Circle cx="32.5" cy="8.5" r="1.5" fill="#ffcc4d" />
<Circle cx="29.5" cy="12.5" r="1.5" fill="#ffcc4d" />
<Circle cx="7.5" cy="23.5" r="1.5" fill="#ffcc4d" />
</Svg>
)
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,719 @@
import React from 'react'
import {View} from 'react-native'
import Animated, {FadeIn} from 'react-native-reanimated'
import ViewShot from 'react-native-view-shot'
import {Image} from 'expo-image'
import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker'
import * as MediaLibrary from 'expo-media-library'
import {moderateProfile} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {networkRetry} from '#/lib/async/retry'
import {getCanvas} from '#/lib/canvas'
import {shareUrl} from '#/lib/sharing'
import {logEvent} from '#/lib/statsig/statsig'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {isIOS, isNative} from '#/platform/detection'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useProfileQuery} from '#/state/queries/profile'
import {useAgent, useSession} from '#/state/session'
import {useComposerControls} from 'state/shell'
import {formatCount} from '#/view/com/util/numeric/format'
import {Logomark} from '#/view/icons/Logomark'
import * as Toast from 'view/com/util/Toast'
import {
atoms as a,
ThemeProvider,
tokens,
useBreakpoints,
useTheme,
} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {useNuxDialogContext} from '#/components/dialogs/nuxs'
import {OnePercent} from '#/components/dialogs/nuxs/TenMillion/icons/OnePercent'
import {PointOnePercent} from '#/components/dialogs/nuxs/TenMillion/icons/PointOnePercent'
import {TenPercent} from '#/components/dialogs/nuxs/TenMillion/icons/TenPercent'
import {Divider} from '#/components/Divider'
import {GradientFill} from '#/components/GradientFill'
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
import {Download_Stroke2_Corner0_Rounded as Download} from '#/components/icons/Download'
import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
const DEBUG = false
const RATIO = 8 / 10
const WIDTH = 2000
const HEIGHT = WIDTH * RATIO
function getFontSize(count: number) {
const length = count.toString().length
if (length < 7) {
return 80
} else if (length < 5) {
return 100
} else {
return 70
}
}
function getPercentBadge(percent: number) {
if (percent <= 0.001) {
return PointOnePercent
} else if (percent <= 0.01) {
return OnePercent
} else if (percent <= 0.1) {
return TenPercent
}
return null
}
function Frame({children}: {children: React.ReactNode}) {
return (
<View
style={[
a.relative,
a.w_full,
a.overflow_hidden,
{
paddingTop: '80%',
},
]}>
{children}
</View>
)
}
export function TenMillion({
showTimeout,
onClose,
onFallback,
}: {
showTimeout?: number
onClose?: () => void
onFallback?: () => void
}) {
const agent = useAgent()
const nuxDialogs = useNuxDialogContext()
const [userNumber, setUserNumber] = React.useState<number>(0)
const fetching = React.useRef(false)
React.useEffect(() => {
async function fetchUserNumber() {
const isBlueskyHosted = agent.sessionManager.pdsUrl
?.toString()
.includes('bsky.network')
if (isBlueskyHosted && agent.session?.accessJwt) {
const res = await fetch(
`https://bsky.social/xrpc/com.atproto.temp.getSignupNumber`,
{
headers: {
Authorization: `Bearer ${agent.session.accessJwt}`,
},
},
)
if (!res.ok) {
throw new Error('Network request failed')
}
const data = await res.json()
if (data.number && data.number <= 10_000_000) {
setUserNumber(data.number)
} else {
// should be rare
nuxDialogs.dismissActiveNux()
onFallback?.()
}
} else {
nuxDialogs.dismissActiveNux()
onFallback?.()
}
}
if (!fetching.current) {
fetching.current = true
networkRetry(3, fetchUserNumber).catch(() => {
nuxDialogs.dismissActiveNux()
onFallback?.()
})
}
}, [
agent.sessionManager.pdsUrl,
agent.session?.accessJwt,
setUserNumber,
nuxDialogs.dismissActiveNux,
nuxDialogs,
onFallback,
])
return userNumber ? (
<TenMillionInner
userNumber={userNumber}
showTimeout={showTimeout ?? 3e3}
onClose={onClose}
/>
) : null
}
export function TenMillionInner({
userNumber,
showTimeout,
onClose: onCloseOuter,
}: {
userNumber: number
showTimeout: number
onClose?: () => void
}) {
const t = useTheme()
const lightTheme = useTheme('light')
const {_, i18n} = useLingui()
const control = Dialog.useDialogControl()
const {gtMobile} = useBreakpoints()
const {openComposer} = useComposerControls()
const {currentAccount} = useSession()
const {
isLoading: isProfileLoading,
data: profile,
error: profileError,
} = useProfileQuery({
did: currentAccount!.did,
})
const moderationOpts = useModerationOpts()
const nuxDialogs = useNuxDialogContext()
const moderation = React.useMemo(() => {
return profile && moderationOpts
? moderateProfile(profile, moderationOpts)
: undefined
}, [profile, moderationOpts])
const [uri, setUri] = React.useState<string | null>(null)
const percent = userNumber / 10_000_000
const Badge = getPercentBadge(percent)
const isLoadingData = isProfileLoading || !moderation || !profile
const isLoadingImage = !uri
const displayName = React.useMemo(() => {
if (!profile || !moderation) return ''
return sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle),
moderation.ui('displayName'),
)
}, [profile, moderation])
const handle = React.useMemo(() => {
if (!profile) return ''
return sanitizeHandle(profile.handle, '@')
}, [profile])
const joinedDate = React.useMemo(() => {
if (!profile || !profile.createdAt) return ''
const date = i18n.date(profile.createdAt, {
month: 'short',
day: 'numeric',
year: 'numeric',
})
return date
}, [i18n, profile])
const error: string = React.useMemo(() => {
if (profileError) {
return _(
msg`Oh no! We weren't able to generate an image for you to share. Rest assured, we're glad you're here 🦋`,
)
}
return ''
}, [_, profileError])
/*
* Opening and closing
*/
React.useEffect(() => {
const timeout = setTimeout(() => {
control.open()
}, showTimeout)
return () => {
clearTimeout(timeout)
}
}, [control, showTimeout])
const onClose = React.useCallback(() => {
nuxDialogs.dismissActiveNux()
onCloseOuter?.()
}, [nuxDialogs, onCloseOuter])
/*
* Actions
*/
const sharePost = React.useCallback(() => {
if (uri) {
control.close(() => {
setTimeout(() => {
logEvent('tmd:post', {})
openComposer({
text: _(
msg`Bluesky now has over 10 million users, and I was #${i18n.number(
userNumber,
)}!`,
),
imageUris: [
{
uri,
width: WIDTH,
height: HEIGHT,
altText: _(
msg`A virtual certificate with text "Celebrating 10M users on Bluesky, #${i18n.number(
userNumber,
)}, ${displayName} ${handle}, joined on ${joinedDate}"`,
),
},
],
})
}, 1e3)
})
}
}, [
_,
i18n,
control,
openComposer,
uri,
userNumber,
displayName,
handle,
joinedDate,
])
const onNativeShare = React.useCallback(() => {
if (uri) {
control.close(() => {
logEvent('tmd:share', {})
shareUrl(uri)
})
}
}, [uri, control])
const onNativeDownload = React.useCallback(async () => {
if (uri) {
const res = await requestMediaLibraryPermissionsAsync()
if (!res) {
Toast.show(
_(
msg`You must grant access to your photo library to save the image.`,
),
'xmark',
)
return
}
try {
await MediaLibrary.createAssetAsync(uri)
logEvent('tmd:download', {})
Toast.show(_(msg`Image saved to your camera roll!`))
} catch (e: unknown) {
console.log(e)
Toast.show(_(msg`An error occurred while saving the image!`), 'xmark')
return
}
}
}, [_, uri])
const onWebDownload = React.useCallback(async () => {
if (uri) {
const canvas = await getCanvas(uri)
const imgHref = canvas
.toDataURL('image/png')
.replace('image/png', 'image/octet-stream')
const link = document.createElement('a')
link.setAttribute('download', `Bluesky 10M Users.png`)
link.setAttribute('href', imgHref)
link.click()
logEvent('tmd:download', {})
}
}, [uri])
/*
* Canvas stuff
*/
const imageRef = React.useRef<ViewShot>(null)
const captureInProgress = React.useRef(false)
const onCanvasReady = React.useCallback(async () => {
if (
imageRef.current &&
imageRef.current.capture &&
!captureInProgress.current
) {
captureInProgress.current = true
const uri = await imageRef.current.capture()
setUri(uri)
}
}, [setUri])
const canvas = isLoadingData ? null : (
<View
style={[
a.absolute,
a.overflow_hidden,
DEBUG
? {
width: 600,
height: 600 * RATIO,
}
: {
width: 1,
height: 1,
},
]}>
<View style={{width: 600}}>
<ThemeProvider theme="light">
<Frame>
<ViewShot
ref={imageRef}
options={{width: WIDTH, height: HEIGHT}}
style={[a.absolute, a.inset_0]}>
<View
onLayout={onCanvasReady}
style={[
a.absolute,
a.inset_0,
a.align_center,
a.justify_center,
{
top: -1,
bottom: -1,
left: -1,
right: -1,
paddingVertical: 48,
paddingHorizontal: 48,
},
]}>
<GradientFill gradient={tokens.gradients.bonfire} />
<View
style={[
a.flex_1,
a.w_full,
a.align_center,
a.justify_center,
a.rounded_md,
{
backgroundColor: 'white',
shadowRadius: 32,
shadowOpacity: 0.1,
elevation: 24,
shadowColor: tokens.gradients.bonfire.values[0][1],
},
]}>
<View
style={[
a.absolute,
a.px_xl,
a.py_xl,
{
top: 0,
left: 0,
},
]}>
<Logomark fill={t.palette.primary_500} width={36} />
</View>
{/* Centered content */}
<View
style={[
{
paddingBottom: isNative ? 0 : 24,
},
]}>
<Text
allowFontScaling={false}
style={[
a.text_md,
a.font_bold,
a.text_center,
a.pb_sm,
lightTheme.atoms.text_contrast_medium,
]}>
<Trans>
Celebrating {formatCount(i18n, 10000000)} users
</Trans>{' '}
🎉
</Text>
<View style={[a.flex_row, a.align_start]}>
<Text
allowFontScaling={false}
style={[
a.absolute,
{
color: t.palette.primary_500,
fontSize: 32,
fontWeight: '900',
width: 32,
top: isNative ? -10 : 0,
left: 0,
transform: [
{
translateX: -16,
},
],
},
]}>
#
</Text>
<Text
allowFontScaling={false}
style={[
a.relative,
a.text_center,
{
fontStyle: 'italic',
fontSize: getFontSize(userNumber),
lineHeight: getFontSize(userNumber),
fontWeight: '900',
letterSpacing: -2,
},
]}>
{i18n.number(userNumber)}
</Text>
</View>
{Badge && (
<View
style={[
a.absolute,
{
width: 64,
height: 64,
top: isNative ? 75 : 85,
right: '5%',
transform: [
{
rotate: '8deg',
},
],
},
]}>
<Badge fill={t.palette.primary_500} />
</View>
)}
</View>
{/* End centered content */}
<View
style={[
a.absolute,
a.px_xl,
a.py_xl,
{
bottom: 0,
left: 0,
right: 0,
},
]}>
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
{/*
<UserAvatar
size={36}
avatar={profile.avatar}
moderation={moderation.ui('avatar')}
onLoad={onCanvasReady}
/>
*/}
<View style={[a.gap_2xs, a.flex_1]}>
<Text
allowFontScaling={false}
style={[
a.flex_1,
a.text_sm,
a.font_bold,
a.leading_tight,
{maxWidth: '60%'},
]}>
{displayName}
</Text>
<View
style={[a.flex_row, a.justify_between, a.gap_4xl]}>
<Text
allowFontScaling={false}
numberOfLines={1}
style={[
a.flex_1,
a.text_sm,
a.font_semibold,
a.leading_snug,
lightTheme.atoms.text_contrast_medium,
]}>
{handle}
</Text>
{profile.createdAt && (
<Text
allowFontScaling={false}
numberOfLines={1}
ellipsizeMode="head"
style={[
a.flex_1,
a.text_sm,
a.font_semibold,
a.leading_snug,
a.text_right,
lightTheme.atoms.text_contrast_low,
]}>
<Trans>Joined on {joinedDate}</Trans>
</Text>
)}
</View>
</View>
</View>
</View>
</View>
</View>
</ViewShot>
</Frame>
</ThemeProvider>
</View>
</View>
)
return (
<Dialog.Outer control={control} onClose={onClose}>
<Dialog.ScrollableInner
label={_(msg`Ten Million`)}
style={[
{
padding: 0,
paddingTop: 0,
},
]}>
<View
style={[
a.rounded_md,
a.overflow_hidden,
isNative && {
borderTopLeftRadius: 40,
borderTopRightRadius: 40,
},
]}>
<Frame>
<View
style={[a.absolute, a.inset_0, a.align_center, a.justify_center]}>
<GradientFill gradient={tokens.gradients.bonfire} />
{error ? (
<View>
<Text
style={[
a.text_md,
a.leading_snug,
a.text_center,
a.pb_md,
{
maxWidth: 300,
},
]}>
(°°)
</Text>
<Text
style={[
a.text_xl,
a.font_bold,
a.leading_snug,
a.text_center,
{
maxWidth: 300,
},
]}>
{error}
</Text>
</View>
) : isLoadingData || isLoadingImage ? (
<Loader size="xl" fill="white" />
) : (
<Animated.View
entering={FadeIn.duration(150)}
style={[a.w_full, a.h_full]}>
<Image
accessibilityIgnoresInvertColors
source={{uri}}
style={[a.w_full, a.h_full]}
/>
</Animated.View>
)}
</View>
</Frame>
{canvas}
<View style={[gtMobile ? a.p_2xl : a.p_xl]}>
<Text
allowFontScaling={false}
style={[
a.text_5xl,
a.leading_tight,
a.pb_lg,
{
fontWeight: '900',
},
]}>
<Trans>Thanks for being one of our first 10 million users.</Trans>
</Text>
<Text style={[a.leading_snug, a.text_lg, a.pb_xl]}>
<Trans>
Together, we're rebuilding the social internet. We're glad
you're here.
</Trans>
</Text>
<Divider />
<View
style={[
a.flex_row,
a.align_center,
a.justify_end,
a.gap_md,
a.pt_xl,
]}>
{gtMobile && (
<Text
style={[a.text_md, a.italic, t.atoms.text_contrast_medium]}>
<Trans>Brag a little!</Trans>
</Text>
)}
<Button
disabled={isLoadingImage}
label={
isNative && isIOS
? _(msg`Share image externally`)
: _(msg`Download image`)
}
size="large"
variant="solid"
color="secondary"
shape="square"
onPress={
isNative
? isIOS
? onNativeShare
: onNativeDownload
: onWebDownload
}>
<ButtonIcon icon={isNative && isIOS ? Share : Download} />
</Button>
<Button
disabled={isLoadingImage}
label={_(msg`Share image in post`)}
size="large"
variant="solid"
color="primary"
onPress={sharePost}>
<ButtonText>{_(msg`Share`)}</ButtonText>
<ButtonIcon position="right" icon={ImageIcon} />
</Button>
</View>
</View>
</View>
<Dialog.Close />
</Dialog.ScrollableInner>
</Dialog.Outer>
)
}

View File

@ -0,0 +1,183 @@
import React from 'react'
import {AppBskyActorDefs} from '@atproto/api'
import {useGate} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {
Nux,
useNuxs,
useRemoveNuxsMutation,
useUpsertNuxMutation,
} from '#/state/queries/nuxs'
import {
usePreferencesQuery,
UsePreferencesQueryResponse,
} from '#/state/queries/preferences'
import {useProfileQuery} from '#/state/queries/profile'
import {SessionAccount, useSession} from '#/state/session'
import {useOnboardingState} from '#/state/shell'
import {NeueTypography} from '#/components/dialogs/nuxs/NeueTypography'
import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing'
// NUXs
import {TenMillion} from '#/components/dialogs/nuxs/TenMillion'
import {IS_DEV} from '#/env'
type Context = {
activeNux: Nux | undefined
dismissActiveNux: () => void
}
const queuedNuxs: {
id: Nux
enabled?: (props: {
gate: ReturnType<typeof useGate>
currentAccount: SessionAccount
currentProfile: AppBskyActorDefs.ProfileViewDetailed
preferences: UsePreferencesQueryResponse
}) => boolean
}[] = [
{
id: Nux.TenMillionDialog,
},
{
id: Nux.NeueTypography,
enabled(props) {
if (props.currentProfile.createdAt) {
if (new Date(props.currentProfile.createdAt) < new Date('2024-09-25')) {
return true
}
}
return false
},
},
]
const Context = React.createContext<Context>({
activeNux: undefined,
dismissActiveNux: () => {},
})
export function useNuxDialogContext() {
return React.useContext(Context)
}
export function NuxDialogs() {
const {currentAccount} = useSession()
const {data: preferences} = usePreferencesQuery()
const {data: profile} = useProfileQuery({did: currentAccount?.did})
const onboardingActive = useOnboardingState().isActive
const isLoading =
!currentAccount || !preferences || !profile || onboardingActive
return !isLoading ? (
<Inner
currentAccount={currentAccount}
currentProfile={profile}
preferences={preferences}
/>
) : null
}
function Inner({
currentAccount,
currentProfile,
preferences,
}: {
currentAccount: SessionAccount
currentProfile: AppBskyActorDefs.ProfileViewDetailed
preferences: UsePreferencesQueryResponse
}) {
const gate = useGate()
const {nuxs} = useNuxs()
const [snoozed, setSnoozed] = React.useState(() => {
return isSnoozed()
})
const [activeNux, setActiveNux] = React.useState<Nux | undefined>()
const {mutateAsync: upsertNux} = useUpsertNuxMutation()
const {mutate: removeNuxs} = useRemoveNuxsMutation()
const snoozeNuxDialog = React.useCallback(() => {
snooze()
setSnoozed(true)
}, [setSnoozed])
const dismissActiveNux = React.useCallback(() => {
if (!activeNux) return
setActiveNux(undefined)
}, [activeNux, setActiveNux])
if (IS_DEV && typeof window !== 'undefined') {
// @ts-ignore
window.clearNuxDialog = (id: Nux) => {
if (!IS_DEV || !id) return
removeNuxs([id])
unsnooze()
}
}
React.useEffect(() => {
if (snoozed) return
if (!nuxs) return
for (const {id, enabled} of queuedNuxs) {
const nux = nuxs.find(nux => nux.id === id)
// check if completed first
if (nux && nux.completed) {
continue
}
// then check gate (track exposure)
if (
enabled &&
!enabled({gate, currentAccount, currentProfile, preferences})
) {
continue
}
logger.debug(`NUX dialogs: activating '${id}' NUX`)
// we have a winner
setActiveNux(id)
// immediately snooze for a day
snoozeNuxDialog()
// immediately update remote data (affects next reload)
upsertNux({
id,
completed: true,
data: undefined,
}).catch(e => {
logger.error(`NUX dialogs: failed to upsert '${id}' NUX`, {
safeMessage: e.message,
})
})
break
}
}, [
nuxs,
snoozed,
snoozeNuxDialog,
upsertNux,
gate,
currentAccount,
currentProfile,
preferences,
])
const ctx = React.useMemo(() => {
return {
activeNux,
dismissActiveNux,
}
}, [activeNux, dismissActiveNux])
return (
<Context.Provider value={ctx}>
{activeNux === Nux.TenMillionDialog && <TenMillion />}
{activeNux === Nux.NeueTypography && <NeueTypography />}
</Context.Provider>
)
}

View File

@ -0,0 +1,22 @@
import {simpleAreDatesEqual} from '#/lib/strings/time'
import {device} from '#/storage'
export function snooze() {
device.set(['lastNuxDialog'], new Date().toISOString())
}
export function unsnooze() {
device.set(['lastNuxDialog'], undefined)
}
export function isSnoozed() {
const lastNuxDialog = device.get(['lastNuxDialog'])
if (!lastNuxDialog) return false
const last = new Date(lastNuxDialog)
const now = new Date()
// already snoozed today
if (simpleAreDatesEqual(last, now)) {
return true
}
return false
}

View File

@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Download_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M12 3a1 1 0 0 1 1 1v8.086l1.793-1.793a1 1 0 1 1 1.414 1.414l-3.5 3.5a1 1 0 0 1-1.414 0l-3.5-3.5a1 1 0 1 1 1.414-1.414L11 12.086V4a1 1 0 0 1 1-1ZM4 14a1 1 0 0 1 1 1v4h14v-4a1 1 0 1 1 2 0v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-5a1 1 0 0 1 1-1Z',
})

View File

@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const TextSize_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M9 5a1 1 0 0 1 1-1h12a1 1 0 1 1 0 2h-5v14a1 1 0 1 1-2 0V6h-5a1 1 0 0 1-1-1Zm-3.073 7v8a1 1 0 1 0 2 0v-8H12a1 1 0 1 0 0-2H6.971a1.015 1.015 0 0 0-.089 0H2a1 1 0 1 0 0 2h3.927Z',
})

View File

@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const TitleCase_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M3.65 17.247c-.242.832-.632 1.178-1.325 1.178-.814 0-1.325-.476-1.325-1.23 0-.216.06-.51.173-.831L4.586 7.07c.364-1.014.979-1.482 1.966-1.482 1.022 0 1.629.45 2.001 1.473l3.43 9.303c.121.337.165.571.165.831 0 .72-.546 1.23-1.308 1.23-.736 0-1.126-.338-1.36-1.152l-.658-1.975H4.309l-.658 1.95ZM6.5 8.152l-1.62 5.12h3.335l-1.654-5.12H6.5Zm13.005 8.688c-.52.988-1.68 1.568-2.84 1.568-1.768 0-3.11-1.144-3.11-2.815 0-1.69 1.299-2.668 3.62-2.807l2.34-.138v-.615c0-.867-.607-1.369-1.56-1.369-.771 0-1.239.251-1.802.979-.277.312-.597.468-1.004.468-.615 0-1.057-.399-1.057-.97 0-.2.043-.382.13-.572.433-1.109 1.923-1.793 3.845-1.793 2.383 0 3.933 1.23 3.933 3.1v5.293c0 .84-.511 1.273-1.23 1.273-.684 0-1.16-.38-1.213-1.126v-.476h-.052Zm-3.43-1.386c0 .693.572 1.126 1.42 1.126 1.11 0 2.02-.719 2.02-1.723v-.676l-1.959.121c-.944.07-1.48.494-1.48 1.152Z',
})

View File

@ -4,7 +4,7 @@ import {View} from 'react-native'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play' import {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play'
export function PlayButtonIcon({size = 36}: {size?: number}) { export function PlayButtonIcon({size = 32}: {size?: number}) {
const t = useTheme() const t = useTheme()
const bg = t.name === 'light' ? t.palette.contrast_25 : t.palette.contrast_975 const bg = t.name === 'light' ? t.palette.contrast_25 : t.palette.contrast_975
const fg = t.name === 'light' ? t.palette.contrast_975 : t.palette.contrast_25 const fg = t.name === 'light' ? t.palette.contrast_975 : t.palette.contrast_25

View File

@ -2,12 +2,12 @@ import React from 'react'
import {AppState, AppStateStatus} from 'react-native' import {AppState, AppStateStatus} from 'react-native'
import AsyncStorage from '@react-native-async-storage/async-storage' import AsyncStorage from '@react-native-async-storage/async-storage'
import {createClient, SegmentClient} from '@segment/analytics-react-native' import {createClient, SegmentClient} from '@segment/analytics-react-native'
import * as Sentry from '@sentry/react-native'
import {sha256} from 'js-sha256' import {sha256} from 'js-sha256'
import {Native} from 'sentry-expo'
import {useSession, SessionAccount} from '#/state/session'
import {ScreenPropertiesMap, TrackPropertiesMap} from './types'
import {logger} from '#/logger' import {logger} from '#/logger'
import {SessionAccount, useSession} from '#/state/session'
import {ScreenPropertiesMap, TrackPropertiesMap} from './types'
type AppInfo = { type AppInfo = {
build?: string | undefined build?: string | undefined
@ -72,7 +72,7 @@ export function init(account: SessionAccount | undefined) {
if (account.did) { if (account.did) {
const did_hashed = sha256(account.did) const did_hashed = sha256(account.did)
client.identify(did_hashed, {did_hashed}) client.identify(did_hashed, {did_hashed})
Native.setUser({id: did_hashed}) Sentry.setUser({id: did_hashed})
logger.debug('Ping w/hash') logger.debug('Ping w/hash')
} else { } else {
logger.debug('Ping w/o hash') logger.debug('Ping w/o hash')

View File

@ -1,11 +1,11 @@
import React from 'react' import React from 'react'
import {createClient} from '@segment/analytics-react' import {createClient} from '@segment/analytics-react'
import * as Sentry from '@sentry/react-native'
import {sha256} from 'js-sha256' import {sha256} from 'js-sha256'
import {Browser} from 'sentry-expo'
import {ScreenPropertiesMap, TrackPropertiesMap} from './types'
import {useSession, SessionAccount} from '#/state/session'
import {logger} from '#/logger' import {logger} from '#/logger'
import {SessionAccount, useSession} from '#/state/session'
import {ScreenPropertiesMap, TrackPropertiesMap} from './types'
type SegmentClient = ReturnType<typeof createClient> type SegmentClient = ReturnType<typeof createClient>
@ -70,7 +70,7 @@ export function init(account: SessionAccount | undefined) {
if (account.did) { if (account.did) {
const did_hashed = sha256(account.did) const did_hashed = sha256(account.did)
client.identify(did_hashed, {did_hashed}) client.identify(did_hashed, {did_hashed})
Browser.setUser({id: did_hashed}) Sentry.setUser({id: did_hashed})
logger.debug('Ping w/hash') logger.debug('Ping w/hash')
} else { } else {
logger.debug('Ping w/o hash') logger.debug('Ping w/o hash')

View File

@ -2,6 +2,7 @@ import {
AppBskyFeedDefs, AppBskyFeedDefs,
AppBskyFeedGetFeed as GetCustomFeed, AppBskyFeedGetFeed as GetCustomFeed,
BskyAgent, BskyAgent,
jsonStringToLex,
} from '@atproto/api' } from '@atproto/api'
import {getContentLanguages} from '#/state/preferences/languages' import {getContentLanguages} from '#/state/preferences/languages'
@ -111,7 +112,7 @@ async function loggedOutFetch({
}&limit=${limit}&lang=${contentLangs}`, }&limit=${limit}&lang=${contentLangs}`,
{method: 'GET', headers: {'Accept-Language': contentLangs}}, {method: 'GET', headers: {'Accept-Language': contentLangs}},
) )
let data = res.ok ? await res.json() : null let data = res.ok ? jsonStringToLex(await res.text()) : null
if (data?.feed?.length) { if (data?.feed?.length) {
return { return {
success: true, success: true,
@ -126,7 +127,7 @@ async function loggedOutFetch({
}&limit=${limit}`, }&limit=${limit}`,
{method: 'GET', headers: {'Accept-Language': ''}}, {method: 'GET', headers: {'Accept-Language': ''}},
) )
data = res.ok ? await res.json() : null data = res.ok ? jsonStringToLex(await res.text()) : null
if (data?.feed?.length) { if (data?.feed?.length) {
return { return {
success: true, success: true,

15
src/lib/canvas.ts 100644
View File

@ -0,0 +1,15 @@
export const getCanvas = (base64: string): Promise<HTMLCanvasElement> => {
return new Promise(resolve => {
const image = new Image()
image.onload = () => {
const canvas = document.createElement('canvas')
canvas.width = image.width
canvas.height = image.height
const ctx = canvas.getContext('2d')
ctx?.drawImage(image, 0, 0)
resolve(canvas)
}
image.src = base64
})
}

View File

@ -10,7 +10,7 @@ import {
} from '#/components/icons/Heart2' } from '#/components/icons/Heart2'
const animationConfig = { const animationConfig = {
duration: 400, duration: 600,
easing: 'cubic-bezier(0.4, 0, 0.2, 1)', easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
fill: 'forwards' as FillMode, fill: 'forwards' as FillMode,
} }

View File

@ -71,7 +71,7 @@ export function useIntentHandler() {
}, [incomingUrl, composeIntent, verifyEmailIntent]) }, [incomingUrl, composeIntent, verifyEmailIntent])
} }
function useComposeIntent() { export function useComposeIntent() {
const closeAllActiveElements = useCloseAllActiveElements() const closeAllActiveElements = useCloseAllActiveElements()
const {openComposer} = useComposerControls() const {openComposer} = useComposerControls()
const {hasSession} = useSession() const {hasSession} = useSession()

View File

@ -5,7 +5,7 @@
import {Platform} from 'react-native' import {Platform} from 'react-native'
import {nativeApplicationVersion, nativeBuildVersion} from 'expo-application' import {nativeApplicationVersion, nativeBuildVersion} from 'expo-application'
import {init} from 'sentry-expo' import {init} from '@sentry/react-native'
import {BUILD_ENV, IS_DEV, IS_TESTFLIGHT} from 'lib/app-info' import {BUILD_ENV, IS_DEV, IS_TESTFLIGHT} from 'lib/app-info'
@ -30,10 +30,10 @@ const dist = `${Platform.OS}.${nativeBuildVersion}.${
}${IS_DEV ? 'dev' : ''}` }${IS_DEV ? 'dev' : ''}`
init({ init({
enabled: !__DEV__,
autoSessionTracking: false, autoSessionTracking: false,
dsn: 'https://05bc3789bf994b81bd7ce20c86ccd3ae@o4505071687041024.ingest.sentry.io/4505071690514432', dsn: 'https://05bc3789bf994b81bd7ce20c86ccd3ae@o4505071687041024.ingest.sentry.io/4505071690514432',
debug: false, // If `true`, Sentry will try to print out useful debugging information if something goes wrong with sending the event. Set it to `false` in production debug: false, // If `true`, Sentry will try to print out useful debugging information if something goes wrong with sending the event. Set it to `false` in production
enableInExpoDevelopment: false, // enable this to test in dev
environment: BUILD_ENV ?? 'development', environment: BUILD_ENV ?? 'development',
dist, dist,
release, release,

View File

@ -225,4 +225,8 @@ export type LogEvents = {
'test:gate1:sometimes': {} 'test:gate1:sometimes': {}
'test:gate2:always': {} 'test:gate2:always': {}
'test:gate2:sometimes': {} 'test:gate2:sometimes': {}
'tmd:share': {}
'tmd:download': {}
'tmd:post': {}
} }

View File

@ -79,13 +79,13 @@ export const s = StyleSheet.create({
// font weights // font weights
fw600: {fontWeight: '600'}, fw600: {fontWeight: '600'},
bold: {fontWeight: 'bold'}, bold: {fontWeight: '700'},
fw500: {fontWeight: '500'}, fw500: {fontWeight: '500'},
semiBold: {fontWeight: '500'}, semiBold: {fontWeight: '500'},
fw400: {fontWeight: '400'}, fw400: {fontWeight: '400'},
normal: {fontWeight: '400'}, normal: {fontWeight: '400'},
fw300: {fontWeight: '300'}, fw300: {fontWeight: '400'},
light: {fontWeight: '300'}, light: {fontWeight: '400'},
fw200: {fontWeight: '200'}, fw200: {fontWeight: '200'},
// text decoration // text decoration

View File

@ -1,5 +1,6 @@
import {Platform} from 'react-native' import {Platform} from 'react-native'
import {tokens} from '#/alf'
import {darkPalette, dimPalette, lightPalette} from '#/alf/themes' import {darkPalette, dimPalette, lightPalette} from '#/alf/themes'
import {colors} from './styles' import {colors} from './styles'
import type {Theme} from './ThemeContext' import type {Theme} from './ThemeContext'
@ -88,163 +89,163 @@ export const defaultTheme: Theme = {
typography: { typography: {
'2xl-thin': { '2xl-thin': {
fontSize: 18, fontSize: 18,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '300', fontWeight: '400',
}, },
'2xl': { '2xl': {
fontSize: 18, fontSize: 18,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '400', fontWeight: '400',
}, },
'2xl-medium': { '2xl-medium': {
fontSize: 18, fontSize: 18,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '500', fontWeight: '500',
}, },
'2xl-bold': { '2xl-bold': {
fontSize: 18, fontSize: 18,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '700', fontWeight: '700',
}, },
'2xl-heavy': { '2xl-heavy': {
fontSize: 18, fontSize: 18,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '800', fontWeight: '800',
}, },
'xl-thin': { 'xl-thin': {
fontSize: 17, fontSize: 17,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '300', fontWeight: '400',
}, },
xl: { xl: {
fontSize: 17, fontSize: 17,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '400', fontWeight: '400',
}, },
'xl-medium': { 'xl-medium': {
fontSize: 17, fontSize: 17,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '500', fontWeight: '500',
}, },
'xl-bold': { 'xl-bold': {
fontSize: 17, fontSize: 17,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '700', fontWeight: '700',
}, },
'xl-heavy': { 'xl-heavy': {
fontSize: 17, fontSize: 17,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '800', fontWeight: '800',
}, },
'lg-thin': { 'lg-thin': {
fontSize: 16, fontSize: 16,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '300', fontWeight: '400',
}, },
lg: { lg: {
fontSize: 16, fontSize: 16,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '400', fontWeight: '400',
}, },
'lg-medium': { 'lg-medium': {
fontSize: 16, fontSize: 16,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '500', fontWeight: '500',
}, },
'lg-bold': { 'lg-bold': {
fontSize: 16, fontSize: 16,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '700', fontWeight: '700',
}, },
'lg-heavy': { 'lg-heavy': {
fontSize: 16, fontSize: 16,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '800', fontWeight: '800',
}, },
'md-thin': { 'md-thin': {
fontSize: 15, fontSize: 15,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '300', fontWeight: '400',
}, },
md: { md: {
fontSize: 15, fontSize: 15,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '400', fontWeight: '400',
}, },
'md-medium': { 'md-medium': {
fontSize: 15, fontSize: 15,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '500', fontWeight: '500',
}, },
'md-bold': { 'md-bold': {
fontSize: 15, fontSize: 15,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '700', fontWeight: '700',
}, },
'md-heavy': { 'md-heavy': {
fontSize: 15, fontSize: 15,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '800', fontWeight: '800',
}, },
'sm-thin': { 'sm-thin': {
fontSize: 14, fontSize: 14,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '300', fontWeight: '400',
}, },
sm: { sm: {
fontSize: 14, fontSize: 14,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '400', fontWeight: '400',
}, },
'sm-medium': { 'sm-medium': {
fontSize: 14, fontSize: 14,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '500', fontWeight: '500',
}, },
'sm-bold': { 'sm-bold': {
fontSize: 14, fontSize: 14,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '700', fontWeight: '700',
}, },
'sm-heavy': { 'sm-heavy': {
fontSize: 14, fontSize: 14,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '800', fontWeight: '800',
}, },
'xs-thin': { 'xs-thin': {
fontSize: 13, fontSize: 13,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '300', fontWeight: '400',
}, },
xs: { xs: {
fontSize: 13, fontSize: 13,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '400', fontWeight: '400',
}, },
'xs-medium': { 'xs-medium': {
fontSize: 13, fontSize: 13,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '500', fontWeight: '500',
}, },
'xs-bold': { 'xs-bold': {
fontSize: 13, fontSize: 13,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '700', fontWeight: '700',
}, },
'xs-heavy': { 'xs-heavy': {
fontSize: 13, fontSize: 13,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '800', fontWeight: '800',
}, },
'title-2xl': { 'title-2xl': {
fontSize: 34, fontSize: 34,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '500', fontWeight: '500',
}, },
'title-xl': { 'title-xl': {
fontSize: 28, fontSize: 28,
letterSpacing: 0.25, letterSpacing: tokens.TRACKING,
fontWeight: '500', fontWeight: '500',
}, },
'title-lg': { 'title-lg': {
@ -254,32 +255,32 @@ export const defaultTheme: Theme = {
title: { title: {
fontWeight: '500', fontWeight: '500',
fontSize: 20, fontSize: 20,
letterSpacing: 0.15, letterSpacing: tokens.TRACKING,
}, },
'title-sm': { 'title-sm': {
fontWeight: 'bold', fontWeight: 'bold',
fontSize: 17, fontSize: 17,
letterSpacing: 0.15, letterSpacing: tokens.TRACKING,
}, },
'post-text': { 'post-text': {
fontSize: 16, fontSize: 16,
letterSpacing: 0.2, letterSpacing: tokens.TRACKING,
fontWeight: '400', fontWeight: '400',
}, },
'post-text-lg': { 'post-text-lg': {
fontSize: 20, fontSize: 20,
letterSpacing: 0.2, letterSpacing: tokens.TRACKING,
fontWeight: '400', fontWeight: '400',
}, },
'button-lg': { 'button-lg': {
fontWeight: '500', fontWeight: '500',
fontSize: 18, fontSize: 18,
letterSpacing: 0.5, letterSpacing: tokens.TRACKING,
}, },
button: { button: {
fontWeight: '500', fontWeight: '500',
fontSize: 14, fontSize: 14,
letterSpacing: 0.5, letterSpacing: tokens.TRACKING,
}, },
mono: { mono: {
fontSize: 14, fontSize: 14,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
import {beforeAll, describe, expect, jest, test} from '@jest/globals'
import * as Sentry from '@sentry/react-native'
import {nanoid} from 'nanoid/non-secure' import {nanoid} from 'nanoid/non-secure'
import {jest, describe, expect, test, beforeAll} from '@jest/globals'
import {Native as Sentry} from 'sentry-expo'
import {Logger, LogLevel, sentryTransport} from '#/logger' import {Logger, LogLevel, sentryTransport} from '#/logger'
@ -16,12 +16,10 @@ jest.mock('#/env', () => ({
LOG_DEBUG: '', LOG_DEBUG: '',
})) }))
jest.mock('sentry-expo', () => ({ jest.mock('@sentry/react-native', () => ({
Native: {
addBreadcrumb: jest.fn(), addBreadcrumb: jest.fn(),
captureException: jest.fn(), captureException: jest.fn(),
captureMessage: jest.fn(), captureMessage: jest.fn(),
},
})) }))
beforeAll(() => { beforeAll(() => {

View File

@ -1 +1 @@
export {Native as Sentry} from 'sentry-expo' export * as Sentry from '@sentry/react-native'

View File

@ -1 +1 @@
export {Browser as Sentry} from 'sentry-expo' export * as Sentry from '@sentry/react-native'

View File

@ -144,8 +144,7 @@ export const ForgotPasswordForm = ({
variant="solid" variant="solid"
color={'primary'} color={'primary'}
size="medium" size="medium"
onPress={onPressNext} onPress={onPressNext}>
disabled={!email}>
<ButtonText> <ButtonText>
<Trans>Next</Trans> <Trans>Next</Trans>
</ButtonText> </ButtonText>

View File

@ -60,7 +60,6 @@ export const LoginForm = ({
const {track} = useAnalytics() const {track} = useAnalytics()
const t = useTheme() const t = useTheme()
const [isProcessing, setIsProcessing] = useState<boolean>(false) const [isProcessing, setIsProcessing] = useState<boolean>(false)
const [isReady, setIsReady] = useState<boolean>(false)
const [isAuthFactorTokenNeeded, setIsAuthFactorTokenNeeded] = const [isAuthFactorTokenNeeded, setIsAuthFactorTokenNeeded] =
useState<boolean>(false) useState<boolean>(false)
const identifierValueRef = useRef<string>(initialHandle || '') const identifierValueRef = useRef<string>(initialHandle || '')
@ -83,12 +82,18 @@ export const LoginForm = ({
Keyboard.dismiss() Keyboard.dismiss()
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
setError('') setError('')
setIsProcessing(true)
const identifier = identifierValueRef.current.toLowerCase().trim() const identifier = identifierValueRef.current.toLowerCase().trim()
const password = passwordValueRef.current const password = passwordValueRef.current
const authFactorToken = authFactorTokenValueRef.current const authFactorToken = authFactorTokenValueRef.current
if (!identifier || !password) {
setError(_(msg`Invalid username or password`))
return
}
setIsProcessing(true)
try { try {
// try to guess the handle if the user just gave their own username // try to guess the handle if the user just gave their own username
let fullIdent = identifier let fullIdent = identifier
@ -157,22 +162,6 @@ export const LoginForm = ({
} }
} }
const checkIsReady = () => {
if (
!!serviceDescription &&
!!identifierValueRef.current &&
!!passwordValueRef.current
) {
if (!isReady) {
setIsReady(true)
}
} else {
if (isReady) {
setIsReady(false)
}
}
}
return ( return (
<FormContainer testID="loginForm" titleText={<Trans>Sign in</Trans>}> <FormContainer testID="loginForm" titleText={<Trans>Sign in</Trans>}>
<View> <View>
@ -204,7 +193,6 @@ export const LoginForm = ({
defaultValue={initialHandle || ''} defaultValue={initialHandle || ''}
onChangeText={v => { onChangeText={v => {
identifierValueRef.current = v identifierValueRef.current = v
checkIsReady()
}} }}
onSubmitEditing={() => { onSubmitEditing={() => {
passwordRef.current?.focus() passwordRef.current?.focus()
@ -233,7 +221,6 @@ export const LoginForm = ({
clearButtonMode="while-editing" clearButtonMode="while-editing"
onChangeText={v => { onChangeText={v => {
passwordValueRef.current = v passwordValueRef.current = v
checkIsReady()
}} }}
onSubmitEditing={onPressNext} onSubmitEditing={onPressNext}
blurOnSubmit={false} // HACK: https://github.com/facebook/react-native/issues/21911#issuecomment-558343069 Keyboard blur behavior is now handled in onSubmitEditing blurOnSubmit={false} // HACK: https://github.com/facebook/react-native/issues/21911#issuecomment-558343069 Keyboard blur behavior is now handled in onSubmitEditing
@ -325,7 +312,7 @@ export const LoginForm = ({
<Trans>Connecting...</Trans> <Trans>Connecting...</Trans>
</Text> </Text>
</> </>
) : isReady ? ( ) : (
<Button <Button
testID="loginNextButton" testID="loginNextButton"
label={_(msg`Next`)} label={_(msg`Next`)}
@ -339,7 +326,7 @@ export const LoginForm = ({
</ButtonText> </ButtonText>
{isProcessing && <ButtonIcon icon={Loader} />} {isProcessing && <ButtonIcon icon={Loader} />}
</Button> </Button>
) : undefined} )}
</View> </View>
</FormContainer> </FormContainer>
) )

View File

@ -14,17 +14,21 @@ import {s} from '#/lib/styles'
import {useSetThemePrefs, useThemePrefs} from '#/state/shell' import {useSetThemePrefs, useThemePrefs} from '#/state/shell'
import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader' import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader'
import {ScrollView} from '#/view/com/util/Views' import {ScrollView} from '#/view/com/util/Views'
import {atoms as a, native, useTheme} from '#/alf' import {atoms as a, native, useAlf, useTheme} from '#/alf'
import * as ToggleButton from '#/components/forms/ToggleButton' import * as ToggleButton from '#/components/forms/ToggleButton'
import {Props as SVGIconProps} from '#/components/icons/common'
import {Moon_Stroke2_Corner0_Rounded as MoonIcon} from '#/components/icons/Moon' import {Moon_Stroke2_Corner0_Rounded as MoonIcon} from '#/components/icons/Moon'
import {Phone_Stroke2_Corner0_Rounded as PhoneIcon} from '#/components/icons/Phone' import {Phone_Stroke2_Corner0_Rounded as PhoneIcon} from '#/components/icons/Phone'
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' import {Text} from '#/components/Typography'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppearanceSettings'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppearanceSettings'>
export function AppearanceSettingsScreen({}: Props) { export function AppearanceSettingsScreen({}: Props) {
const {_} = useLingui()
const t = useTheme() const t = useTheme()
const {_} = useLingui()
const {isTabletOrMobile} = useWebMediaQueries() const {isTabletOrMobile} = useWebMediaQueries()
const {fonts} = useAlf()
const {colorMode, darkTheme} = useThemePrefs() const {colorMode, darkTheme} = useThemePrefs()
const {setColorMode, setDarkTheme} = useSetThemePrefs() const {setColorMode, setDarkTheme} = useSetThemePrefs()
@ -54,6 +58,22 @@ export function AppearanceSettingsScreen({}: Props) {
[setDarkTheme, darkTheme], [setDarkTheme, darkTheme],
) )
const onChangeFontFamily = useCallback(
(values: string[]) => {
const next = values[0] === 'system' ? 'system' : 'theme'
fonts.setFontFamily(next)
},
[fonts],
)
const onChangeFontScale = useCallback(
(values: string[]) => {
const next = values[0] || ('0' as any)
fonts.setFontScale(next)
},
[fonts],
)
return ( return (
<LayoutAnimationConfig skipExiting skipEntering> <LayoutAnimationConfig skipExiting skipEntering>
<View testID="preferencesThreadsScreen" style={s.hContentRegion}> <View testID="preferencesThreadsScreen" style={s.hContentRegion}>
@ -71,65 +91,143 @@ export function AppearanceSettingsScreen({}: Props) {
</View> </View>
</SimpleViewHeader> </SimpleViewHeader>
<View style={[a.p_xl, a.gap_lg]}> <View style={[a.gap_3xl, a.pt_xl, a.px_xl]}>
<View style={[a.flex_row, a.align_center, a.gap_md]}> <View style={[a.gap_lg]}>
<PhoneIcon style={t.atoms.text} /> <AppearanceToggleButtonGroup
<Text style={a.text_md}> title={_(msg`Color mode`)}
<Trans>Mode</Trans> icon={PhoneIcon}
</Text> items={[
</View> {
<ToggleButton.Group label: _(msg`System`),
label={_(msg`Dark mode`)} name: 'system',
},
{
label: _(msg`Light`),
name: 'light',
},
{
label: _(msg`Dark`),
name: 'dark',
},
]}
values={[colorMode]} values={[colorMode]}
onChange={onChangeAppearance}> onChange={onChangeAppearance}
<ToggleButton.Button label={_(msg`System`)} name="system"> />
<ToggleButton.ButtonText>
<Trans>System</Trans>
</ToggleButton.ButtonText>
</ToggleButton.Button>
<ToggleButton.Button label={_(msg`Light`)} name="light">
<ToggleButton.ButtonText>
<Trans>Light</Trans>
</ToggleButton.ButtonText>
</ToggleButton.Button>
<ToggleButton.Button label={_(msg`Dark`)} name="dark">
<ToggleButton.ButtonText>
<Trans>Dark</Trans>
</ToggleButton.ButtonText>
</ToggleButton.Button>
</ToggleButton.Group>
{colorMode !== 'light' && ( {colorMode !== 'light' && (
<Animated.View <Animated.View
entering={native(FadeInDown)} entering={native(FadeInDown)}
exiting={native(FadeOutDown)} exiting={native(FadeOutDown)}>
style={[a.mt_md, a.gap_lg]}> <AppearanceToggleButtonGroup
<View style={[a.flex_row, a.align_center, a.gap_md]}> title={_(msg`Dark theme`)}
<MoonIcon style={t.atoms.text} /> icon={MoonIcon}
<Text style={a.text_md}> items={[
<Trans>Dark theme</Trans> {
</Text> label: _(msg`Dim`),
</View> name: 'dim',
},
<ToggleButton.Group {
label={_(msg`Dark theme`)} label: _(msg`Dark`),
name: 'dark',
},
]}
values={[darkTheme ?? 'dim']} values={[darkTheme ?? 'dim']}
onChange={onChangeDarkTheme}> onChange={onChangeDarkTheme}
<ToggleButton.Button label={_(msg`Dim`)} name="dim"> />
<ToggleButton.ButtonText>
<Trans>Dim</Trans>
</ToggleButton.ButtonText>
</ToggleButton.Button>
<ToggleButton.Button label={_(msg`Dark`)} name="dark">
<ToggleButton.ButtonText>
<Trans>Dark</Trans>
</ToggleButton.ButtonText>
</ToggleButton.Button>
</ToggleButton.Group>
</Animated.View> </Animated.View>
)} )}
<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> </View>
</ScrollView> </ScrollView>
</View> </View>
</LayoutAnimationConfig> </LayoutAnimationConfig>
) )
} }
export function AppearanceToggleButtonGroup({
title,
description,
icon: Icon,
items,
values,
onChange,
}: {
title: string
description?: string
icon: React.ComponentType<SVGIconProps>
items: {
label: string
name: string
}[]
values: string[]
onChange: (values: string[]) => void
}) {
const t = useTheme()
return (
<View style={[a.gap_md]}>
<View style={[a.gap_xs]}>
<View style={[a.flex_row, a.align_center, a.gap_md]}>
<Icon style={t.atoms.text} />
<Text style={[a.text_md, a.font_bold]}>{title}</Text>
</View>
{description && (
<Text
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
{description}
</Text>
)}
</View>
<ToggleButton.Group label={title} values={values} onChange={onChange}>
{items.map(item => (
<ToggleButton.Button
key={item.name}
label={item.label}
name={item.name}>
<ToggleButton.ButtonText>{item.label}</ToggleButton.ButtonText>
</ToggleButton.Button>
))}
</ToggleButton.Group>
</View>
)
}

View File

@ -1,19 +1,20 @@
import {makeAutoObservable, runInAction} from 'mobx' import {makeAutoObservable, runInAction} from 'mobx'
import {ImageModel} from './image'
import {Image as RNImage} from 'react-native-image-crop-picker'
import {openPicker} from 'lib/media/picker'
import {getImageDim} from 'lib/media/manip' import {getImageDim} from 'lib/media/manip'
import {openPicker} from 'lib/media/picker'
import {ImageInitOptions, ImageModel} from './image'
interface InitialImageUri { interface InitialImageUri {
uri: string uri: string
width: number width: number
height: number height: number
altText?: string
} }
export class GalleryModel { export class GalleryModel {
images: ImageModel[] = [] images: ImageModel[] = []
constructor(uris?: {uri: string; width: number; height: number}[]) { constructor(uris?: InitialImageUri[]) {
makeAutoObservable(this) makeAutoObservable(this)
if (uris) { if (uris) {
@ -33,7 +34,7 @@ export class GalleryModel {
return this.images.some(image => image.altText.trim() === '') return this.images.some(image => image.altText.trim() === '')
} }
*add(image_: Omit<RNImage, 'size'>) { *add(image_: ImageInitOptions) {
if (this.size >= 4) { if (this.size >= 4) {
return return
} }
@ -59,7 +60,6 @@ export class GalleryModel {
path: uri, path: uri,
height, height,
width, width,
mime: 'image/jpeg',
} }
runInAction(() => { runInAction(() => {
@ -100,10 +100,10 @@ export class GalleryModel {
async addFromUris(uris: InitialImageUri[]) { async addFromUris(uris: InitialImageUri[]) {
for (const uriObj of uris) { for (const uriObj of uris) {
this.add({ this.add({
mime: 'image/jpeg',
height: uriObj.height, height: uriObj.height,
width: uriObj.width, width: uriObj.width,
path: uriObj.uri, path: uriObj.uri,
altText: uriObj.altText,
}) })
} }
} }

View File

@ -1,14 +1,15 @@
import {Image as RNImage} from 'react-native-image-crop-picker' import {Image as RNImage} from 'react-native-image-crop-picker'
import {makeAutoObservable, runInAction} from 'mobx'
import {POST_IMG_MAX} from 'lib/constants'
import * as ImageManipulator from 'expo-image-manipulator' import * as ImageManipulator from 'expo-image-manipulator'
import {getDataUriSize} from 'lib/media/util'
import {openCropper} from 'lib/media/picker'
import {ActionCrop, FlipType, SaveFormat} from 'expo-image-manipulator' import {ActionCrop, FlipType, SaveFormat} from 'expo-image-manipulator'
import {makeAutoObservable, runInAction} from 'mobx'
import {Position} from 'react-avatar-editor' import {Position} from 'react-avatar-editor'
import {Dimensions} from 'lib/media/types'
import {isIOS} from 'platform/detection'
import {logger} from '#/logger' import {logger} from '#/logger'
import {POST_IMG_MAX} from 'lib/constants'
import {openCropper} from 'lib/media/picker'
import {Dimensions} from 'lib/media/types'
import {getDataUriSize} from 'lib/media/util'
import {isIOS} from 'platform/detection'
export interface ImageManipulationAttributes { export interface ImageManipulationAttributes {
aspectRatio?: '4:3' | '1:1' | '3:4' | 'None' aspectRatio?: '4:3' | '1:1' | '3:4' | 'None'
@ -19,6 +20,13 @@ export interface ImageManipulationAttributes {
flipVertical?: boolean flipVertical?: boolean
} }
export interface ImageInitOptions {
path: string
width: number
height: number
altText?: string
}
const MAX_IMAGE_SIZE_IN_BYTES = 976560 const MAX_IMAGE_SIZE_IN_BYTES = 976560
export class ImageModel implements Omit<RNImage, 'size'> { export class ImageModel implements Omit<RNImage, 'size'> {
@ -41,12 +49,15 @@ export class ImageModel implements Omit<RNImage, 'size'> {
} }
prevAttributes: ImageManipulationAttributes = {} prevAttributes: ImageManipulationAttributes = {}
constructor(image: Omit<RNImage, 'size'>) { constructor(image: ImageInitOptions) {
makeAutoObservable(this) makeAutoObservable(this)
this.path = image.path this.path = image.path
this.width = image.width this.width = image.width
this.height = image.height this.height = image.height
if (image.altText !== undefined) {
this.setAltText(image.altText)
}
} }
setRatio(aspectRatio: ImageManipulationAttributes['aspectRatio']) { setRatio(aspectRatio: ImageManipulationAttributes['aspectRatio']) {

View File

@ -21,31 +21,7 @@ export function useFeedTuners(feedDesc: FeedDescriptor) {
if (feedDesc.startsWith('feedgen')) { if (feedDesc.startsWith('feedgen')) {
return [FeedTuner.preferredLangOnly(langPrefs.contentLanguages)] return [FeedTuner.preferredLangOnly(langPrefs.contentLanguages)]
} }
if (feedDesc.startsWith('list')) { if (feedDesc === 'following' || feedDesc.startsWith('list')) {
let feedTuners = []
if (feedDesc.endsWith('|as_following')) {
// Same as Following tuners below, copypaste for now.
feedTuners.push(FeedTuner.removeOrphans)
if (preferences?.feedViewPrefs.hideReposts) {
feedTuners.push(FeedTuner.removeReposts)
}
if (preferences?.feedViewPrefs.hideReplies) {
feedTuners.push(FeedTuner.removeReplies)
} else {
feedTuners.push(
FeedTuner.followedRepliesOnly({
userDid: currentAccount?.did || '',
}),
)
}
if (preferences?.feedViewPrefs.hideQuotePosts) {
feedTuners.push(FeedTuner.removeQuotePosts)
}
feedTuners.push(FeedTuner.dedupThreads)
}
return feedTuners
}
if (feedDesc === 'following') {
const feedTuners = [FeedTuner.removeOrphans] const feedTuners = [FeedTuner.removeOrphans]
if (preferences?.feedViewPrefs.hideReposts) { if (preferences?.feedViewPrefs.hideReposts) {

View File

@ -175,19 +175,9 @@ async function fetchSubjects(
}> { }> {
const postUris = new Set<string>() const postUris = new Set<string>()
const packUris = new Set<string>() const packUris = new Set<string>()
const postUrisWithLikes = new Set<string>()
const postUrisWithReposts = new Set<string>()
for (const notif of groupedNotifs) { for (const notif of groupedNotifs) {
if (notif.subjectUri?.includes('app.bsky.feed.post')) { if (notif.subjectUri?.includes('app.bsky.feed.post')) {
postUris.add(notif.subjectUri) postUris.add(notif.subjectUri)
if (notif.type === 'post-like') {
postUrisWithLikes.add(notif.subjectUri)
}
if (notif.type === 'repost') {
postUrisWithReposts.add(notif.subjectUri)
}
} else if ( } else if (
notif.notification.reasonSubject?.includes('app.bsky.graph.starterpack') notif.notification.reasonSubject?.includes('app.bsky.graph.starterpack')
) { ) {
@ -216,15 +206,6 @@ async function fetchSubjects(
AppBskyFeedPost.validateRecord(post.record).success AppBskyFeedPost.validateRecord(post.record).success
) { ) {
postsMap.set(post.uri, post) postsMap.set(post.uri, post)
// HACK. In some cases, the appview appears to lag behind and returns empty counters.
// To prevent scroll jump due to missing metrics, fill in 1 like/repost instead of 0.
if (post.likeCount === 0 && postUrisWithLikes.has(post.uri)) {
post.likeCount = 1
}
if (post.repostCount === 0 && postUrisWithReposts.has(post.uri)) {
post.repostCount = 1
}
} }
} }
for (const pack of packsChunks.flat()) { for (const pack of packsChunks.flat()) {

View File

@ -3,27 +3,23 @@ import zod from 'zod'
import {BaseNux} from '#/state/queries/nuxs/types' import {BaseNux} from '#/state/queries/nuxs/types'
export enum Nux { export enum Nux {
One = 'one', TenMillionDialog = 'TenMillionDialog',
Two = 'two', NeueTypography = 'NeueTypography',
} }
export const nuxNames = new Set(Object.values(Nux)) export const nuxNames = new Set(Object.values(Nux))
export type AppNux = export type AppNux =
| BaseNux<{ | BaseNux<{
id: Nux.One id: Nux.TenMillionDialog
data: { data: undefined
likes: number
}
}> }>
| BaseNux<{ | BaseNux<{
id: Nux.Two id: Nux.NeueTypography
data: undefined data: undefined
}> }>
export const NuxSchemas = { export const NuxSchemas: Record<Nux, zod.ZodObject<any> | undefined> = {
[Nux.One]: zod.object({ [Nux.TenMillionDialog]: undefined,
likes: zod.number(), [Nux.NeueTypography]: undefined,
}),
[Nux.Two]: undefined,
} }

View File

@ -57,6 +57,7 @@ export function useUpsertNuxMutation() {
const agent = useAgent() const agent = useAgent()
return useMutation({ return useMutation({
retry: 3,
mutationFn: async (nux: AppNux) => { mutationFn: async (nux: AppNux) => {
await agent.bskyAppUpsertNux(serializeAppNux(nux)) await agent.bskyAppUpsertNux(serializeAppNux(nux))
// triggers a refetch // triggers a refetch
@ -72,6 +73,7 @@ export function useRemoveNuxsMutation() {
const agent = useAgent() const agent = useAgent()
return useMutation({ return useMutation({
retry: 3,
mutationFn: async (ids: string[]) => { mutationFn: async (ids: string[]) => {
await agent.bskyAppRemoveNuxs(ids) await agent.bskyAppRemoveNuxs(ids)
// triggers a refetch // triggers a refetch

View File

@ -4,6 +4,4 @@ export type Data = Record<string, unknown> | undefined
export type BaseNux< export type BaseNux<
T extends Pick<AppBskyActorDefs.Nux, 'id' | 'expiresAt'> & {data: Data}, T extends Pick<AppBskyActorDefs.Nux, 'id' | 'expiresAt'> & {data: Data},
> = T & { > = Pick<AppBskyActorDefs.Nux, 'id' | 'completed' | 'expiresAt'> & T
completed: boolean
}

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