Compare commits

...

25 Commits

Author SHA1 Message Date
Eric Bailey 5f6ba8fc02 Update ruby version to latest homebrew stable 2024-12-07 14:22:20 -06:00
dan ea6ac25684 Revert "[Video] Revert safari hackfix (#5367)" (#7001)
This reverts commit 94e7bfbe40.
2024-12-07 17:10:13 +00:00
Eric Bailey 296aab4736 Fix squashed content on mod screens (#7004)
* Fix squashed content on mod screens

* Fix lint
2024-12-07 17:09:46 +00:00
Samuel Newman 5297973f68 [ELI5] Change feed page copy (#6932) 2024-12-07 11:50:50 +00:00
Eric Bailey d05217e560 Immediately parse pre-filled links in composer state (#6974)
* Immediately parse pre-filled links in composer state

* Add hack to fix PasteInput height bug

* Parse out ext links separately from post links
2024-12-06 22:43:49 +00:00
Eric Bailey 8c68093d24 Add useFormatCurrency hook (#6978) 2024-12-06 16:29:34 -06:00
dan cd4c14e8db Update view shot (#6995) 2024-12-06 22:14:05 +00:00
dan d7b492e89a Remove icon (#6994)
Co-authored-by: Eric Bailey <git@esb.lol>
2024-12-06 20:37:46 +00:00
Hailey 1f6acc11ab clean rn 0.76 upgrade (#6887)
* package upgrades

* upgrade system ui

* update patches

* rename patch

* rm

* use .set/.set

* resolve yarnlock

* fix accidentally removed package

* fix use permissions hook

* fix some type errors

* type fixes

* more tweaking

* clean

* Discard changes to src/screens/Onboarding/StepProfile/index.tsx

* oops

* fix splash

* use ios/android in config

* Fix tests

* add back patch

* add to rn patch

* fullscreen?

* Revert "add to rn patch"

This reverts commit 4716d2c643.

* try this

* test with revert

* test

* maybe this

* fix config

* Bump @react-native-picker/picker

* Bump some packages

* Rm unused

* Update lockfile

* Rename expo-notifications+0.29.8.patch.md to expo-notifications+0.29.10.patch.md

* Update react-native+0.76.3.patch.md

* Update react-native+0.76.3.patch.md

* Inline splash configs

Jumping around the file is annoying and makes it harder to understand how this is structured.

* Start fixing Android splash

* Downgrade compressor

This version isn't building for me due to https://github.com/numandev1/react-native-compressor/issues/322.

* Make Android splash empty for now

* Work around a bug

* Bump the compressor

* Bump again

* Include splash fixes

* Try updating

* No custom Android splash

* Revert to using icons

* welp

* Fix sizes

* Make sizing work

* Bump size

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2024-12-06 17:52:08 +00:00
Eric Bailey 3ab6c435df Trim back prefs exposure in NUXs, make naming more friendly (#6980) 2024-12-06 09:42:42 -06:00
Eric Bailey 143e2c802d [Layout] Base (#6907)
* Add common gutter styles as hook

* Add computed scrollbar gutter CSS vars

* Add new layout components

* Replace layout components in settings screens

* Remove old back button

* Invert web border logic for easier migration

* Clean up Slot API

* Port over FF handling of scrollbar offset

* Trade boilerplate for ease of use

* Limit to one line

* Allow two lines, fix wrapping

* Fix alignment

* sticky headers

* set max with on header and center

* [Layout] Notifications Header (#6910)

* Replace notifications screen header

* fix cropped indicator

---------

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

* Replace Hashtag header (#6928)

* [Layout] ChatList header (#6929)

* Replace ChatList header

* update chat settings as well

---------

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

* Add web borders to Chat settings

* Remove unused var

* Move ChatList header outside center

* Replace empty chat layout

* fix breakpoints

* [Layout] Scrollbar gutters (#6908)

* Fix sidebar alignment

* Make sure scrollbars don't hide

* Gift left nav more space

* Use stable one-edge, update logic in RightNav

* Ope

* Increase width

* Reset

* Add transform to sidebars

* Remove bg in sidebars

* Handle shifts in layout components

* Replace scroll-removal handling

* Make react-remove-scroll an explicit dep

* Remove unused script

* use correct scroll insets (#6950)

* [Layout] Feeds headers (#6913)

* Replace ViewHeader internals, duplicate old ViewHeader

* Replace Feeds header

* Replace SavedFeeds header

* Visual alignment

* Uglier but clear

* Use old ViewHeader for SavedFeeds

* use Layout.Center instead of Layout.Content

* use left-aligned header for feed edit

* delete unused old view header

---------

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

* [Layout] Every other screen (#6953)

* attempt to fix double borders on every other screen

* delete ListHeaderDesktop

* delete `SimpleViewHeader` and fix screens (#6956)

* Make Layout.Center not full height

* Refactor List to use Layout.Center, remove built-in borders

* Fix Home screen

* Refactor PagerWithHeader to use Layout components

* Replace components in ProfileFeed and ProfileList

* Borders on Profile

* Search screen replacements

* use new header for profile subpage header (#6958)

* Search AutocompleteResults

* use new header for starter pack wizard (#6957)

* Fix post thread

* Enable borders by default

* Moderation muted and blocked accounts

* Fix scrollbar offset on Labeler

* Remove ScrollView from Moderation

* Remove ScrollView from Deactivated

* Remove ScrollView from onboarding

* Remove ScrollView from SignupQueued

* Mark deprecations

* fix lint

* Fix double borders on profile load

* Remove unneeded CenteredView from noty Feed

* Remove double Center layout on Notifications screen

* Remove double Center layout on ChatList screen

* Handle scrollbar offset in chat

* Use new atom for other scrollbar offsets

* Remove borders from old views

* Better doc

* Remove temp migration prop

* Fix new atom usage on native

* Clean up Hashtag screen

* Layout docs

* Clarify usage in Pager

* Handle nested offset contexts

* Clean up Layout

* fix feeds page

* asymmetric header on native (#6969)

* Reusable header const

* Fix up home header

* Add back button to convo

* Add hitslop to header buttons

* Comment

* Better handling on native for new atom

* Format

* Fix nested flatlist on mod screens

* Use react-remove-scroll-bar directly

* Fix notification count overflow on web

* Clarify doc

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
2024-12-05 18:59:26 -06:00
Eric Bailey 8467dfd452 Add some icons (#6976) 2024-12-05 18:15:30 -06:00
Foster 0ee20731d4 adjust feed scroll bar top offset (#6940)
Co-authored-by: skipness <ckhf@proton.me>
2024-12-04 21:42:15 +00:00
Laurence Gonsalves 4582a9cf1f Fix oEmbed provider_url and provider_name (#6942)
Fixes https://github.com/bluesky-social/social-app/issues/6941
2024-12-04 17:21:52 +00:00
Samuel Newman df3b43bb64 simplify date picker theme (#6926) 2024-12-03 23:57:26 +00:00
Paul Frazee 0c71f8196f Add link-click metrics (#6934)
* Add link-click metrics

* Fix conditional
2024-12-03 15:03:22 -08:00
Samuel Newman 79f807dc4f ALF TabBar (#6924)
* alf tabbar

* reduce indicator height
2024-12-03 18:03:43 +00:00
David Buchanan 3607cfb991 Update VideoClip icon - Fix pixel grid alignment (#6923)
* Update VideoClip.tsx

* minify path
2024-12-03 17:42:53 +00:00
Ryan McLeod 147efb628a Fix title/buttons in date picker not being visible on Android in dark mode (#6880) 2024-12-03 02:25:10 +00:00
dan 84f4afd3d7 Fix scroll gesture alignment (#6898) 2024-12-03 01:35:36 +00:00
dan cd811114ef [Nicer Tabs] New native pager (#6868)
* Remove tab bar autoscroll

This will be replaced by a different mechanism.

* Track pager drag gesture in a worklet

* Track pager state change in a worklet

* Track offset relative to current page

* Sync scroll to swipe

* Extract TabBarItem

* Sync scroll to swipe properly

* Implement all interactions

* Clarify more hacks

* Simplify the implementation

I was trying to be too smart and this was causing the current page event to lag behind if you continuously drag. Better to let the library do its job.

* Interpolate the indicator

* Fix an infinite swipe loop

* Add TODO

* Animate header color

* Respect initial page

* Keep layouts in a shared value

* Fix profile and types

* Fast path for initial styles

* Scroll to initial

* Factor out a helper

* Fix positioning

* Scroll into view on tap if needed

* Divide free space proportionally

* Scroll into view more aggressively

* Fix corner case

* Ignore spurious event on iOS

* Simplify the condition

Due to RN onLayout event ordering, we know that by now we'll have container and content sizes already.

* Change boolean state to enum

* Better syncing heuristic

* Rm extra return
2024-12-03 01:29:45 +00:00
dan 5a313c2d10 [Nicer Tabs] Fork TabBar, simplify Pager (#6762)
* Fork TabBar.web.tsx

* Trim dead code from both forks

* Remove onPageSelecting event

It's difficult to tell what exactly it's supposed to represent, and in practice it's not really used aside from logging. Let's rip it out for now to keep other changes simpler.

* Remove early onPageSelected call

It was added to try to do some work eagerly when we're sure which way the scroll is snapping. This is not necessarily a good idea though. It schedules a potentially expensive re-render right during the deceleration animation, which is not great. Whatever we're optimizing there, we should optimize smarter (e.g. prewarm just the network call). The other thing it used to help with is triggering the pager header autoscroll earlier. But we're going to rewrite that part differently anyway so that's not relevant either.

* Prune more dead code from the native version

We'll have to revisit this when adding tablet support but for now I'd prefer to remove a codepath that is not being tested or ever run.

* Use regular ScrollView on native

The Draggable thing was needed for web-only behavior so we can drop it in the native fork.
2024-12-03 01:12:58 +00:00
dan 996871d88b Prefetch adjacent Home pages (#6904) 2024-12-03 01:11:11 +00:00
Hailey 2f10be2cfe remove masked view dependency (#6886) 2024-12-02 16:16:39 -08:00
surfdude29 770eeb59ca Add new languages to CFBundleLocalizations key (#6835)
* Order tweak in lingui.config.js

* Add new languages to `CFBundleLocalizations` key

* Order tweak in dates.ts

* hyphen to en-dash

* change two `zh` tags to use script rather than region

* `zh_HK` ➡️ `yue-Hant`

* `pt` ➡️ `pt-BR`
2024-12-02 17:28:11 +00:00
165 changed files with 5638 additions and 5506 deletions
+1 -1
View File
@@ -1 +1 @@
2.7.6
3.3.6
+41 -41
View File
@@ -1,26 +1,6 @@
const pkg = require('./package.json')
const SPLASH_CONFIG = {
backgroundColor: '#ffffff',
image: './assets/splash.png',
resizeMode: 'cover',
}
const DARK_SPLASH_CONFIG = {
backgroundColor: '#001429',
image: './assets/splash-dark.png',
resizeMode: 'cover',
}
const SPLASH_CONFIG_ANDROID = {
backgroundColor: '#0c7cff',
image: './assets/splash.png',
resizeMode: 'cover',
}
const DARK_SPLASH_CONFIG_ANDROID = {
backgroundColor: '#0f141b',
image: './assets/splash-dark.png',
resizeMode: 'cover',
}
const DARK_SPLASH_ANDROID_BACKGROUND = '#0f141b'
module.exports = function (config) {
/**
@@ -73,8 +53,6 @@ module.exports = function (config) {
orientation: 'portrait',
icon: './assets/app-icons/ios_icon_default_light.png',
userInterfaceStyle: 'automatic',
splash: SPLASH_CONFIG,
// hsl(211, 99%, 53%), same as palette.default.brandText
primaryColor: '#1083fe',
ios: {
supportsTablet: false,
@@ -95,34 +73,35 @@ module.exports = function (config) {
CFBundleSpokenName: 'Blue Sky',
CFBundleLocalizations: [
'en',
'an',
'ast',
'ca',
'de',
'es',
'fi',
'fr',
'ga',
'gl',
'hi',
'hu',
'id',
'it',
'ja',
'ko',
'nl',
'pl',
'pt',
'pt-BR',
'ru',
'th',
'tr',
'uk',
'zh_CN',
'zh_HK',
'zh_TW',
'vi',
'yue-Hant',
'zh-Hans',
'zh-Hant',
],
},
associatedDomains: ASSOCIATED_DOMAINS,
splash: {
...SPLASH_CONFIG,
dark: DARK_SPLASH_CONFIG,
},
entitlements: {
'com.apple.developer.kernel.increased-memory-limit': true,
'com.apple.developer.kernel.extended-virtual-addressing': true,
@@ -159,7 +138,7 @@ module.exports = function (config) {
// Dark nav bar in light mode is better than light nav bar in dark mode
androidNavigationBar: {
barStyle: 'light-content',
backgroundColor: DARK_SPLASH_CONFIG_ANDROID.backgroundColor,
backgroundColor: DARK_SPLASH_ANDROID_BACKGROUND,
},
android: {
icon: './assets/app-icons/android_icon_default_light.png',
@@ -188,10 +167,6 @@ module.exports = function (config) {
category: ['BROWSABLE', 'DEFAULT'],
},
],
splash: {
...SPLASH_CONFIG_ANDROID,
dark: DARK_SPLASH_CONFIG_ANDROID,
},
},
web: {
favicon: './assets/favicon.png',
@@ -231,10 +206,9 @@ module.exports = function (config) {
newArchEnabled: false,
},
android: {
compileSdkVersion: 34,
targetSdkVersion: 34,
buildToolsVersion: '34.0.0',
kotlinVersion: '1.8.0',
compileSdkVersion: 35,
targetSdkVersion: 35,
buildToolsVersion: '35.0.0',
newArchEnabled: false,
},
},
@@ -251,7 +225,6 @@ module.exports = function (config) {
'./plugins/starterPackAppClipExtension/withStarterPackAppClip.js',
'./plugins/withAndroidManifestPlugin.js',
'./plugins/withAndroidManifestFCMIconPlugin.js',
'./plugins/withAndroidStylesWindowBackgroundPlugin.js',
'./plugins/withAndroidStylesAccentColorPlugin.js',
'./plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js',
'./plugins/shareExtension/withShareExtensions.js',
@@ -273,6 +246,33 @@ module.exports = function (config) {
],
},
],
[
'expo-splash-screen',
{
ios: {
enableFullScreenImage_legacy: true,
backgroundColor: '#ffffff',
image: './assets/splash.png',
resizeMode: 'cover',
dark: {
enableFullScreenImage_legacy: true,
backgroundColor: '#001429',
image: './assets/splash-dark.png',
resizeMode: 'cover',
},
},
android: {
backgroundColor: '#0c7cff',
image: './assets/splash-android-icon.png',
imageWidth: 150,
dark: {
backgroundColor: '#0c2a49',
image: './assets/splash-android-icon-dark.png',
imageWidth: 150,
},
},
},
],
[
'@mozzius/expo-dynamic-app-icon',
{
+1
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="m15.604 2.47-.642 1.112a5 5 0 0 1 2.05 4.034H7.005c0-1.658.809-3.123 2.05-4.034L8.41 2.47a.313.313 0 0 1 .542-.313l.627 1.086a5 5 0 0 1 2.428-.63c.882 0 1.709.23 2.428.63l.627-1.085a.31.31 0 0 1 .426-.115.31.31 0 0 1 .115.428ZM9.506 5.114a.625.625 0 1 0 1.25 0 .625.625 0 0 0-1.25 0Zm3.753 0a.625.625 0 1 0 1.25 0 .625.625 0 0 0-1.25 0ZM3.878 8.866a1.251 1.251 0 0 1 2.501 0v5.004a1.252 1.252 0 0 1-2.501 0V8.866Zm13.759 0a1.251 1.251 0 0 1 2.501 0v5.004a1.251 1.251 0 0 1-2.501 0V8.866ZM7.005 17.622h1.25v3.128a1.251 1.251 0 0 0 2.502 0v-3.128h2.502v3.128a1.251 1.251 0 0 0 2.501 0v-3.128h1.25V8.241H7.006v9.381Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 756 B

+1
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="M14.57 4.348c.65-.775 1.11-1.85 1.11-2.926 0-.149-.013-.3-.04-.422-1.057.04-2.314.708-3.072 1.592-.595.68-1.15 1.756-1.15 2.846 0 .164.027.327.04.38.066.012.175.027.283.027.948 0 2.14-.638 2.83-1.497Zm4.835 3.847.052-.035c-1.407-2.03-3.546-2.084-4.14-2.084-.911 0-1.726.325-2.411.598-.497.198-.925.368-1.271.368-.383 0-.822-.178-1.311-.377-.618-.25-1.317-.534-2.087-.534C5.64 6.13 3 8.296 3 12.379c0 2.545.975 5.227 2.182 6.954C6.224 20.803 7.13 22 8.43 22c.616 0 1.068-.193 1.543-.395.526-.225 1.082-.462 1.922-.462.849 0 1.356.223 1.845.437.455.2.895.393 1.58.393 1.42 0 2.353-1.292 3.246-2.586 1.003-1.47 1.422-2.913 1.435-2.98-.081-.027-2.802-1.13-2.802-4.246 0-2.51 1.855-3.734 2.207-3.966Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 838 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M4 12a8 8 0 1 1 16 0 8 8 0 0 1-16 0Zm8-10C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2Zm1 6a1 1 0 1 0-2 0v4a1 1 0 0 0 .293.707l2.5 2.5a1 1 0 0 0 1.414-1.414L13 11.586V8Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 332 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M3 4a1 1 0 0 1 1-1h13a1 1 0 0 1 .707.293l3 3A1 1 0 0 1 21 7v13a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm6 15h6v-5H9v5Zm8 0v-6a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v6H5V5h2v3a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V5.414l2 2V19h-2ZM15 5H9v2h6V5Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 361 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M6 4.667A2.667 2.667 0 0 1 8.667 2c1.34 0 2.538.608 3.333 1.564A4.32 4.32 0 0 1 15.333 2 2.667 2.667 0 0 1 18 4.667c0 .859-.25 1.66-.681 2.333H20a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-7V7h.667A2.333 2.333 0 0 0 16 4.667.667.667 0 0 0 15.333 4 2.333 2.333 0 0 0 13 6.333V7h-2v-.667A2.333 2.333 0 0 0 8.667 4 .667.667 0 0 0 8 4.667 2.333 2.333 0 0 0 10.333 7H11v4H4a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1h2.681A4.3 4.3 0 0 1 6 4.667ZM11 13H4v7a1 1 0 0 0 1 1h6v-8Zm9 0h-7v8h6a1 1 0 0 0 1-1v-7Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 617 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#1185FE" d="M6.335 4.212c2.293 1.76 4.76 5.327 5.665 7.241.906-1.914 3.372-5.482 5.665-7.241C19.319 2.942 22 1.96 22 5.086c0 .624-.35 5.244-.556 5.994-.713 2.608-3.315 3.273-5.629 2.87 4.045.704 5.074 3.035 2.852 5.366-4.22 4.426-6.066-1.111-6.54-2.53-.086-.26-.126-.382-.127-.278 0-.104-.041.018-.128.278-.473 1.419-2.318 6.956-6.539 2.53-2.222-2.331-1.193-4.662 2.852-5.366-2.314.403-4.916-.262-5.63-2.87C2.35 10.33 2 5.71 2 5.086c0-3.126 2.68-2.144 4.335-.874Z"/></svg>

After

Width:  |  Height:  |  Size: 557 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

+3 -1
View File
@@ -53,7 +53,8 @@ type OEmbedResponse struct {
Version string `json:"version"`
AuthorName string `json:"author_name,omitempty"`
AuthorURL string `json:"author_url,omitempty"`
ProviderName string `json:"provider_url,omitempty"`
ProviderName string `json:"provider_name,omitempty"`
ProviderURL string `json:"provider_url,omitempty"`
CacheAge int `json:"cache_age,omitempty"`
Width *int `json:"width"`
Height *int `json:"height"`
@@ -170,6 +171,7 @@ func (srv *Server) WebOEmbed(c echo.Context) error {
AuthorName: "@" + post.Author.Handle,
AuthorURL: fmt.Sprintf("https://bsky.app/profile/%s", post.Author.Handle),
ProviderName: "Bluesky Social",
ProviderURL: "https://bsky.app",
CacheAge: 86400,
Width: &width,
Height: nil,
+7 -2
View File
@@ -40,7 +40,6 @@
}
html {
background-color: white;
scrollbar-gutter: stable both-edges;
}
@media (prefers-color-scheme: dark) {
html {
@@ -76,9 +75,15 @@
top: 50%;
transform: translateX(-50%) translateY(-50%) translateY(-50px);
}
/* We need this style to prevent web dropdowns from shifting the display when opening */
/**
* We need these styles to prevent shifting due to scrollbar show/hide on
* OSs that have them enabled by default. This also handles cases where the
* screen wouldn't otherwise scroll, and therefore hide the scrollbar and
* shift the content, by forcing the page to show a scrollbar.
*/
body {
width: 100%;
overflow-y: scroll;
}
</style>
+3 -4
View File
@@ -18,9 +18,6 @@ jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter', () => {
}
})
// Silence the warning: Animated: `useNativeDriver` is not supported
jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper')
jest.mock('@fortawesome/react-native-fontawesome', () => ({
FontAwesomeIcon: '',
}))
@@ -51,7 +48,9 @@ jest.mock('expo-image-manipulator', () => ({
manipulateAsync: jest.fn().mockResolvedValue({
uri: 'file://resized-image',
}),
SaveFormat: jest.requireActual('expo-image-manipulator').SaveFormat,
SaveFormat: {
JPEG: 'jpeg',
},
}))
jest.mock('expo-camera', () => ({
+1 -1
View File
@@ -2,8 +2,8 @@
module.exports = {
locales: [
'en',
'ast',
'an',
'ast',
'ca',
'de',
'en-GB',
@@ -88,7 +88,7 @@ repositories {
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
def GLIDE_VERSION = "4.13.2"
def GLIDE_VERSION = "4.16.0"
implementation project(':expo-modules-core')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
@@ -5,6 +5,7 @@ import android.graphics.Color
import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
@@ -15,8 +16,8 @@ import expo.modules.kotlin.viewevent.EventDispatcher
import expo.modules.kotlin.views.ExpoView
class GifView(
context: Context,
appContext: AppContext,
context: Context,
appContext: AppContext,
) : ExpoView(context, appContext) {
// Events
private val onPlayerStateChange by EventDispatcher()
@@ -81,66 +82,65 @@ class GifView(
}
this.webpRequest =
glide
.load(source)
.diskCacheStrategy(DiskCacheStrategy.DATA)
.skipMemoryCache(false)
.listener(
object : RequestListener<Drawable> {
override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: Target<Drawable>?,
dataSource: com.bumptech.glide.load.DataSource?,
isFirstResource: Boolean,
): Boolean {
if (placeholderRequest != null) {
glide.clear(placeholderRequest)
}
return false
}
glide.load(source)
.diskCacheStrategy(DiskCacheStrategy.DATA)
.skipMemoryCache(false)
.listener(
object : RequestListener<Drawable> {
override fun onResourceReady(
resource: Drawable,
model: Any,
target: Target<Drawable>?,
dataSource: DataSource,
isFirstResource: Boolean
): Boolean {
placeholderRequest?.let { glide.clear(it) }
return false
}
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>?,
isFirstResource: Boolean,
): Boolean = true
},
).into(this.imageView)
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>,
isFirstResource: Boolean
): Boolean = true
}
)
.into(this.imageView)
if (this.imageView.drawable == null || this.imageView.drawable !is Animatable) {
this.placeholderRequest =
glide
.load(placeholderSource)
.diskCacheStrategy(DiskCacheStrategy.DATA)
// Let's not bloat the memory cache with placeholders
.skipMemoryCache(true)
.listener(
object : RequestListener<Drawable> {
override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: Target<Drawable>?,
dataSource: com.bumptech.glide.load.DataSource?,
isFirstResource: Boolean,
): Boolean {
// Incase this request finishes after the webp, let's just not set
// the drawable. This shouldn't happen because the request should get cancelled
if (imageView.drawable == null) {
imageView.setImageDrawable(resource)
}
return true
}
glide.load(placeholderSource)
.diskCacheStrategy(DiskCacheStrategy.DATA)
// Let's not bloat the memory cache with placeholders
.skipMemoryCache(true)
.listener(
object : RequestListener<Drawable> {
override fun onResourceReady(
resource: Drawable,
model: Any,
target: Target<Drawable>?,
dataSource: DataSource,
isFirstResource: Boolean
): Boolean {
// Incase this request finishes after the webp, let's just not set
// the drawable. This shouldn't happen because the request should
// get cancelled
if (imageView.drawable == null) {
imageView.setImageDrawable(resource)
}
return true
}
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>?,
isFirstResource: Boolean,
): Boolean = true
},
).submit()
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>,
isFirstResource: Boolean
): Boolean = true
},
)
.submit()
}
}
@@ -174,10 +174,10 @@ class GifView(
fun firePlayerStateChange() {
onPlayerStateChange(
mapOf(
"isPlaying" to this.isPlaying,
"isLoaded" to this.isLoaded,
),
mapOf(
"isPlaying" to this.isPlaying,
"isLoaded" to this.isLoaded,
),
)
}
+70 -68
View File
@@ -71,7 +71,7 @@
"@fortawesome/react-native-fontawesome": "^0.3.2",
"@haileyok/bluesky-video": "0.2.4",
"@ipld/dag-cbor": "^9.2.0",
"@lingui/react": "^4.5.0",
"@lingui/react": "^4.14.1",
"@mattermost/react-native-paste-input": "^0.7.1",
"@miblanchard/react-native-slider": "^2.3.1",
"@mozzius/expo-dynamic-app-icon": "^1.4.1",
@@ -80,9 +80,8 @@
"@radix-ui/react-focus-guards": "^1.1.1",
"@radix-ui/react-focus-scope": "^1.1.0",
"@react-native-async-storage/async-storage": "1.23.1",
"@react-native-masked-view/masked-view": "0.3.0",
"@react-native-menu/menu": "^1.1.0",
"@react-native-picker/picker": "2.6.1",
"@react-native-menu/menu": "^1.1.7",
"@react-native-picker/picker": "2.10.2",
"@react-navigation/bottom-tabs": "^6.5.20",
"@react-navigation/drawer": "^6.6.15",
"@react-navigation/native": "^6.1.17",
@@ -119,33 +118,33 @@
"emoji-mart": "^5.5.2",
"emoji-regex": "^10.4.0",
"eventemitter3": "^5.0.1",
"expo": "^51.0.8",
"expo-application": "^5.9.1",
"expo-blur": "^13.0.2",
"expo-build-properties": "^0.12.1",
"expo-camera": "~15.0.9",
"expo-clipboard": "^6.0.3",
"expo-dev-client": "^4.0.14",
"expo-device": "~6.0.2",
"expo-file-system": "^17.0.1",
"expo-font": "~12.0.10",
"expo-haptics": "^13.0.1",
"expo-image": "~1.12.9",
"expo-image-manipulator": "^12.0.5",
"expo-image-picker": "~15.0.5",
"expo-linear-gradient": "^13.0.2",
"expo-linking": "^6.3.1",
"expo-localization": "~15.0.3",
"expo-media-library": "~16.0.3",
"expo-navigation-bar": "~3.0.4",
"expo-notifications": "~0.28.7",
"expo-sharing": "^12.0.1",
"expo-splash-screen": "~0.27.4",
"expo-status-bar": "~1.12.1",
"expo-system-ui": "^3.0.4",
"expo-task-manager": "~11.8.1",
"expo-updates": "~0.25.14",
"expo-web-browser": "~13.0.3",
"expo": "~52.0.17",
"expo-application": "^6.0.1",
"expo-blur": "^14.0.1",
"expo-build-properties": "^0.13.1",
"expo-camera": "~16.0.7",
"expo-clipboard": "^7.0.0",
"expo-dev-client": "^5.0.4",
"expo-device": "~7.0.1",
"expo-file-system": "^18.0.4",
"expo-font": "~13.0.1",
"expo-haptics": "^14.0.0",
"expo-image": "~2.0.3",
"expo-image-manipulator": "^13.0.5",
"expo-image-picker": "~16.0.3",
"expo-linear-gradient": "^14.0.1",
"expo-linking": "^7.0.3",
"expo-localization": "~16.0.0",
"expo-media-library": "~17.0.3",
"expo-navigation-bar": "~4.0.4",
"expo-notifications": "~0.29.10",
"expo-sharing": "^13.0.0",
"expo-splash-screen": "~0.29.16",
"expo-status-bar": "~2.0.0",
"expo-system-ui": "^4.0.4",
"expo-task-manager": "~12.0.3",
"expo-updates": "~0.26.9",
"expo-web-browser": "~14.0.1",
"fast-text-encoding": "^1.0.6",
"history": "^5.3.0",
"hls.js": "^1.5.11",
@@ -168,32 +167,33 @@
"react-dom": "^18.2.0",
"react-image-crop": "^11.0.7",
"react-keyed-flatten-children": "^3.0.0",
"react-native": "0.74.1",
"react-native-compressor": "^1.8.24",
"react-native-date-picker": "^4.4.2",
"react-native-drawer-layout": "^4.0.1",
"react-native-gesture-handler": "2.20.0",
"react-native": "0.76.3",
"react-native-compressor": "1.10.3",
"react-native-date-picker": "^5.0.7",
"react-native-drawer-layout": "^4.0.4",
"react-native-gesture-handler": "2.20.2",
"react-native-get-random-values": "~1.11.0",
"react-native-image-crop-picker": "0.41.2",
"react-native-image-crop-picker": "^0.41.6",
"react-native-ios-context-menu": "^1.15.3",
"react-native-keyboard-controller": "^1.14.0",
"react-native-keyboard-controller": "^1.14.5",
"react-native-mmkv": "^2.12.2",
"react-native-pager-view": "6.2.3",
"react-native-picker-select": "^9.1.3",
"react-native-pager-view": "6.6.1",
"react-native-picker-select": "^9.3.1",
"react-native-progress": "bluesky-social/react-native-progress",
"react-native-qrcode-styled": "^0.3.1",
"react-native-reanimated": "^3.16.1",
"react-native-qrcode-styled": "^0.3.3",
"react-native-reanimated": "^3.16.3",
"react-native-root-siblings": "^4.1.1",
"react-native-safe-area-context": "4.10.1",
"react-native-screens": "~3.31.1",
"react-native-svg": "^15.3.0",
"react-native-uitextview": "^1.3.0",
"react-native-safe-area-context": "4.14.0",
"react-native-screens": "~4.3.0",
"react-native-svg": "^15.8.0",
"react-native-uitextview": "^1.4.0",
"react-native-url-polyfill": "^1.3.0",
"react-native-uuid": "^2.0.2",
"react-native-view-shot": "^3.8.0",
"react-native-web": "~0.19.11",
"react-native-view-shot": "^4.0.3",
"react-native-web": "~0.19.13",
"react-native-web-webview": "^1.0.2",
"react-native-webview": "13.10.2",
"react-native-webview": "13.12.3",
"react-remove-scroll-bar": "^2.3.6",
"react-responsive": "^9.0.2",
"react-textarea-autosize": "^8.5.3",
"rn-fetch-blob": "^0.12.0",
@@ -206,35 +206,35 @@
},
"devDependencies": {
"@atproto/dev-env": "^0.3.64",
"@babel/core": "^7.23.2",
"@babel/preset-env": "^7.20.0",
"@babel/runtime": "^7.20.0",
"@babel/core": "^7.26.0",
"@babel/preset-env": "^7.26.0",
"@babel/runtime": "^7.26.0",
"@did-plc/server": "^0.0.1",
"@expo/config-plugins": "8.0.4",
"@expo/prebuild-config": "6.7.0",
"@lingui/cli": "^4.5.0",
"@lingui/macro": "^4.5.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.11",
"@expo/config-plugins": "9.0.10",
"@expo/prebuild-config": "8.0.22",
"@lingui/cli": "^4.14.1",
"@lingui/macro": "^4.14.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
"@react-native/eslint-config": "^0.76.2",
"@react-native/typescript-config": "^0.74.1",
"@testing-library/jest-native": "^5.4.1",
"@testing-library/react-native": "^11.5.2",
"@react-native/typescript-config": "^0.76.1",
"@testing-library/jest-native": "^5.4.3",
"@testing-library/react-native": "^12.8.1",
"@types/jest": "^29.4.0",
"@types/lodash.chunk": "^4.2.7",
"@types/lodash.debounce": "^4.0.7",
"@types/lodash.isequal": "^4.5.6",
"@types/lodash.shuffle": "^4.2.7",
"@types/psl": "^1.1.1",
"@types/react-dom": "^18.2.18",
"@types/react-dom": "^18.3.1",
"@types/react-responsive": "^8.0.5",
"@types/react-test-renderer": "^17.0.1",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"babel-jest": "^29.7.0",
"babel-plugin-macros": "^3.1.0",
"babel-plugin-module-resolver": "^5.0.0",
"babel-plugin-module-resolver": "^5.0.2",
"babel-plugin-react-compiler": "19.0.0-beta-a7bf2bd-20241110",
"babel-preset-expo": "^10.0.0",
"babel-preset-expo": "^12.0.2",
"eslint": "^8.19.0",
"eslint-plugin-bsky-internal": "link:./eslint",
"eslint-plugin-ft-flow": "^2.0.3",
@@ -247,12 +247,12 @@
"husky": "^8.0.3",
"is-ci": "^3.0.1",
"jest": "^29.7.0",
"jest-expo": "^50.0.1",
"jest-junit": "^15.0.0",
"jest-expo": "^52.0.2",
"jest-junit": "^16.0.0",
"lint-staged": "^13.2.3",
"metro-react-native-babel-preset": "^0.74.1",
"metro-react-native-babel-preset": "^0.76.9",
"prettier": "^2.8.3",
"react-native-dotenv": "^3.3.1",
"react-native-dotenv": "^3.4.11",
"react-refresh": "^0.14.0",
"react-test-renderer": "18.2.0",
"svgo": "^3.3.2",
@@ -261,8 +261,10 @@
"webpack-bundle-analyzer": "^4.10.1"
},
"resolutions": {
"@expo/image-utils": "0.6.3",
"@react-native/babel-preset": "0.76.1",
"@react-native/normalize-colors": "0.76.1",
"@radix-ui/react-focus-scope": "1.1.0",
"@react-native/babel-preset": "0.74.1",
"@types/react": "^18",
"**/expo-constants": "16.0.1",
"**/expo-device": "6.0.2",
-18
View File
@@ -1,18 +0,0 @@
diff --git a/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/exporters/RawImageExporter.kt b/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/exporters/RawImageExporter.kt
index f339e5f..fa35e82 100644
--- a/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/exporters/RawImageExporter.kt
+++ b/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/exporters/RawImageExporter.kt
@@ -16,7 +16,12 @@ class RawImageExporter : ImageExporter {
copyFile(source, output, contentResolver)
val exifInterface = ExifInterface(output.absolutePath)
val imageRotation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, 0)
- val isRotatedLandscape = (imageRotation == ExifInterface.ORIENTATION_ROTATE_90 || imageRotation == ExifInterface.ORIENTATION_ROTATE_270)
+ val isRotatedLandscape = (
+ imageRotation == ExifInterface.ORIENTATION_ROTATE_90 ||
+ imageRotation == ExifInterface.ORIENTATION_ROTATE_270 ||
+ imageRotation == ExifInterface.ORIENTATION_TRANSPOSE ||
+ imageRotation == ExifInterface.ORIENTATION_TRANSVERSE
+ )
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeFile(output.absolutePath, options)
-5
View File
@@ -1,5 +0,0 @@
## expo-modules-core Patch
This patch fixes crashes seen in some Android clients when using intents to open the app. See https://github.com/expo/expo/pull/29513.
Do not remove this patch until that PR lands in Expo and is released.
-26
View File
@@ -1,26 +0,0 @@
diff --git a/node_modules/expo-modules-core/android/src/main/java/expo/modules/adapters/react/NativeModulesProxy.java b/node_modules/expo-modules-core/android/src/main/java/expo/modules/adapters/react/NativeModulesProxy.java
index bb74e80..0aa0202 100644
--- a/node_modules/expo-modules-core/android/src/main/java/expo/modules/adapters/react/NativeModulesProxy.java
+++ b/node_modules/expo-modules-core/android/src/main/java/expo/modules/adapters/react/NativeModulesProxy.java
@@ -90,8 +90,8 @@ public class NativeModulesProxy extends ReactContextBaseJavaModule {
mModuleRegistry.ensureIsInitialized();
KotlinInteropModuleRegistry kotlinModuleRegistry = getKotlinInteropModuleRegistry();
- kotlinModuleRegistry.emitOnCreate();
kotlinModuleRegistry.installJSIInterop();
+ kotlinModuleRegistry.emitOnCreate();
Map<String, Object> constants = new HashMap<>(3);
constants.put(MODULES_CONSTANTS_KEY, new HashMap<>());
diff --git a/node_modules/expo-modules-core/build/uuid/uuid.js b/node_modules/expo-modules-core/build/uuid/uuid.js
index 109d3fe..c7fce9e 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() {
-216
View File
@@ -1,216 +0,0 @@
diff --git a/node_modules/expo-notifications/android/build.gradle b/node_modules/expo-notifications/android/build.gradle
index b863077..8b5209e 100644
--- a/node_modules/expo-notifications/android/build.gradle
+++ b/node_modules/expo-notifications/android/build.gradle
@@ -32,6 +32,7 @@ dependencies {
api 'com.google.firebase:firebase-messaging:22.0.0'
api 'me.leolin:ShortcutBadger:1.1.22@aar'
+ implementation project(':expo-background-notification-handler')
if (project.findProject(':expo-modules-test-core')) {
testImplementation project(':expo-modules-test-core')
diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/badge/BadgeHelper.kt b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/badge/BadgeHelper.kt
index 63a46c5..f911834 100644
--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/badge/BadgeHelper.kt
+++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/badge/BadgeHelper.kt
@@ -1,5 +1,6 @@
package expo.modules.notifications.badge
+import android.app.NotificationManager
import android.content.Context
import android.util.Log
import me.leolin.shortcutbadger.ShortcutBadgeException
@@ -12,7 +13,12 @@ object BadgeHelper {
fun setBadgeCount(context: Context, badgeCount: Int): Boolean {
return try {
- ShortcutBadger.applyCountOrThrow(context.applicationContext, badgeCount)
+ if (badgeCount == 0) {
+ val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.cancelAll()
+ } else {
+ ShortcutBadger.applyCountOrThrow(context.applicationContext, badgeCount)
+ }
BadgeHelper.badgeCount = badgeCount
true
} catch (e: ShortcutBadgeException) {
diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/JSONNotificationContentBuilder.java b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/JSONNotificationContentBuilder.java
index 0af7fe0..8f2c8d8 100644
--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/JSONNotificationContentBuilder.java
+++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/JSONNotificationContentBuilder.java
@@ -14,6 +14,7 @@ import expo.modules.notifications.notifications.enums.NotificationPriority;
import expo.modules.notifications.notifications.model.NotificationContent;
public class JSONNotificationContentBuilder extends NotificationContent.Builder {
+ private static final String CHANNEL_ID_KEY = "channelId";
private static final String TITLE_KEY = "title";
private static final String TEXT_KEY = "message";
private static final String SUBTITLE_KEY = "subtitle";
@@ -36,6 +37,7 @@ public class JSONNotificationContentBuilder extends NotificationContent.Builder
public NotificationContent.Builder setPayload(JSONObject payload) {
this.setTitle(getTitle(payload))
+ .setChannelId(getChannelId(payload))
.setSubtitle(getSubtitle(payload))
.setText(getText(payload))
.setBody(getBody(payload))
@@ -60,6 +62,14 @@ public class JSONNotificationContentBuilder extends NotificationContent.Builder
return this;
}
+ protected String getChannelId(JSONObject payload) {
+ try {
+ return payload.getString(CHANNEL_ID_KEY);
+ } catch (JSONException e) {
+ return null;
+ }
+ }
+
protected String getTitle(JSONObject payload) {
try {
return payload.getString(TITLE_KEY);
diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java
index f1fed19..012757b 100644
--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java
+++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java
@@ -20,6 +20,7 @@ import expo.modules.notifications.notifications.enums.NotificationPriority;
* should be created using {@link NotificationContent.Builder}.
*/
public class NotificationContent implements Parcelable, Serializable {
+ private String mChannelId;
private String mTitle;
private String mText;
private String mSubtitle;
@@ -50,6 +51,11 @@ public class NotificationContent implements Parcelable, Serializable {
}
};
+ @Nullable
+ public String getChannelId() {
+ return mChannelId;
+ }
+
@Nullable
public String getTitle() {
return mTitle;
@@ -121,6 +127,7 @@ public class NotificationContent implements Parcelable, Serializable {
}
protected NotificationContent(Parcel in) {
+ mChannelId = in.readString();
mTitle = in.readString();
mText = in.readString();
mSubtitle = in.readString();
@@ -146,6 +153,7 @@ public class NotificationContent implements Parcelable, Serializable {
@Override
public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mChannelId);
dest.writeString(mTitle);
dest.writeString(mText);
dest.writeString(mSubtitle);
@@ -166,6 +174,7 @@ public class NotificationContent implements Parcelable, Serializable {
private static final long serialVersionUID = 397666843266836802L;
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
+ out.writeObject(mChannelId);
out.writeObject(mTitle);
out.writeObject(mText);
out.writeObject(mSubtitle);
@@ -190,6 +199,7 @@ public class NotificationContent implements Parcelable, Serializable {
}
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
+ mChannelId = (String) in.readObject();
mTitle = (String) in.readObject();
mText = (String) in.readObject();
mSubtitle = (String) in.readObject();
@@ -240,6 +250,7 @@ public class NotificationContent implements Parcelable, Serializable {
}
public static class Builder {
+ private String mChannelId;
private String mTitle;
private String mText;
private String mSubtitle;
@@ -260,6 +271,11 @@ public class NotificationContent implements Parcelable, Serializable {
useDefaultVibrationPattern();
}
+ public Builder setChannelId(String channelId) {
+ mChannelId = channelId;
+ return this;
+ }
+
public Builder setTitle(String title) {
mTitle = title;
return this;
@@ -336,6 +352,7 @@ public class NotificationContent implements Parcelable, Serializable {
public NotificationContent build() {
NotificationContent content = new NotificationContent();
+ content.mChannelId = mChannelId;
content.mTitle = mTitle;
content.mSubtitle = mSubtitle;
content.mText = mText;
diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.java b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.java
index 6bd9928..ee93d70 100644
--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.java
+++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.java
@@ -48,6 +48,10 @@ public class ExpoNotificationBuilder extends ChannelAwareNotificationBuilder {
NotificationContent content = getNotificationContent();
+ if (content.getChannelId() != null) {
+ builder.setChannelId(content.getChannelId());
+ }
+
builder.setAutoCancel(content.isAutoDismiss());
builder.setOngoing(content.isSticky());
diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt
index 55b3a8d..1b99d5b 100644
--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt
+++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt
@@ -12,11 +12,14 @@ import expo.modules.notifications.notifications.model.triggers.FirebaseNotificat
import expo.modules.notifications.service.NotificationsService
import expo.modules.notifications.service.interfaces.FirebaseMessagingDelegate
import expo.modules.notifications.tokens.interfaces.FirebaseTokenListener
+import expo.modules.backgroundnotificationhandler.BackgroundNotificationHandler
+import expo.modules.backgroundnotificationhandler.BackgroundNotificationHandlerInterface
+import expo.modules.backgroundnotificationhandler.ExpoBackgroundNotificationHandlerModule
import org.json.JSONObject
import java.lang.ref.WeakReference
import java.util.*
-open class FirebaseMessagingDelegate(protected val context: Context) : FirebaseMessagingDelegate {
+open class FirebaseMessagingDelegate(protected val context: Context) : FirebaseMessagingDelegate, BackgroundNotificationHandlerInterface {
companion object {
// Unfortunately we cannot save state between instances of a service other way
// than by static properties. Fortunately, using weak references we can
@@ -89,12 +92,21 @@ open class FirebaseMessagingDelegate(protected val context: Context) : FirebaseM
fun getBackgroundTasks() = sBackgroundTaskConsumerReferences.values.mapNotNull { it.get() }
override fun onMessageReceived(remoteMessage: RemoteMessage) {
- NotificationsService.receive(context, createNotification(remoteMessage))
- getBackgroundTasks().forEach {
- it.scheduleJob(RemoteMessageSerializer.toBundle(remoteMessage))
+ if (!ExpoBackgroundNotificationHandlerModule.isForegrounded) {
+ BackgroundNotificationHandler(context, this).handleMessage(remoteMessage)
+ return
+ } else {
+ showMessage(remoteMessage)
+ getBackgroundTasks().forEach {
+ it.scheduleJob(RemoteMessageSerializer.toBundle(remoteMessage))
+ }
}
}
+ override fun showMessage(remoteMessage: RemoteMessage) {
+ NotificationsService.receive(context, createNotification(remoteMessage))
+ }
+
protected fun createNotification(remoteMessage: RemoteMessage): Notification {
val identifier = getNotificationIdentifier(remoteMessage)
val payload = JSONObject(remoteMessage.data as Map<*, *>)
+169
View File
@@ -0,0 +1,169 @@
diff --git a/node_modules/expo-notifications/android/build.gradle b/node_modules/expo-notifications/android/build.gradle
index 5dd0c61..bf536dd 100644
--- a/node_modules/expo-notifications/android/build.gradle
+++ b/node_modules/expo-notifications/android/build.gradle
@@ -46,6 +46,7 @@ dependencies {
implementation 'com.google.firebase:firebase-messaging:24.0.1'
implementation 'me.leolin:ShortcutBadger:1.1.22@aar'
+ implementation project(':expo-background-notification-handler')
if (project.findProject(':expo-modules-test-core')) {
testImplementation project(':expo-modules-test-core')
diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/interfaces/INotificationContent.kt b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/interfaces/INotificationContent.kt
index 7b99e6c..45a450d 100644
--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/interfaces/INotificationContent.kt
+++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/interfaces/INotificationContent.kt
@@ -15,6 +15,7 @@ import org.json.JSONObject
* This interface exists to provide a common API for both classes.
* */
interface INotificationContent : Parcelable {
+ val channelId: String?
val title: String?
val text: String?
val subText: String?
diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java
index 191b64e..fe8b3c5 100644
--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java
+++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java
@@ -35,6 +35,7 @@ import kotlin.coroutines.Continuation;
* Refactoring this class may require a migration strategy for the data stored in SharedPreferences.
*/
public class NotificationContent implements Parcelable, Serializable, INotificationContent {
+ private String mChannelId;
private String mTitle;
private String mText;
private String mSubtitle;
@@ -65,6 +66,11 @@ public class NotificationContent implements Parcelable, Serializable, INotificat
}
};
+ @Nullable
+ public String getChannelId() {
+ return mChannelId;
+ }
+
@Nullable
public String getTitle() {
return mTitle;
@@ -158,6 +164,7 @@ public class NotificationContent implements Parcelable, Serializable, INotificat
}
protected NotificationContent(Parcel in) {
+ mChannelId = in.readString();
mTitle = in.readString();
mText = in.readString();
mSubtitle = in.readString();
@@ -183,6 +190,7 @@ public class NotificationContent implements Parcelable, Serializable, INotificat
@Override
public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mChannelId);
dest.writeString(mTitle);
dest.writeString(mText);
dest.writeString(mSubtitle);
@@ -203,6 +211,7 @@ public class NotificationContent implements Parcelable, Serializable, INotificat
private static final long serialVersionUID = 397666843266836802L;
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
+ out.writeObject(mChannelId);
out.writeObject(mTitle);
out.writeObject(mText);
out.writeObject(mSubtitle);
@@ -285,6 +294,11 @@ public class NotificationContent implements Parcelable, Serializable, INotificat
useDefaultVibrationPattern();
}
+ public Builder setChannelId(String channelId) {
+ content.mChannelId = channelId;
+ return this;
+ }
+
public Builder setTitle(String title) {
content.mTitle = title;
return this;
diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationData.kt b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationData.kt
index 39b5aad..e50797d 100644
--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationData.kt
+++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationData.kt
@@ -11,6 +11,9 @@ import org.json.JSONObject
* */
@JvmInline
value class NotificationData(private val data: Map<String, String>) {
+ val channelId: String?
+ get() = data["channelId"]
+
val title: String?
get() = data["title"]
diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/RemoteNotificationContent.kt b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/RemoteNotificationContent.kt
index d2cc6cf..6a48ff2 100644
--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/RemoteNotificationContent.kt
+++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/RemoteNotificationContent.kt
@@ -31,6 +31,8 @@ class RemoteNotificationContent(private val remoteMessage: RemoteMessage) : INot
return remoteMessage.notification?.imageUrl != null
}
+ override val channelId = remoteMessage.notification?.channelId ?: notificationData.channelId
+
override val title = remoteMessage.notification?.title ?: notificationData.title
override val text = remoteMessage.notification?.body ?: notificationData.message
diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.kt b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.kt
index 8ca6ec5..57c3599 100644
--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.kt
+++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.kt
@@ -101,6 +101,9 @@ open class ExpoNotificationBuilder(
builder.setOngoing(content.isSticky)
// see "Notification anatomy" https://developer.android.com/develop/ui/views/notifications#Templates
+ content.channelId?.let {
+ builder.setChannelId(it)
+ }
builder.setContentTitle(content.title)
builder.setContentText(content.text)
builder.setSubText(content.subText)
diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt
index 9f22441..5f92f80 100644
--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt
+++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt
@@ -2,6 +2,9 @@ package expo.modules.notifications.service.delegates
import android.content.Context
import com.google.firebase.messaging.RemoteMessage
+import expo.modules.backgroundnotificationhandler.BackgroundNotificationHandler
+import expo.modules.backgroundnotificationhandler.BackgroundNotificationHandlerInterface
+import expo.modules.backgroundnotificationhandler.ExpoBackgroundNotificationHandlerModule
import expo.modules.interfaces.taskManager.TaskServiceProviderHelper
import expo.modules.notifications.notifications.RemoteMessageSerializer
import expo.modules.notifications.notifications.background.BackgroundRemoteNotificationTaskConsumer
@@ -17,7 +20,8 @@ import expo.modules.notifications.tokens.interfaces.FirebaseTokenListener
import java.lang.ref.WeakReference
import java.util.*
-open class FirebaseMessagingDelegate(protected val context: Context) : FirebaseMessagingDelegate {
+open class FirebaseMessagingDelegate(protected val context: Context) : FirebaseMessagingDelegate,
+ BackgroundNotificationHandlerInterface {
companion object {
// Unfortunately we cannot save state between instances of a service other way
// than by static properties. Fortunately, using weak references we can
@@ -94,8 +98,17 @@ open class FirebaseMessagingDelegate(protected val context: Context) : FirebaseM
DebugLogging.logRemoteMessage("FirebaseMessagingDelegate.onMessageReceived: message", remoteMessage)
val notification = createNotification(remoteMessage)
DebugLogging.logNotification("FirebaseMessagingDelegate.onMessageReceived: notification", notification)
- NotificationsService.receive(context, notification)
- runTaskManagerTasks(remoteMessage)
+
+ if (!ExpoBackgroundNotificationHandlerModule.isForegrounded) {
+ BackgroundNotificationHandler(context, this).handleMessage(remoteMessage)
+ } else {
+ showMessage(remoteMessage)
+ runTaskManagerTasks(remoteMessage)
+ }
+ }
+
+ override fun showMessage(remoteMessage: RemoteMessage) {
+ NotificationsService.receive(context, createNotification(remoteMessage))
}
private fun runTaskManagerTasks(remoteMessage: RemoteMessage) {
-44
View File
@@ -1,44 +0,0 @@
diff --git a/node_modules/metro/src/ModuleGraph/worker/JsFileWrapping.js b/node_modules/metro/src/ModuleGraph/worker/JsFileWrapping.js
index 48a1409..ef185c9 100644
--- a/node_modules/metro/src/ModuleGraph/worker/JsFileWrapping.js
+++ b/node_modules/metro/src/ModuleGraph/worker/JsFileWrapping.js
@@ -70,14 +70,19 @@ function wrapModule(
importDefaultName,
importAllName,
dependencyMapName,
- globalPrefix
+ globalPrefix,
+ moduleFactoryName
) {
const params = buildParameters(
importDefaultName,
importAllName,
dependencyMapName
);
- const factory = functionFromProgram(fileAst.program, params);
+ const factory = functionFromProgram(
+ fileAst.program,
+ params,
+ moduleFactoryName
+ );
const def = t.callExpression(t.identifier(`${globalPrefix}__d`), [factory]);
const ast = t.file(t.program([t.expressionStatement(def)]));
const requireName = renameRequires(ast);
@@ -107,7 +112,16 @@ function wrapJson(source, globalPrefix) {
"});",
].join("\n");
}
-function functionFromProgram(program, parameters) {
+const JS_INVALID_IDENT_RE = /[^a-zA-Z0-9$_]/g;
+function functionFromProgram(program, parameters, moduleFactoryName) {
+ let identifier;
+ if (typeof moduleFactoryName === "string" && moduleFactoryName !== "") {
+ // Keep the name readable so it shows up in profiler traces.
+ // Add an unlikely suffix to avoid collisions with the module code.
+ identifier = t.identifier(
+ `${moduleFactoryName.replace(JS_INVALID_IDENT_RE, "_")}__module_factory__`
+ );
+ }
return t.functionExpression(
undefined,
parameters.map(makeIdentifier),
-50
View File
@@ -1,50 +0,0 @@
diff --git a/node_modules/metro-runtime/src/polyfills/require.js b/node_modules/metro-runtime/src/polyfills/require.js
index ce67cb4..eeeae84 100644
--- a/node_modules/metro-runtime/src/polyfills/require.js
+++ b/node_modules/metro-runtime/src/polyfills/require.js
@@ -22,6 +22,13 @@ global.__c = clear;
global.__registerSegment = registerSegment;
var modules = clear();
+if (__DEV__) {
+ // Added by Dan for module init logging.
+ global.__INIT_LOGS__ = []
+ var initModuleCounter = 0
+ var initModuleStack = []
+}
+
// Don't use a Symbol here, it would pull in an extra polyfill with all sorts of
// additional stuff (e.g. Array.from).
const EMPTY = {};
@@ -303,7 +310,30 @@ function loadModuleImplementation(moduleId, module) {
throw module.error;
}
if (__DEV__) {
- var Systrace = requireSystrace();
+ // Added by Dan for module init logging.
+ var Systrace = {
+ beginEvent(label) {
+ let fullLabel = initModuleCounter++ + ' ' + label
+ global.__INIT_LOGS__.push(
+ ' '.repeat(initModuleStack.length) +
+ ' ENTER ' + fullLabel
+ )
+ initModuleStack.push({
+ fullLabel,
+ startTime: nativePerformanceNow(),
+ })
+ },
+ endEvent() {
+ const res = initModuleStack.pop()
+ const fullLabel = res.fullLabel
+ const startTime = res.startTime
+ const timeElapsed = Math.round(nativePerformanceNow() - startTime)
+ global.__INIT_LOGS__.push(
+ ' '.repeat(initModuleStack.length) +
+ ' LEAVE ' + fullLabel + ' [' + timeElapsed + 'ms]',
+ )
+ }
+ };
var Refresh = requireRefresh();
}
@@ -1,41 +0,0 @@
diff --git a/node_modules/metro-transform-worker/src/index.js b/node_modules/metro-transform-worker/src/index.js
index 9f2e3d2..5222c8e 100644
--- a/node_modules/metro-transform-worker/src/index.js
+++ b/node_modules/metro-transform-worker/src/index.js
@@ -189,6 +189,10 @@ async function transformJS(file, { config, options, projectRoot }) {
let dependencyMapName = "";
let dependencies;
let wrappedAst;
+ const minify =
+ options.minify &&
+ options.unstable_transformProfile !== "hermes-canary" &&
+ options.unstable_transformProfile !== "hermes-stable";
// If the module to transform is a script (meaning that is not part of the
// dependency graph and it code will just be prepended to the bundle modules),
@@ -228,19 +232,20 @@ async function transformJS(file, { config, options, projectRoot }) {
if (config.unstable_disableModuleWrapping === true) {
wrappedAst = ast;
} else {
+ let moduleFactoryName;
+ if (options.dev && !minify) {
+ moduleFactoryName = file.filename;
+ }
({ ast: wrappedAst } = JsFileWrapping.wrapModule(
ast,
importDefault,
importAll,
dependencyMapName,
- config.globalPrefix
+ config.globalPrefix,
+ moduleFactoryName
));
}
}
- const minify =
- options.minify &&
- options.unstable_transformProfile !== "hermes-canary" &&
- options.unstable_transformProfile !== "hermes-stable";
const reserved = [];
if (config.unstable_dependencyMapReservedName != null) {
reserved.push(config.unstable_dependencyMapReservedName);
-283
View File
@@ -1,283 +0,0 @@
diff --git a/node_modules/react-native/Libraries/Blob/RCTFileReaderModule.mm b/node_modules/react-native/Libraries/Blob/RCTFileReaderModule.mm
index caa5540..c5d4e67 100644
--- a/node_modules/react-native/Libraries/Blob/RCTFileReaderModule.mm
+++ b/node_modules/react-native/Libraries/Blob/RCTFileReaderModule.mm
@@ -73,7 +73,7 @@ @implementation RCTFileReaderModule
} else {
NSString *type = [RCTConvert NSString:blob[@"type"]];
NSString *text = [NSString stringWithFormat:@"data:%@;base64,%@",
- type != nil && [type length] > 0 ? type : @"application/octet-stream",
+ ![type isEqual:[NSNull null]] && [type length] > 0 ? type : @"application/octet-stream",
[data base64EncodedStringWithOptions:0]];
resolve(text);
diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm
index b0d71dc..41b9a0e 100644
--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm
+++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm
@@ -377,10 +377,6 @@ - (void)textInputDidBeginEditing
self.backedTextInputView.attributedText = [NSAttributedString new];
}
- if (_selectTextOnFocus) {
- [self.backedTextInputView selectAll:nil];
- }
-
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeFocus
reactTag:self.reactTag
text:[self.backedTextInputView.attributedText.string copy]
@@ -611,6 +607,10 @@ - (UIView *)reactAccessibilityElement
- (void)reactFocus
{
[self.backedTextInputView reactFocus];
+
+ if (_selectTextOnFocus) {
+ [self.backedTextInputView selectAll:nil];
+ }
}
- (void)reactBlur
diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h
index e9b330f..ec5f58c 100644
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h
+++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h
@@ -15,5 +15,8 @@
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) RCTDirectEventBlock onRefresh;
@property (nonatomic, weak) UIScrollView *scrollView;
+@property (nonatomic, copy) UIColor *customTintColor;
+
+- (void)forwarderBeginRefreshing;
@end
diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
index b09e653..d2b4e05 100644
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
+++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
@@ -22,6 +22,7 @@ @implementation RCTRefreshControl {
NSString *_title;
UIColor *_titleColor;
CGFloat _progressViewOffset;
+ UIColor *_customTintColor;
}
- (instancetype)init
@@ -56,6 +57,12 @@ - (void)layoutSubviews
_isInitialRender = false;
}
+- (void)didMoveToSuperview
+{
+ [super didMoveToSuperview];
+ [self setTintColor:_customTintColor];
+}
+
- (void)beginRefreshingProgrammatically
{
UInt64 beginRefreshingTimestamp = _currentRefreshingStateTimestamp;
@@ -203,4 +210,58 @@ - (void)refreshControlValueChanged
}
}
+- (void)setCustomTintColor:(UIColor *)customTintColor
+{
+ _customTintColor = customTintColor;
+ [self setTintColor:customTintColor];
+}
+
+// Fix for https://github.com/facebook/react-native/issues/43388
+// A bug in iOS 17.4 causes the haptic to not play when refreshing if the tintColor
+// is set before the refresh control gets added to the scrollview. We'll call this
+// function whenever the superview changes. We'll also call it if the value of customTintColor
+// changes.
+- (void)setTintColor:(UIColor *)tintColor
+{
+ if ([self.superview isKindOfClass:[UIScrollView class]] && self.tintColor != tintColor) {
+ [super setTintColor:tintColor];
+ }
+}
+
+/*
+ This method is used by Bluesky's ExpoScrollForwarder. This allows other React Native
+ libraries to perform a refresh of a scrollview and access the refresh control's onRefresh
+ function.
+ */
+- (void)forwarderBeginRefreshing
+{
+ _refreshingProgrammatically = NO;
+
+ [self sizeToFit];
+
+ if (!self.scrollView) {
+ return;
+ }
+
+ UIScrollView *scrollView = (UIScrollView *)self.scrollView;
+
+ [UIView animateWithDuration:0.3
+ delay:0
+ options:UIViewAnimationOptionBeginFromCurrentState
+ animations:^(void) {
+ // Whenever we call this method, the scrollview will always be at a position of
+ // -130 or less. Scrolling back to -65 simulates the default behavior of RCTRefreshControl
+ [scrollView setContentOffset:CGPointMake(0, -65)];
+ }
+ completion:^(__unused BOOL finished) {
+ [super beginRefreshing];
+ [self setCurrentRefreshingState:super.refreshing];
+
+ if (self->_onRefresh) {
+ self->_onRefresh(nil);
+ }
+ }
+ ];
+}
+
@end
diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m
index 40aaf9c..1c60164 100644
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m
+++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m
@@ -22,11 +22,12 @@ - (UIView *)view
RCT_EXPORT_VIEW_PROPERTY(onRefresh, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(refreshing, BOOL)
-RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(title, NSString)
RCT_EXPORT_VIEW_PROPERTY(titleColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(progressViewOffset, CGFloat)
+RCT_REMAP_VIEW_PROPERTY(tintColor, customTintColor, UIColor)
+
RCT_EXPORT_METHOD(setNativeRefreshing : (nonnull NSNumber *)viewTag toRefreshing : (BOOL)refreshing)
{
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
diff --git a/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m b/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m
index 1aead51..39e6244 100644
--- a/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m
+++ b/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m
@@ -159,31 +159,6 @@ - (BOOL)touchesShouldCancelInContentView:(__unused UIView *)view
return !shouldDisableScrollInteraction;
}
-/*
- * Automatically centers the content such that if the content is smaller than the
- * ScrollView, we force it to be centered, but when you zoom or the content otherwise
- * becomes larger than the ScrollView, there is no padding around the content but it
- * can still fill the whole view.
- */
-- (void)setContentOffset:(CGPoint)contentOffset
-{
- UIView *contentView = [self contentView];
- if (contentView && _centerContent && !CGSizeEqualToSize(contentView.frame.size, CGSizeZero)) {
- CGSize subviewSize = contentView.frame.size;
- CGSize scrollViewSize = self.bounds.size;
- if (subviewSize.width <= scrollViewSize.width) {
- contentOffset.x = -(scrollViewSize.width - subviewSize.width) / 2.0;
- }
- if (subviewSize.height <= scrollViewSize.height) {
- contentOffset.y = -(scrollViewSize.height - subviewSize.height) / 2.0;
- }
- }
-
- super.contentOffset = CGPointMake(
- RCTSanitizeNaNValue(contentOffset.x, @"scrollView.contentOffset.x"),
- RCTSanitizeNaNValue(contentOffset.y, @"scrollView.contentOffset.y"));
-}
-
- (void)setFrame:(CGRect)frame
{
// Preserving and revalidating `contentOffset`.
@@ -427,6 +402,11 @@ - (void)setRemoveClippedSubviews:(__unused BOOL)removeClippedSubviews
// Does nothing
}
+- (void)setFrame:(CGRect)frame {
+ [super setFrame:frame];
+ [self centerContentIfNeeded];
+}
+
- (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex
{
[super insertReactSubview:view atIndex:atIndex];
@@ -444,6 +424,8 @@ - (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex
RCTApplyTransformationAccordingLayoutDirection(_contentView, self.reactLayoutDirection);
[_scrollView addSubview:view];
}
+
+ [self centerContentIfNeeded];
}
- (void)removeReactSubview:(UIView *)subview
@@ -652,9 +634,46 @@ -(void)delegateMethod : (UIScrollView *)scrollView \
}
RCT_SCROLL_EVENT_HANDLER(scrollViewWillBeginDecelerating, onMomentumScrollBegin)
-RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, onScroll)
RCT_SCROLL_EVENT_HANDLER(scrollViewDidScrollToTop, onScrollToTop)
+-(void)scrollViewDidZoom : (UIScrollView *)scrollView
+{
+ [self centerContentIfNeeded];
+
+ RCT_SEND_SCROLL_EVENT(onScroll, nil);
+ RCT_FORWARD_SCROLL_EVENT(scrollViewDidZoom : scrollView);
+}
+
+/*
+ * Automatically centers the content such that if the content is smaller than the
+ * ScrollView, we force it to be centered, but when you zoom or the content otherwise
+ * becomes larger than the ScrollView, there is no padding around the content but it
+ * can still fill the whole view.
+ *
+ * PATCHED: This deviates from the original React Native implementation to fix two issues:
+ *
+ * - The scroll view was swallowing any taps immediately after pinching
+ * - The scroll view was jittering when crossing the full screen threshold while pinching
+ *
+ * This implementation is based on https://petersteinberger.com/blog/2013/how-to-center-uiscrollview/.
+ */
+-(void)centerContentIfNeeded
+{
+ if (_scrollView.centerContent &&
+ !CGSizeEqualToSize(self.contentSize, CGSizeZero) &&
+ !CGSizeEqualToSize(self.bounds.size, CGSizeZero)
+ ) {
+ CGFloat top = 0, left = 0;
+ if (self.contentSize.width < self.bounds.size.width) {
+ left = (self.bounds.size.width - self.contentSize.width) * 0.5f;
+ }
+ if (self.contentSize.height < self.bounds.size.height) {
+ top = (self.bounds.size.height - self.contentSize.height) * 0.5f;
+ }
+ _scrollView.contentInset = UIEdgeInsetsMake(top, left, top, left);
+ }
+}
+
- (void)addScrollListener:(NSObject<UIScrollViewDelegate> *)scrollListener
{
[_scrollListeners addObject:scrollListener];
@@ -913,6 +932,7 @@ - (void)updateContentSizeIfNeeded
CGSize contentSize = self.contentSize;
if (!CGSizeEqualToSize(_scrollView.contentSize, contentSize)) {
_scrollView.contentSize = contentSize;
+ [self centerContentIfNeeded];
}
}
diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java
index 5f5e1ab..aac00b6 100644
--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java
+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java
@@ -99,8 +99,9 @@ public class JavaTimerManager {
}
// If the JS thread is busy for multiple frames we cancel any other pending runnable.
- if (mCurrentIdleCallbackRunnable != null) {
- mCurrentIdleCallbackRunnable.cancel();
+ IdleCallbackRunnable currentRunnable = mCurrentIdleCallbackRunnable;
+ if (currentRunnable != null) {
+ currentRunnable.cancel();
}
mCurrentIdleCallbackRunnable = new IdleCallbackRunnable(frameTimeNanos);
+331
View File
@@ -0,0 +1,331 @@
diff --git a/node_modules/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts b/node_modules/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts
index 62f52a7..ca30165 100644
--- a/node_modules/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts
+++ b/node_modules/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts
@@ -426,9 +426,10 @@ export interface ViewStyle extends FlexStyle, ShadowStyleIOS, TransformsStyle {
*/
pointerEvents?: 'box-none' | 'none' | 'box-only' | 'auto' | undefined;
isolation?: 'auto' | 'isolate' | undefined;
- cursor?: CursorValue | undefined;
+ cursor?: CursorValue | string | undefined;
boxShadow?: ReadonlyArray<BoxShadowValue> | string | undefined;
filter?: ReadonlyArray<FilterFunction> | string | undefined;
+ transformOrigin?: string | (string | number)[] | undefined;
}
export type FontVariant =
@@ -536,7 +537,11 @@ export interface TextStyle extends TextStyleIOS, TextStyleAndroid, ViewStyle {
textShadowOffset?: {width: number; height: number} | undefined;
textShadowRadius?: number | undefined;
textTransform?: 'none' | 'capitalize' | 'uppercase' | 'lowercase' | undefined;
- userSelect?: 'auto' | 'none' | 'text' | 'contain' | 'all' | undefined;
+ userSelect?: 'auto' | 'none' | 'text' | 'contain' | 'all' | string | undefined;
+ cursor?: CursorValue | string | undefined;
+ boxShadow?: ReadonlyArray<BoxShadowValue> | string | undefined;
+ filter?: ReadonlyArray<FilterFunction> | string | undefined;
+ transformOrigin?: string | (string | number)[] | undefined;
}
/**
@@ -558,5 +563,8 @@ export interface ImageStyle extends FlexStyle, ShadowStyleIOS, TransformsStyle {
tintColor?: ColorValue | undefined;
opacity?: AnimatableNumericValue | undefined;
objectFit?: 'cover' | 'contain' | 'fill' | 'scale-down' | undefined;
- cursor?: CursorValue | undefined;
+ cursor?: CursorValue | string | undefined;
+ boxShadow?: ReadonlyArray<BoxShadowValue> | string | undefined;
+ filter?: ReadonlyArray<FilterFunction> | string | undefined;
+ transformOrigin?: string | (string | number)[] | undefined;
}
diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm
index 93af874..106f8ec 100644
--- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm
+++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm
@@ -66,28 +66,51 @@ - (void)preserveContentOffsetWithBlock:(void (^)())block
* ScrollView, we force it to be centered, but when you zoom or the content otherwise
* becomes larger than the ScrollView, there is no padding around the content but it
* can still fill the whole view.
+ * This implementation is based on https://petersteinberger.com/blog/2013/how-to-center-uiscrollview/.
*/
-- (void)setContentOffset:(CGPoint)contentOffset
+-(void)centerContentIfNeeded
{
- if (_isSetContentOffsetDisabled) {
+ if (!_centerContent) {
return;
}
- if (_centerContent && !CGSizeEqualToSize(self.contentSize, CGSizeZero)) {
- CGSize scrollViewSize = self.bounds.size;
- if (self.contentSize.width <= scrollViewSize.width) {
- contentOffset.x = -(scrollViewSize.width - self.contentSize.width) / 2.0;
- }
- if (self.contentSize.height <= scrollViewSize.height) {
- contentOffset.y = -(scrollViewSize.height - self.contentSize.height) / 2.0;
- }
+ CGSize contentSize = self.contentSize;
+ CGSize boundsSize = self.bounds.size;
+ if (CGSizeEqualToSize(contentSize, CGSizeZero) ||
+ CGSizeEqualToSize(boundsSize, CGSizeZero)) {
+ return;
}
+ CGFloat top = 0, left = 0;
+ if (contentSize.width < boundsSize.width) {
+ left = (boundsSize.width - contentSize.width) * 0.5f;
+ }
+ if (contentSize.height < boundsSize.height) {
+ top = (boundsSize.height - contentSize.height) * 0.5f;
+ }
+ self.contentInset = UIEdgeInsetsMake(top, left, top, left);
+}
+
+- (void)setContentOffset:(CGPoint)contentOffset
+{
+ if (_isSetContentOffsetDisabled) {
+ return;
+ }
super.contentOffset = CGPointMake(
RCTSanitizeNaNValue(contentOffset.x, @"scrollView.contentOffset.x"),
RCTSanitizeNaNValue(contentOffset.y, @"scrollView.contentOffset.y"));
}
+- (void)setFrame:(CGRect)frame {
+ [super setFrame:frame];
+ [self centerContentIfNeeded];
+}
+
+- (void)didAddSubview:(UIView *)subview {
+ [super didAddSubview:subview];
+ [self centerContentIfNeeded];
+}
+
- (BOOL)touchesShouldCancelInContentView:(UIView *)view
{
if ([_overridingDelegate respondsToSelector:@selector(touchesShouldCancelInContentView:)]) {
@@ -257,6 +280,10 @@ - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView
}
}
+- (void)scrollViewDidZoom:(__unused UIScrollView *)scrollView {
+ [self centerContentIfNeeded];
+}
+
#pragma mark -
- (BOOL)isHorizontal:(UIScrollView *)scrollView
diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h
index e9b330f..ec5f58c 100644
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h
+++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h
@@ -15,5 +15,8 @@
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) RCTDirectEventBlock onRefresh;
@property (nonatomic, weak) UIScrollView *scrollView;
+@property (nonatomic, copy) UIColor *customTintColor;
+
+- (void)forwarderBeginRefreshing;
@end
diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
index 53bfd04..ff1b1ed 100644
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
+++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
@@ -23,6 +23,7 @@ @implementation RCTRefreshControl {
UIColor *_titleColor;
CGFloat _progressViewOffset;
BOOL _hasMovedToWindow;
+ UIColor *_customTintColor;
}
- (instancetype)init
@@ -58,6 +59,12 @@ - (void)layoutSubviews
_isInitialRender = false;
}
+- (void)didMoveToSuperview
+{
+ [super didMoveToSuperview];
+ [self setTintColor:_customTintColor];
+}
+
- (void)didMoveToWindow
{
[super didMoveToWindow];
@@ -221,4 +228,50 @@ - (void)refreshControlValueChanged
}
}
+// Fix for https://github.com/facebook/react-native/issues/43388
+// A bug in iOS 17.4 causes the haptic to not play when refreshing if the tintColor
+// is set before the refresh control gets added to the scrollview. We'll call this
+// function whenever the superview changes. We'll also call it if the value of customTintColor
+// changes.
+- (void)setTintColor:(UIColor *)tintColor
+{
+ if ([self.superview isKindOfClass:[UIScrollView class]] && self.tintColor != tintColor) {
+ [super setTintColor:tintColor];
+ }
+}
+
+// This method is used by Bluesky's ExpoScrollForwarder. This allows other React Native
+// libraries to perform a refresh of a scrollview and access the refresh control's onRefresh
+// function.
+- (void)forwarderBeginRefreshing
+{
+ _refreshingProgrammatically = NO;
+
+ [self sizeToFit];
+
+ if (!self.scrollView) {
+ return;
+ }
+
+ UIScrollView *scrollView = (UIScrollView *)self.scrollView;
+
+ [UIView animateWithDuration:0.3
+ delay:0
+ options:UIViewAnimationOptionBeginFromCurrentState
+ animations:^(void) {
+ // Whenever we call this method, the scrollview will always be at a position of
+ // -130 or less. Scrolling back to -65 simulates the default behavior of RCTRefreshControl
+ [scrollView setContentOffset:CGPointMake(0, -65)];
+ }
+ completion:^(__unused BOOL finished) {
+ [super beginRefreshing];
+ [self setCurrentRefreshingState:super.refreshing];
+
+ if (self->_onRefresh) {
+ self->_onRefresh(nil);
+ }
+ }
+ ];
+}
+
@end
diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m
index 40aaf9c..1c60164 100644
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m
+++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m
@@ -22,11 +22,12 @@ - (UIView *)view
RCT_EXPORT_VIEW_PROPERTY(onRefresh, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(refreshing, BOOL)
-RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(title, NSString)
RCT_EXPORT_VIEW_PROPERTY(titleColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(progressViewOffset, CGFloat)
+RCT_REMAP_VIEW_PROPERTY(tintColor, customTintColor, UIColor)
+
RCT_EXPORT_METHOD(setNativeRefreshing : (nonnull NSNumber *)viewTag toRefreshing : (BOOL)refreshing)
{
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
diff --git a/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m b/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m
index e9ce48c..ccd9ad6 100644
--- a/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m
+++ b/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m
@@ -159,26 +159,8 @@ - (BOOL)touchesShouldCancelInContentView:(__unused UIView *)view
return !shouldDisableScrollInteraction;
}
-/*
- * Automatically centers the content such that if the content is smaller than the
- * ScrollView, we force it to be centered, but when you zoom or the content otherwise
- * becomes larger than the ScrollView, there is no padding around the content but it
- * can still fill the whole view.
- */
- (void)setContentOffset:(CGPoint)contentOffset
{
- UIView *contentView = [self contentView];
- if (contentView && _centerContent && !CGSizeEqualToSize(contentView.frame.size, CGSizeZero)) {
- CGSize subviewSize = contentView.frame.size;
- CGSize scrollViewSize = self.bounds.size;
- if (subviewSize.width <= scrollViewSize.width) {
- contentOffset.x = -(scrollViewSize.width - subviewSize.width) / 2.0;
- }
- if (subviewSize.height <= scrollViewSize.height) {
- contentOffset.y = -(scrollViewSize.height - subviewSize.height) / 2.0;
- }
- }
-
super.contentOffset = CGPointMake(
RCTSanitizeNaNValue(contentOffset.x, @"scrollView.contentOffset.x"),
RCTSanitizeNaNValue(contentOffset.y, @"scrollView.contentOffset.y"));
@@ -427,6 +409,11 @@ - (void)setRemoveClippedSubviews:(__unused BOOL)removeClippedSubviews
// Does nothing
}
+- (void)setFrame:(CGRect)frame {
+ [super setFrame:frame];
+ [self centerContentIfNeeded];
+}
+
- (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex
{
[super insertReactSubview:view atIndex:atIndex];
@@ -443,6 +430,8 @@ - (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex
_contentView = view;
RCTApplyTransformationAccordingLayoutDirection(_contentView, self.reactLayoutDirection);
[_scrollView addSubview:view];
+
+ [self centerContentIfNeeded];
}
}
@@ -652,9 +641,46 @@ -(void)delegateMethod : (UIScrollView *)scrollView \
}
RCT_SCROLL_EVENT_HANDLER(scrollViewWillBeginDecelerating, onMomentumScrollBegin)
-RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, onScroll)
RCT_SCROLL_EVENT_HANDLER(scrollViewDidScrollToTop, onScrollToTop)
+-(void)scrollViewDidZoom : (UIScrollView *)scrollView
+{
+ [self centerContentIfNeeded];
+
+ RCT_SEND_SCROLL_EVENT(onScroll, nil);
+ RCT_FORWARD_SCROLL_EVENT(scrollViewDidZoom : scrollView);
+}
+
+/*
+ * Automatically centers the content such that if the content is smaller than the
+ * ScrollView, we force it to be centered, but when you zoom or the content otherwise
+ * becomes larger than the ScrollView, there is no padding around the content but it
+ * can still fill the whole view.
+ * This implementation is based on https://petersteinberger.com/blog/2013/how-to-center-uiscrollview/.
+ */
+-(void)centerContentIfNeeded
+{
+ if (!_scrollView.centerContent) {
+ return;
+ }
+
+ CGSize contentSize = self.contentSize;
+ CGSize boundsSize = self.bounds.size;
+ if (CGSizeEqualToSize(contentSize, CGSizeZero) ||
+ CGSizeEqualToSize(boundsSize, CGSizeZero)) {
+ return;
+ }
+
+ CGFloat top = 0, left = 0;
+ if (contentSize.width < boundsSize.width) {
+ left = (boundsSize.width - contentSize.width) * 0.5f;
+ }
+ if (contentSize.height < boundsSize.height) {
+ top = (boundsSize.height - contentSize.height) * 0.5f;
+ }
+ _scrollView.contentInset = UIEdgeInsetsMake(top, left, top, left);
+}
+
- (void)addScrollListener:(NSObject<UIScrollViewDelegate> *)scrollListener
{
[_scrollListeners addObject:scrollListener];
@@ -933,6 +959,7 @@ - (void)updateContentSizeIfNeeded
CGSize contentSize = self.contentSize;
if (!CGSizeEqualToSize(_scrollView.contentSize, contentSize)) {
_scrollView.contentSize = contentSize;
+ [self centerContentIfNeeded];
}
}
@@ -12,9 +12,6 @@ Patching `RCTRefreshControl.m` and `RCTRefreshControl.h` to add a new `forwarder
This method is used by `ExpoScrollForwarder` to initiate a refresh of the underlying `UIScrollView` from inside that
module.
## ScrollView centerContent fix
## TextInput Patch - `selectTextOnFocus` fix
Patching `RCTBaseTextInputView.m` to fix an issue where `selectTextOnFocus` does not work as expected on iOS 17. This
patch _only_ fixes the Paper version of `TextInput`. If we migrate to Fabric and the fix has not been made upstream,
we can apply the same fix. See https://github.com/facebook/react-native/pull/44307.
Includes https://github.com/facebook/react-native/pull/47591 early. Delete when it's in a release.
@@ -1,39 +0,0 @@
diff --git a/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java b/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java
index 5de0845..fa477e7 100644
--- a/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java
+++ b/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java
@@ -21,6 +21,7 @@ import android.webkit.MimeTypeMap;
import androidx.core.app.ActivityCompat;
import androidx.core.content.FileProvider;
+import androidx.exifinterface.media.ExifInterface;
import com.facebook.react.bridge.ActivityEventListener;
import com.facebook.react.bridge.Callback;
@@ -678,6 +679,15 @@ class PickerModule extends ReactContextBaseJavaModule implements ActivityEventLi
throw new Exception("Cannot select remote files");
}
BitmapFactory.Options original = validateImage(path);
+ ExifInterface originalExif = new ExifInterface(path);
+ int orientation = originalExif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 1);
+ boolean invertDimensions = (
+ orientation == ExifInterface.ORIENTATION_ROTATE_90 ||
+ orientation == ExifInterface.ORIENTATION_ROTATE_270 ||
+ orientation == ExifInterface.ORIENTATION_TRANSPOSE ||
+ orientation == ExifInterface.ORIENTATION_TRANSVERSE
+ );
+
// if compression options are provided image will be compressed. If none options is provided,
// then original image will be returned
@@ -687,8 +697,8 @@ class PickerModule extends ReactContextBaseJavaModule implements ActivityEventLi
long modificationDate = new File(path).lastModified();
image.putString("path", "file://" + compressedImagePath);
- image.putInt("width", options.outWidth);
- image.putInt("height", options.outHeight);
+ image.putInt("width", invertDimensions ? options.outHeight : options.outWidth);
+ image.putInt("height", invertDimensions ? options.outWidth : options.outHeight);
image.putString("mime", options.outMimeType);
image.putInt("size", (int) new File(compressedImagePath).length());
image.putString("modificationDate", String.valueOf(modificationDate));
@@ -1,20 +0,0 @@
const {withAndroidStyles, AndroidConfig} = require('@expo/config-plugins')
module.exports = function withAndroidStylesWindowBackgroundPlugin(appConfig) {
return withAndroidStyles(appConfig, function (decoratedAppConfig) {
try {
decoratedAppConfig.modResults = AndroidConfig.Styles.assignStylesValue(
decoratedAppConfig.modResults,
{
add: true,
parent: AndroidConfig.Styles.getAppThemeLightNoActionBarGroup(),
name: 'android:windowBackground',
value: '@drawable/splashscreen',
},
)
} catch (e) {
console.error(`withAndroidStylesWindowBackgroundPlugin failed`, e)
}
return decoratedAppConfig
})
}
+17
View File
@@ -0,0 +1,17 @@
import {useEffect} from 'react'
import * as SplashScreen from 'expo-splash-screen'
type Props = {
isReady: boolean
}
export function Splash({isReady, children}: React.PropsWithChildren<Props>) {
useEffect(() => {
if (isReady) {
SplashScreen.hideAsync()
}
}, [isReady])
if (isReady) {
return children
}
}
+22 -63
View File
@@ -18,9 +18,7 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
import Svg, {Path, SvgProps} from 'react-native-svg'
import {Image} from 'expo-image'
import * as SplashScreen from 'expo-splash-screen'
import MaskedView from '@react-native-masked-view/masked-view'
import {isAndroid} from '#/platform/detection'
import {Logotype} from '#/view/icons/Logotype'
// @ts-ignore
import splashImagePointer from '../assets/splash.png'
@@ -53,8 +51,6 @@ type Props = {
isReady: boolean
}
const AnimatedLogo = Animated.createAnimatedComponent(Logo)
export function Splash(props: React.PropsWithChildren<Props>) {
'use no memo'
const insets = useSafeAreaInsets()
@@ -152,8 +148,6 @@ export function Splash(props: React.PropsWithChildren<Props>) {
{duration: 400, easing: Easing.out(Easing.cubic)},
async () => {
// set these values to check animation at specific point
// outroLogo.set(0.1)
// outroApp.set(0.1)
outroLogo.set(() =>
withTiming(
1,
@@ -221,66 +215,31 @@ export function Splash(props: React.PropsWithChildren<Props>) {
</View>
)}
{isReady &&
(isAndroid || reduceMotion === true ? (
// Use a simple fade on older versions of android (work around a bug)
<>
<Animated.View style={[{flex: 1}, appAnimation]}>
{props.children}
</Animated.View>
{isReady && (
<>
<Animated.View style={[{flex: 1}, appAnimation]}>
{props.children}
</Animated.View>
{!isAnimationComplete && (
<Animated.View
style={[
StyleSheet.absoluteFillObject,
logoWrapperAnimation,
{
flex: 1,
justifyContent: 'center',
alignItems: 'center',
transform: [{translateY: -(insets.top / 2)}, {scale: 0.1}], // scale from 1000px to 100px
},
]}>
<AnimatedLogo
fill={logoBg}
style={[{opacity: 0}, logoAnimations]}
/>
{!isAnimationComplete && (
<Animated.View
style={[
StyleSheet.absoluteFillObject,
logoWrapperAnimation,
{
flex: 1,
justifyContent: 'center',
alignItems: 'center',
transform: [{translateY: -(insets.top / 2)}, {scale: 0.1}], // scale from 1000px to 100px
},
]}>
<Animated.View style={[logoAnimations]}>
<Logo fill={logoBg} />
</Animated.View>
)}
</>
) : (
<MaskedView
style={[StyleSheet.absoluteFillObject]}
maskElement={
<Animated.View
style={[
{
// Transparent background because mask is based off alpha channel.
backgroundColor: 'transparent',
flex: 1,
justifyContent: 'center',
alignItems: 'center',
transform: [{translateY: -(insets.top / 2)}, {scale: 0.1}], // scale from 1000px to 100px
},
]}>
<AnimatedLogo fill={logoBg} style={[logoAnimations]} />
</Animated.View>
}>
{!isAnimationComplete && (
<View
style={[
StyleSheet.absoluteFillObject,
{
backgroundColor: logoBg,
},
]}
/>
)}
<Animated.View style={[{flex: 1}, appAnimation]}>
{props.children}
</Animated.View>
</MaskedView>
))}
)}
</>
)}
</View>
)
}
+22 -1
View File
@@ -1,7 +1,8 @@
import {Platform, StyleProp, StyleSheet, ViewStyle} from 'react-native'
import * as tokens from '#/alf/tokens'
import {ios, native, web} from '#/alf/util/platform'
import {ios, native, platform, web} from '#/alf/util/platform'
import * as Layout from '#/components/Layout'
export const atoms = {
debug: {
@@ -21,6 +22,9 @@ export const atoms = {
relative: {
position: 'relative',
},
sticky: web({
position: 'sticky',
}),
inset_0: {
top: 0,
left: 0,
@@ -64,6 +68,7 @@ export const atoms = {
* Used for the outermost components on screens, to ensure that they can fill
* the screen and extend beyond.
*/
// @ts-ignore - web only minHeight string
util_screen_outer: [
web({
minHeight: '100vh',
@@ -941,4 +946,20 @@ export const atoms = {
transitionTimingFunction: 'cubic-bezier(0.17, 0.73, 0.14, 1)',
transitionDuration: '100ms',
}),
/**
* {@link Layout.SCROLLBAR_OFFSET}
*/
scrollbar_offset: platform({
web: {
transform: [
{
translateX: Layout.SCROLLBAR_OFFSET,
},
],
},
native: {
transform: [],
},
}) as {transform: Exclude<ViewStyle['transform'], string | undefined>},
} as const
+1
View File
@@ -20,6 +20,7 @@ export * from '#/alf/types'
export * from '#/alf/util/flatten'
export * from '#/alf/util/platform'
export * from '#/alf/util/themeSelector'
export * from '#/alf/util/useGutterStyles'
export type Alf = {
themeName: ThemeName
+21
View File
@@ -0,0 +1,21 @@
import React from 'react'
import {atoms as a, useBreakpoints, ViewStyleProp} from '#/alf'
export function useGutterStyles({
top,
bottom,
}: {
top?: boolean
bottom?: boolean
} = {}) {
const {gtMobile} = useBreakpoints()
return React.useMemo<ViewStyleProp['style']>(() => {
return [
a.px_lg,
top && a.pt_md,
bottom && a.pb_md,
gtMobile && [a.px_xl, top && a.pt_lg, bottom && a.pb_lg],
]
}, [gtMobile, top, bottom])
}
+41 -30
View File
@@ -405,37 +405,47 @@ export const Button = React.forwardRef<View, ButtonProps>(
}
}, [t, variant, color, size, shape, disabled])
const {gradientColors, gradientHoverColors, gradientLocations} =
React.useMemo(() => {
const colors: string[] = []
const hoverColors: string[] = []
const locations: number[] = []
const gradient = {
primary: tokens.gradients.sky,
secondary: tokens.gradients.sky,
secondary_inverted: tokens.gradients.sky,
negative: tokens.gradients.sky,
gradient_primary: tokens.gradients.primary,
gradient_sky: tokens.gradients.sky,
gradient_midnight: tokens.gradients.midnight,
gradient_sunrise: tokens.gradients.sunrise,
gradient_sunset: tokens.gradients.sunset,
gradient_nordic: tokens.gradients.nordic,
gradient_bonfire: tokens.gradients.bonfire,
}[color || 'primary']
const gradientValues = React.useMemo(() => {
const gradient = {
primary: tokens.gradients.sky,
secondary: tokens.gradients.sky,
secondary_inverted: tokens.gradients.sky,
negative: tokens.gradients.sky,
gradient_primary: tokens.gradients.primary,
gradient_sky: tokens.gradients.sky,
gradient_midnight: tokens.gradients.midnight,
gradient_sunrise: tokens.gradients.sunrise,
gradient_sunset: tokens.gradients.sunset,
gradient_nordic: tokens.gradients.nordic,
gradient_bonfire: tokens.gradients.bonfire,
}[color || 'primary']
if (variant === 'gradient') {
colors.push(...gradient.values.map(([_, color]) => color))
hoverColors.push(...gradient.values.map(_ => gradient.hover_value))
locations.push(...gradient.values.map(([location, _]) => location))
if (variant === 'gradient') {
if (gradient.values.length < 2) {
throw new Error(
'Gradient buttons must have at least two colors in the gradient',
)
}
return {
gradientColors: colors,
gradientHoverColors: hoverColors,
gradientLocations: locations,
colors: gradient.values.map(([_, color]) => color) as [
string,
string,
...string[],
],
hoverColors: gradient.values.map(_ => gradient.hover_value) as [
string,
string,
...string[],
],
locations: gradient.values.map(([location, _]) => location) as [
number,
number,
...number[],
],
}
}, [variant, color])
}
}, [variant, color])
const context = React.useMemo<ButtonContext>(
() => ({
@@ -458,6 +468,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
// @ts-ignore - this will always be a pressable
ref={ref}
aria-label={label}
aria-pressed={state.pressed}
accessibilityLabel={label}
disabled={disabled || false}
accessibilityState={{
@@ -478,7 +489,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
onHoverOut={onHoverOut}
onFocus={onFocus}
onBlur={onBlur}>
{variant === 'gradient' && (
{variant === 'gradient' && gradientValues && (
<View
style={[
a.absolute,
@@ -489,10 +500,10 @@ export const Button = React.forwardRef<View, ButtonProps>(
<LinearGradient
colors={
state.hovered || state.pressed
? gradientHoverColors
: gradientColors
? gradientValues.hoverColors
: gradientValues.colors
}
locations={gradientLocations}
locations={gradientValues.locations}
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
style={[a.absolute, a.inset_0]}
+2
View File
@@ -12,6 +12,7 @@ import {useLingui} from '@lingui/react'
import {DismissableLayer} from '@radix-ui/react-dismissable-layer'
import {useFocusGuards} from '@radix-ui/react-focus-guards'
import {FocusScope} from '@radix-ui/react-focus-scope'
import {RemoveScrollBar} from 'react-remove-scroll-bar'
import {logger} from '#/logger'
import {useDialogStateControlContext} from '#/state/dialogs'
@@ -103,6 +104,7 @@ export function Outer({
{isOpen && (
<Portal>
<Context.Provider value={context}>
<RemoveScrollBar />
<TouchableWithoutFeedback
accessibilityHint={undefined}
accessibilityLabel={_(msg`Close active dialog`)}
+8 -2
View File
@@ -14,10 +14,16 @@ export function GradientFill({
| typeof tokens.gradients.summer
| typeof tokens.gradients.nordic
}) {
if (gradient.values.length < 2) {
throw new Error('Gradient must have at least 2 colors')
}
return (
<LinearGradient
colors={gradient.values.map(c => c[1])}
locations={gradient.values.map(c => c[0])}
colors={gradient.values.map(c => c[1]) as [string, string, ...string[]]}
locations={
gradient.values.map(c => c[0]) as [number, number, ...number[]]
}
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
style={[a.absolute, a.inset_0]}
-100
View File
@@ -1,100 +0,0 @@
import React, {useContext, useMemo} from 'react'
import {View, ViewStyle} from 'react-native'
import {StyleProp} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {ScrollView} from '#/view/com/util/Views'
import {CenteredView} from '#/view/com/util/Views'
import {atoms as a} from '#/alf'
// Every screen should have a Layout component wrapping it.
// This component provides a default padding for the top of the screen.
// This allows certain screens to avoid the top padding if they want to.
const LayoutContext = React.createContext({
withinScreen: false,
topPaddingDisabled: false,
withinScrollView: false,
})
/**
* Every screen should have a Layout.Screen component wrapping it.
* This component provides a default padding for the top of the screen
* and height/minHeight
*/
let Screen = ({
disableTopPadding = false,
style,
...props
}: React.ComponentProps<typeof View> & {
disableTopPadding?: boolean
style?: StyleProp<ViewStyle>
}): React.ReactNode => {
const {top} = useSafeAreaInsets()
const context = useMemo(
() => ({
withinScreen: true,
topPaddingDisabled: disableTopPadding,
withinScrollView: false,
}),
[disableTopPadding],
)
return (
<LayoutContext.Provider value={context}>
<View
style={[
{paddingTop: disableTopPadding ? 0 : top},
a.util_screen_outer,
style,
]}
{...props}
/>
</LayoutContext.Provider>
)
}
Screen = React.memo(Screen)
export {Screen}
let Header = (
props: React.ComponentProps<typeof ViewHeader>,
): React.ReactNode => {
const {withinScrollView} = useContext(LayoutContext)
if (!withinScrollView) {
return (
<CenteredView topBorder={false} sideBorders>
<ViewHeader showOnDesktop showBorder {...props} />
</CenteredView>
)
} else {
return <ViewHeader showOnDesktop showBorder {...props} />
}
}
Header = React.memo(Header)
export {Header}
let Content = ({
style,
contentContainerStyle,
...props
}: React.ComponentProps<typeof ScrollView> & {
style?: StyleProp<ViewStyle>
contentContainerStyle?: StyleProp<ViewStyle>
}): React.ReactNode => {
const context = useContext(LayoutContext)
const newContext = useMemo(
() => ({...context, withinScrollView: true}),
[context],
)
return (
<LayoutContext.Provider value={newContext}>
<ScrollView
style={[a.flex_1, style]}
contentContainerStyle={[{paddingBottom: 100}, contentContainerStyle]}
{...props}
/>
</LayoutContext.Provider>
)
}
Content = React.memo(Content)
export {Content}
+199
View File
@@ -0,0 +1,199 @@
import {createContext, useCallback, useContext} from 'react'
import {GestureResponderEvent, View} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {HITSLOP_30} from '#/lib/constants'
import {NavigationProp} from '#/lib/routes/types'
import {isIOS} from '#/platform/detection'
import {useSetDrawerOpen} from '#/state/shell'
import {
atoms as a,
platform,
TextStyleProp,
useBreakpoints,
useGutterStyles,
useTheme,
} from '#/alf'
import {Button, ButtonIcon, ButtonProps} from '#/components/Button'
import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeft} from '#/components/icons/Arrow'
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
import {
BUTTON_VISUAL_ALIGNMENT_OFFSET,
HEADER_SLOT_SIZE,
} from '#/components/Layout/const'
import {ScrollbarOffsetContext} from '#/components/Layout/context'
import {Text} from '#/components/Typography'
export function Outer({
children,
noBottomBorder,
}: {
children: React.ReactNode
noBottomBorder?: boolean
}) {
const t = useTheme()
const gutter = useGutterStyles()
const {gtMobile} = useBreakpoints()
const {isWithinOffsetView} = useContext(ScrollbarOffsetContext)
return (
<View
style={[
a.w_full,
!noBottomBorder && a.border_b,
a.flex_row,
a.align_center,
a.gap_sm,
gutter,
platform({
native: [a.pb_sm, a.pt_xs],
web: [a.py_sm],
}),
t.atoms.border_contrast_low,
gtMobile && [a.mx_auto, {maxWidth: 600}],
!isWithinOffsetView && a.scrollbar_offset,
]}>
{children}
</View>
)
}
const AlignmentContext = createContext<'platform' | 'left'>('platform')
export function Content({
children,
align = 'platform',
}: {
children?: React.ReactNode
align?: 'platform' | 'left'
}) {
return (
<View
style={[
a.flex_1,
a.justify_center,
isIOS && align === 'platform' && a.align_center,
{minHeight: HEADER_SLOT_SIZE},
]}>
<AlignmentContext.Provider value={align}>
{children}
</AlignmentContext.Provider>
</View>
)
}
export function Slot({children}: {children?: React.ReactNode}) {
return (
<View
style={[
a.z_50,
{
width: HEADER_SLOT_SIZE,
},
]}>
{children}
</View>
)
}
export function BackButton({onPress, style, ...props}: Partial<ButtonProps>) {
const {_} = useLingui()
const navigation = useNavigation<NavigationProp>()
const onPressBack = useCallback(
(evt: GestureResponderEvent) => {
onPress?.(evt)
if (evt.defaultPrevented) return
if (navigation.canGoBack()) {
navigation.goBack()
} else {
navigation.navigate('Home')
}
},
[onPress, navigation],
)
return (
<Slot>
<Button
label={_(msg`Go back`)}
size="small"
variant="ghost"
color="secondary"
shape="square"
onPress={onPressBack}
hitSlop={HITSLOP_30}
style={[{marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET}, style]}
{...props}>
<ButtonIcon icon={ArrowLeft} size="lg" />
</Button>
</Slot>
)
}
export function MenuButton() {
const {_} = useLingui()
const setDrawerOpen = useSetDrawerOpen()
const {gtMobile} = useBreakpoints()
const onPress = useCallback(() => {
setDrawerOpen(true)
}, [setDrawerOpen])
return gtMobile ? null : (
<Slot>
<Button
label={_(msg`Open drawer menu`)}
size="small"
variant="ghost"
color="secondary"
shape="square"
onPress={onPress}
hitSlop={HITSLOP_30}
style={[{marginLeft: -BUTTON_VISUAL_ALIGNMENT_OFFSET}]}>
<ButtonIcon icon={Menu} size="lg" />
</Button>
</Slot>
)
}
export function TitleText({
children,
style,
}: {children: React.ReactNode} & TextStyleProp) {
const {gtMobile} = useBreakpoints()
const align = useContext(AlignmentContext)
return (
<Text
style={[
a.text_lg,
a.font_heavy,
a.leading_tight,
isIOS && align === 'platform' && a.text_center,
gtMobile && a.text_xl,
style,
]}
numberOfLines={2}>
{children}
</Text>
)
}
export function SubtitleText({children}: {children: React.ReactNode}) {
const t = useTheme()
const align = useContext(AlignmentContext)
return (
<Text
style={[
a.text_sm,
a.leading_snug,
isIOS && align === 'platform' && a.text_center,
t.atoms.text_contrast_medium,
]}
numberOfLines={2}>
{children}
</Text>
)
}
+172
View File
@@ -0,0 +1,172 @@
# Layout
This directory contains our core layout components. Use these when creating new
screens, or when supplementing other components with functionality like
centering.
## Usage
If we aren't talking about the `shell` components, layouts on individual screens
look like more or less like this:
```tsx
<Outer>
<Header>...</Header>
<Content>...</Content>
</Outer>
```
I'll map these words to real components.
### `Layout.Screen`
Provides the "Outer" functionality for a screen, like taking up the full height
of the screen. **All screens should be wrapped with this component,** probably
as the outermost component.
> [!NOTE]
> On web, `Layout.Screen` also provides the side borders on our central content
> column. These borders are fixed position, 1px outside our center column width
> of 600px.
>
> What this effectively means is that _nothing inside the center content column
> needs (or should) define left/right borders._ That is now handled in one
> place: within `Layout.Screen`.
### `Layout.Header.*`
The `Layout.Header` component actually contains multiple sub-components. Use
this to compose different versions of the header. The most basic version looks
like this:
```tsx
<Layout.Header.Outer>
<Layout.Header.BackButton /> {/* or <Layout.Header.MenuButton /> */}
<Layout.Header.Content>
<Layout.Header.TitleText>Account</Layout.Header.TitleText>
{/* Optional subtitle */}
<Layout.Header.SubtitleText>Settings for @esb.lol</Layout.Header.SubtitleText>
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
```
Note the additional `Slot` component. This is here to keep the header balanced
and provide correct spacing on all platforms. The `Slot` is 34px wide, which
matches the `BackButton` and `MenuButton`.
> If anyone has better ideas, I'm all ears, but this was simple and the small
> amount of boilerplate is only incurred when creating a new screen, which is
> infrequent.
It can also function as a "slot" for a button positioned on the right side. See
the `Hashtag` screen for an example, abbreviated below:
```tsx
<Layout.Header.Slot>
<Button size='small' shape='round'>...</Button>
</Layout.Header.Slot>
```
If you need additional customization, simply use the components that are helpful
and create new ones as needed. A good example is the `SavedFeeds` screen, which
looks roughly like this:
```tsx
<Layout.Header.Outer>
<Layout.Header.BackButton />
{/* Override to align content to the left, making room for the button */}
<Layout.Header.Content align='left'>
<Layout.Header.TitleText>Edit My Feeds</Layout.Header.TitleText>
</Layout.Header.Content>
{/* Custom button, wider than 34px */}
<Button size='small'>...</Button>
</Layout.Header.Outer>
```
> [!TIP]
> The `Header` should be _outside_ the `Content` component in order to be
> fixed on scroll on native. Placing it inside will make it scroll with the rest
> of the page.
### `Layout.Content`
This provides the "Content" functionality for a screen. This component is
actually an `Animated.ScrollView`, and accepts props for that component. It
provides a little default styling as well. On web, it also _centers the content
inside our center content column of 600px_.
> [!NOTE]
> What about flatlists or pagers? Those components are not colocated here (yet).
> But those components serve the same purpose of "Content".
## Examples
The most basic layout available to us looks like this:
```tsx
<Layout.Screen>
<Layout.Header.Outer>
<Layout.Header.BackButton /> {/* or <Layout.Header.MenuButton /> */}
<Layout.Header.Content>
<Layout.Header.TitleText>Account</Layout.Header.TitleText>
{/* Optional subtitle */}
<Layout.Header.SubtitleText>Settings for @esb.lol</Layout.Header.SubtitleText>
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content>
...
</Layout.Content>
</Layout.Screen>
```
**For `List` views,** you'd sub in `List` for `Layout.Content` and it will
function the same. See `Feeds` screen for an example.
**For `Pager` views,** including `PagerWithHeader`, do the same. See `Hashtag`
screen for an example.
## Utilities
### `Layout.Center`
This component behaves like our old `CenteredView` component.
### `Layout.SCROLLBAR_OFFSET` and `Layout.SCROLLBAR_OFFSET_POSITIVE`
Provide a pre-configured CSS vars for use when aligning fixed position elements.
More on this below.
## Scrollbar gutter handling
Operating systems allow users to configure if their browser _always_ shows
scrollbars not. Some OSs also don't allow configuration.
The presence of scrollbars affects layout, particularly fixed position elements.
Browsers support `scrollbar-gutter`, but each behaves differently. Our approach
is to use the default `scrollbar-gutter: auto`. Basically, we start from a clean
slate.
This handling becomes particularly thorny when we need to lock scroll, like when
opening a dialog or dropdown. Radix uses the library `react-remove-scroll`
internally, which in turn depends on
[`react-remove-scroll-bar`](https://github.com/theKashey/react-remove-scroll-bar).
We've opted to rely on this transient dependency. This library adds some utility
classes and CSS vars to the page when scroll is locked.
**It is this CSS variable that we use in `SCROLLBAR_OFFSET` values.** This
ensures that elements do not shift relative to the screen when opening a
dropdown or dialog.
These styles are applied where needed and we should have very little need of
adjusting them often.
+16
View File
@@ -0,0 +1,16 @@
export const SCROLLBAR_OFFSET =
'calc(-1 * var(--removed-body-scroll-bar-size, 0px) / 2)' as any
export const SCROLLBAR_OFFSET_POSITIVE =
'calc(var(--removed-body-scroll-bar-size, 0px) / 2)' as any
/**
* Useful for visually aligning icons within header buttons with the elements
* below them on the screen. Apply positively or negatively depending on side
* of the screen you're on.
*/
export const BUTTON_VISUAL_ALIGNMENT_OFFSET = 3
/**
* Corresponds to the width of a small square or round button
*/
export const HEADER_SLOT_SIZE = 34
+5
View File
@@ -0,0 +1,5 @@
import React from 'react'
export const ScrollbarOffsetContext = React.createContext({
isWithinOffsetView: false,
})
+188
View File
@@ -0,0 +1,188 @@
import React, {useContext, useMemo} from 'react'
import {StyleSheet, View, ViewProps, ViewStyle} from 'react-native'
import {StyleProp} from 'react-native'
import {
KeyboardAwareScrollView,
KeyboardAwareScrollViewProps,
} from 'react-native-keyboard-controller'
import Animated, {
AnimatedScrollViewProps,
useAnimatedProps,
} from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {isWeb} from '#/platform/detection'
import {useShellLayout} from '#/state/shell/shell-layout'
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
import {ScrollbarOffsetContext} from '#/components/Layout/context'
export * from '#/components/Layout/const'
export * as Header from '#/components/Layout/Header'
export type ScreenProps = React.ComponentProps<typeof View> & {
style?: StyleProp<ViewStyle>
}
/**
* Outermost component of every screen
*/
export const Screen = React.memo(function Screen({
style,
...props
}: ScreenProps) {
const {top} = useSafeAreaInsets()
return (
<>
{isWeb && <WebCenterBorders />}
<View
style={[a.util_screen_outer, {paddingTop: top}, style]}
{...props}
/>
</>
)
})
export type ContentProps = AnimatedScrollViewProps & {
style?: StyleProp<ViewStyle>
contentContainerStyle?: StyleProp<ViewStyle>
}
/**
* Default scroll view for simple pages
*/
export const Content = React.memo(function Content({
children,
style,
contentContainerStyle,
...props
}: ContentProps) {
const {footerHeight} = useShellLayout()
const animatedProps = useAnimatedProps(() => {
return {
scrollIndicatorInsets: {
bottom: footerHeight.get(),
top: 0,
right: 1,
},
} satisfies AnimatedScrollViewProps
})
return (
<Animated.ScrollView
id="content"
automaticallyAdjustsScrollIndicatorInsets={false}
// sets the scroll inset to the height of the footer
animatedProps={animatedProps}
style={[scrollViewStyles.common, style]}
contentContainerStyle={[
scrollViewStyles.contentContainer,
contentContainerStyle,
]}
{...props}>
{isWeb ? (
// @ts-ignore web only -esb
<Center>{children}</Center>
) : (
children
)}
</Animated.ScrollView>
)
})
const scrollViewStyles = StyleSheet.create({
common: {
width: '100%',
},
contentContainer: {
paddingBottom: 100,
},
})
export type KeyboardAwareContentProps = KeyboardAwareScrollViewProps & {
children: React.ReactNode
contentContainerStyle?: StyleProp<ViewStyle>
}
/**
* Default scroll view for simple pages.
*
* BE SURE TO TEST THIS WHEN USING, it's untested as of writing this comment.
*/
export const KeyboardAwareContent = React.memo(function LayoutScrollView({
children,
style,
contentContainerStyle,
...props
}: KeyboardAwareContentProps) {
return (
<KeyboardAwareScrollView
style={[scrollViewStyles.common, style]}
contentContainerStyle={[
scrollViewStyles.contentContainer,
contentContainerStyle,
]}
keyboardShouldPersistTaps="handled"
{...props}>
{isWeb ? <Center>{children}</Center> : children}
</KeyboardAwareScrollView>
)
})
/**
* Utility component to center content within the screen
*/
export const Center = React.memo(function LayoutContent({
children,
style,
...props
}: ViewProps) {
const {isWithinOffsetView} = useContext(ScrollbarOffsetContext)
const {gtMobile} = useBreakpoints()
const ctx = useMemo(() => ({isWithinOffsetView: true}), [])
return (
<View
style={[
a.w_full,
a.mx_auto,
gtMobile && {
maxWidth: 600,
},
style,
!isWithinOffsetView && a.scrollbar_offset,
]}
{...props}>
<ScrollbarOffsetContext.Provider value={ctx}>
{children}
</ScrollbarOffsetContext.Provider>
</View>
)
})
/**
* Only used within `Layout.Screen`, not for reuse
*/
const WebCenterBorders = React.memo(function LayoutContent() {
const t = useTheme()
const {gtMobile} = useBreakpoints()
return gtMobile ? (
<View
style={[
a.fixed,
a.inset_0,
a.border_l,
a.border_r,
t.atoms.border_contrast_low,
web({
width: 602,
left: '50%',
transform: [
{
translateX: '-50%',
},
...a.scrollbar_offset.transform,
],
}),
]}
/>
) : null
})
+11 -2
View File
@@ -12,8 +12,14 @@ import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
import {List} from '#/view/com/util/List'
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
function renderItem({item}: {item: GetLikes.Like}) {
return <ProfileCardWithFollowBtn key={item.actor.did} profile={item.actor} />
function renderItem({item, index}: {item: GetLikes.Like; index: number}) {
return (
<ProfileCardWithFollowBtn
key={item.actor.did}
profile={item.actor}
noBorder={index === 0}
/>
)
}
function keyExtractor(item: GetLikes.Like) {
@@ -81,6 +87,8 @@ export function LikedByList({uri}: {uri: string}) {
)}
errorMessage={cleanError(resolveError || error)}
onRetry={isError ? refetch : undefined}
topBorder={false}
sideBorders={false}
/>
)
}
@@ -103,6 +111,7 @@ export function LikedByList({uri}: {uri: string}) {
onEndReachedThreshold={3}
initialNumToRender={initialNumToRender}
windowSize={11}
sideBorders={false}
/>
)
}
+5 -1
View File
@@ -13,7 +13,11 @@ export function LinearGradientBackground({
}) {
const gradient = gradients.sky.values.map(([_, color]) => {
return color
})
}) as [string, string, ...string[]]
if (gradient.length < 2) {
throw new Error('Gradient must have at least 2 colors')
}
return (
<LinearGradient colors={gradient} style={style}>
+1 -33
View File
@@ -109,38 +109,6 @@ function ListFooterMaybeError({
)
}
export function ListHeaderDesktop({
title,
subtitle,
}: {
title: string
subtitle?: string
}) {
const {gtTablet} = useBreakpoints()
const t = useTheme()
if (!gtTablet) return null
return (
<View
style={[
a.w_full,
a.py_sm,
a.px_xl,
a.gap_xs,
a.justify_center,
{minHeight: 50},
]}>
<Text style={[a.text_2xl, a.font_bold]}>{title}</Text>
{subtitle ? (
<Text style={[a.text_md, t.atoms.text_contrast_medium]}>
{subtitle}
</Text>
) : undefined}
</View>
)
}
let ListMaybePlaceholder = ({
isLoading,
noEmpty,
@@ -154,7 +122,7 @@ let ListMaybePlaceholder = ({
onGoBack,
hideBackButton,
sideBorders,
topBorder = true,
topBorder = false,
}: {
isLoading: boolean
noEmpty?: boolean
+1
View File
@@ -1,5 +1,6 @@
import React from 'react'
import {View} from 'react-native'
// @ts-expect-error missing types
import QRCode from 'react-native-qrcode-styled'
import type ViewShot from 'react-native-view-shot'
import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
+6 -11
View File
@@ -3,12 +3,7 @@ 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 {Nux, useNuxs, useResetNuxs, useSaveNux} from '#/state/queries/nuxs'
import {
usePreferencesQuery,
UsePreferencesQueryResponse,
@@ -85,8 +80,8 @@ function Inner({
return isSnoozed()
})
const [activeNux, setActiveNux] = React.useState<Nux | undefined>()
const {mutateAsync: upsertNux} = useUpsertNuxMutation()
const {mutate: removeNuxs} = useRemoveNuxsMutation()
const {mutateAsync: saveNux} = useSaveNux()
const {mutate: resetNuxs} = useResetNuxs()
const snoozeNuxDialog = React.useCallback(() => {
snooze()
@@ -102,7 +97,7 @@ function Inner({
// @ts-ignore
window.clearNuxDialog = (id: Nux) => {
if (!IS_DEV || !id) return
removeNuxs([id])
resetNuxs([id])
unsnooze()
}
}
@@ -136,7 +131,7 @@ function Inner({
snoozeNuxDialog()
// immediately update remote data (affects next reload)
upsertNux({
saveNux({
id,
completed: true,
data: undefined,
@@ -152,7 +147,7 @@ function Inner({
nuxs,
snoozed,
snoozeNuxDialog,
upsertNux,
saveNux,
gate,
currentAccount,
currentProfile,
+17 -19
View File
@@ -65,25 +65,23 @@ export let MessagesListHeader = ({
a.pr_lg,
a.py_sm,
]}>
{!gtTablet && (
<TouchableOpacity
testID="conversationHeaderBackBtn"
onPress={onPressBack}
hitSlop={BACK_HITSLOP}
style={{width: 30, height: 30, marginTop: isWeb ? 6 : 4}}
accessibilityRole="button"
accessibilityLabel={_(msg`Back`)}
accessibilityHint="">
<FontAwesomeIcon
size={18}
icon="angle-left"
style={{
marginTop: 6,
}}
color={t.atoms.text.color}
/>
</TouchableOpacity>
)}
<TouchableOpacity
testID="conversationHeaderBackBtn"
onPress={onPressBack}
hitSlop={BACK_HITSLOP}
style={{width: 30, height: 30, marginTop: isWeb ? 6 : 4}}
accessibilityRole="button"
accessibilityLabel={_(msg`Back`)}
accessibilityHint="">
<FontAwesomeIcon
size={18}
icon="angle-left"
style={{
marginTop: 6,
}}
color={t.atoms.text.color}
/>
</TouchableOpacity>
{profile && moderation && blockInfo ? (
<HeaderReady
@@ -50,11 +50,15 @@ export function DateField({
/>
{open && (
// Android implementation of DatePicker currently does not change default button colors according to theme and only takes hex values for buttonColor
// Can remove the buttonColor setting if/when this PR is merged: https://github.com/henninghall/react-native-date-picker/pull/871
<DatePicker
modal
open
timeZoneOffsetInMinutes={0}
theme={t.name === 'light' ? 'light' : 'dark'}
theme={t.scheme}
// @ts-ignore TODO
buttonColor={t.name === 'light' ? '#000000' : '#ffffff'}
date={new Date(value)}
onConfirm={onChangeInternal}
onCancel={onCancel}
+1 -1
View File
@@ -44,8 +44,8 @@ import {useLanguagePrefs} from '#/state/preferences'
*/
const locales: Record<AppLanguage, Locale | undefined> = {
en: undefined,
ast: undefined,
an: undefined,
ast: undefined,
ca,
de,
['en-GB']: enGB,
+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const AndroidLogo = createSinglePathSVG({
path: 'm15.604 2.47-.642 1.112a4.997 4.997 0 0 1 2.05 4.034H7.005c0-1.658.809-3.123 2.05-4.034L8.41 2.47a.313.313 0 0 1 .542-.313l.627 1.086a4.97 4.97 0 0 1 2.428-.63c.882 0 1.709.23 2.428.63l.627-1.085a.311.311 0 0 1 .426-.115c.151.087.202.278.115.428ZM9.506 5.114a.625.625 0 1 0 1.25 0 .625.625 0 0 0-1.25 0Zm3.753 0a.625.625 0 1 0 1.25 0 .625.625 0 0 0-1.25 0ZM3.878 8.866a1.251 1.251 0 0 1 2.501 0v5.004a1.252 1.252 0 0 1-2.501 0V8.866Zm13.759 0a1.251 1.251 0 0 1 2.501 0v5.004a1.251 1.251 0 0 1-2.501 0V8.866ZM7.005 17.622h1.25v3.128a1.251 1.251 0 0 0 2.502 0v-3.128h2.502v3.128a1.251 1.251 0 0 0 2.501 0v-3.128h1.25V8.241H7.006v9.381Z',
})
+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const AppleLogo = createSinglePathSVG({
path: 'M14.57 4.348c.65-.775 1.11-1.85 1.11-2.926 0-.149-.013-.3-.04-.422-1.057.04-2.314.708-3.072 1.592-.595.68-1.15 1.756-1.15 2.846 0 .164.027.327.04.38.066.012.175.027.283.027.948 0 2.14-.638 2.83-1.497Zm4.835 3.847.052-.035c-1.407-2.03-3.546-2.084-4.14-2.084-.911 0-1.726.325-2.411.598-.497.198-.925.368-1.271.368-.383 0-.822-.178-1.311-.377-.618-.25-1.317-.534-2.087-.534C5.64 6.13 3 8.296 3 12.379c0 2.545.975 5.227 2.182 6.954C6.224 20.803 7.13 22 8.43 22c.616 0 1.068-.193 1.543-.395.526-.225 1.082-.462 1.922-.462.849 0 1.356.223 1.845.437.455.2.895.393 1.58.393 1.42 0 2.353-1.292 3.246-2.586 1.003-1.47 1.422-2.913 1.435-2.98-.081-.027-2.802-1.13-2.802-4.246 0-2.51 1.855-3.734 2.207-3.966Z',
})
+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Clock_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M4 12a8 8 0 1 1 16 0 8 8 0 0 1-16 0Zm8-10C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2Zm1 6a1 1 0 1 0-2 0v4a1 1 0 0 0 .293.707l2.5 2.5a1 1 0 0 0 1.414-1.414L13 11.586V8Z',
})
+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const FloppyDisk_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M3 4a1 1 0 0 1 1-1h13a1 1 0 0 1 .707.293l3 3A1 1 0 0 1 21 7v13a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm6 15h6v-5H9v5Zm8 0v-6a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v6H5V5h2v3a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V5.414l2 2V19h-2ZM15 5H9v2h6V5Z',
})
+9
View File
@@ -0,0 +1,9 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Gift1_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M6 4.667A2.667 2.667 0 0 1 8.667 2c1.34 0 2.538.608 3.333 1.564A4.324 4.324 0 0 1 15.333 2 2.667 2.667 0 0 1 18 4.667c0 .859-.25 1.66-.681 2.333H20a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1v7a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-7a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1h2.681A4.313 4.313 0 0 1 6 4.667ZM10.333 7H11v-.667A2.333 2.333 0 0 0 8.667 4 .667.667 0 0 0 8 4.667 2.333 2.333 0 0 0 10.333 7ZM13 6.333V7h.667A2.333 2.333 0 0 0 16 4.667.667.667 0 0 0 15.333 4 2.333 2.333 0 0 0 13 6.333ZM11 9H5v2h6V9Zm2 2V9h6v2h-6Zm-2 2H6v6h5v-6Zm2 6v-6h5v6h-5Z',
})
export const Gift1_Filled_Corner0_Rounded = createSinglePathSVG({
path: 'M6 4.667A2.667 2.667 0 0 1 8.667 2c1.34 0 2.538.608 3.333 1.564A4.324 4.324 0 0 1 15.333 2 2.667 2.667 0 0 1 18 4.667c0 .859-.25 1.66-.681 2.333H20a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-7V7h.667A2.333 2.333 0 0 0 16 4.667.667.667 0 0 0 15.333 4 2.333 2.333 0 0 0 13 6.333V7h-2v-.667A2.333 2.333 0 0 0 8.667 4 .667.667 0 0 0 8 4.667 2.333 2.333 0 0 0 10.333 7H11v4H4a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1h2.681A4.313 4.313 0 0 1 6 4.667ZM11 13H4v7a1 1 0 0 0 1 1h6v-8Zm9 0h-7v8h6a1 1 0 0 0 1-1v-7Z',
})
+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Mark = createSinglePathSVG({
path: 'M6.335 4.212c2.293 1.76 4.76 5.327 5.665 7.241.906-1.914 3.372-5.482 5.665-7.241C19.319 2.942 22 1.96 22 5.086c0 .624-.35 5.244-.556 5.994-.713 2.608-3.315 3.273-5.629 2.87 4.045.704 5.074 3.035 2.852 5.366-4.22 4.426-6.066-1.111-6.54-2.53-.086-.26-.126-.382-.127-.278 0-.104-.041.018-.128.278-.473 1.419-2.318 6.956-6.539 2.53-2.222-2.331-1.193-4.662 2.852-5.366-2.314.403-4.916-.262-5.63-2.87C2.35 10.33 2 5.71 2 5.086c0-3.126 2.68-2.144 4.335-.874Z',
})
+1 -1
View File
@@ -1,5 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const VideoClip_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4Zm2 1v2h2V5H5Zm4 0v6h6V5H9Zm8 0v2h2V5h-2Zm2 4h-2v2h2V9Zm0 4h-2v2.444h2V13Zm0 4.444h-2V19h2v-1.556ZM15 19v-6H9v6h6Zm-8 0v-2H5v2h2Zm-2-4h2v-2H5v2Zm0-4h2V9H5v2Z',
path: 'M3 4a1 1 0 011-1h16a1 1 0 011 1v16a1 1 0 01-1 1H4a1 1 0 01-1-1V4Zm2 1v2h2V5H5Zm4 0v6h6V5H9Zm8 0v2h2V5h-2Zm2 4h-2v2h2V9Zm0 4h-2v2h2V13Zm0 4h-2V19h2ZM15 19v-6H9v6h6Zm-8 0v-2H5v2h2Zm-2-4h2v-2H5v2Zm0-4h2V9H5v2Z',
})
+302
View File
@@ -0,0 +1,302 @@
import React from 'react'
import {deviceLocales} from '#/locale/deviceLocales'
import {useGeolocation} from '#/state/geolocation'
import {useLanguagePrefs} from '#/state/preferences'
/**
* From react-native-localize
*
* MIT License
* Copyright (c) 2017-present, Mathieu Acthernoene
*
* @see https://github.com/zoontek/react-native-localize/blob/master/LICENSE
* @see https://github.com/zoontek/react-native-localize/blob/ee5bf25e0bb8f3b8e4f3fd055f67ad46269c81ea/src/constants.ts
*/
export const countryCodeToCurrency: Record<string, string> = {
ad: 'eur',
ae: 'aed',
af: 'afn',
ag: 'xcd',
ai: 'xcd',
al: 'all',
am: 'amd',
an: 'ang',
ao: 'aoa',
ar: 'ars',
as: 'usd',
at: 'eur',
au: 'aud',
aw: 'awg',
ax: 'eur',
az: 'azn',
ba: 'bam',
bb: 'bbd',
bd: 'bdt',
be: 'eur',
bf: 'xof',
bg: 'bgn',
bh: 'bhd',
bi: 'bif',
bj: 'xof',
bl: 'eur',
bm: 'bmd',
bn: 'bnd',
bo: 'bob',
bq: 'usd',
br: 'brl',
bs: 'bsd',
bt: 'btn',
bv: 'nok',
bw: 'bwp',
by: 'byn',
bz: 'bzd',
ca: 'cad',
cc: 'aud',
cd: 'cdf',
cf: 'xaf',
cg: 'xaf',
ch: 'chf',
ci: 'xof',
ck: 'nzd',
cl: 'clp',
cm: 'xaf',
cn: 'cny',
co: 'cop',
cr: 'crc',
cu: 'cup',
cv: 'cve',
cw: 'ang',
cx: 'aud',
cy: 'eur',
cz: 'czk',
de: 'eur',
dj: 'djf',
dk: 'dkk',
dm: 'xcd',
do: 'dop',
dz: 'dzd',
ec: 'usd',
ee: 'eur',
eg: 'egp',
eh: 'mad',
er: 'ern',
es: 'eur',
et: 'etb',
fi: 'eur',
fj: 'fjd',
fk: 'fkp',
fm: 'usd',
fo: 'dkk',
fr: 'eur',
ga: 'xaf',
gb: 'gbp',
gd: 'xcd',
ge: 'gel',
gf: 'eur',
gg: 'gbp',
gh: 'ghs',
gi: 'gip',
gl: 'dkk',
gm: 'gmd',
gn: 'gnf',
gp: 'eur',
gq: 'xaf',
gr: 'eur',
gs: 'gbp',
gt: 'gtq',
gu: 'usd',
gw: 'xof',
gy: 'gyd',
hk: 'hkd',
hm: 'aud',
hn: 'hnl',
hr: 'hrk',
ht: 'htg',
hu: 'huf',
id: 'idr',
ie: 'eur',
il: 'ils',
im: 'gbp',
in: 'inr',
io: 'usd',
iq: 'iqd',
ir: 'irr',
is: 'isk',
it: 'eur',
je: 'gbp',
jm: 'jmd',
jo: 'jod',
jp: 'jpy',
ke: 'kes',
kg: 'kgs',
kh: 'khr',
ki: 'aud',
km: 'kmf',
kn: 'xcd',
kp: 'kpw',
kr: 'krw',
kw: 'kwd',
ky: 'kyd',
kz: 'kzt',
la: 'lak',
lb: 'lbp',
lc: 'xcd',
li: 'chf',
lk: 'lkr',
lr: 'lrd',
ls: 'lsl',
lt: 'eur',
lu: 'eur',
lv: 'eur',
ly: 'lyd',
ma: 'mad',
mc: 'eur',
md: 'mdl',
me: 'eur',
mf: 'eur',
mg: 'mga',
mh: 'usd',
mk: 'mkd',
ml: 'xof',
mm: 'mmk',
mn: 'mnt',
mo: 'mop',
mp: 'usd',
mq: 'eur',
mr: 'mro',
ms: 'xcd',
mt: 'eur',
mu: 'mur',
mv: 'mvr',
mw: 'mwk',
mx: 'mxn',
my: 'myr',
mz: 'mzn',
na: 'nad',
nc: 'xpf',
ne: 'xof',
nf: 'aud',
ng: 'ngn',
ni: 'nio',
nl: 'eur',
no: 'nok',
np: 'npr',
nr: 'aud',
nu: 'nzd',
nz: 'nzd',
om: 'omr',
pa: 'pab',
pe: 'pen',
pf: 'xpf',
pg: 'pgk',
ph: 'php',
pk: 'pkr',
pl: 'pln',
pm: 'eur',
pn: 'nzd',
pr: 'usd',
ps: 'ils',
pt: 'eur',
pw: 'usd',
py: 'pyg',
qa: 'qar',
re: 'eur',
ro: 'ron',
rs: 'rsd',
ru: 'rub',
rw: 'rwf',
sa: 'sar',
sb: 'sbd',
sc: 'scr',
sd: 'sdg',
se: 'sek',
sg: 'sgd',
sh: 'shp',
si: 'eur',
sj: 'nok',
sk: 'eur',
sl: 'sll',
sm: 'eur',
sn: 'xof',
so: 'sos',
sr: 'srd',
ss: 'ssp',
st: 'std',
sv: 'svc',
sx: 'ang',
sy: 'syp',
sz: 'szl',
tc: 'usd',
td: 'xaf',
tf: 'eur',
tg: 'xof',
th: 'thb',
tj: 'tjs',
tk: 'nzd',
tl: 'usd',
tm: 'tmt',
tn: 'tnd',
to: 'top',
tr: 'try',
tt: 'ttd',
tv: 'aud',
tw: 'twd',
tz: 'tzs',
ua: 'uah',
ug: 'ugx',
um: 'usd',
us: 'usd',
uy: 'uyu',
uz: 'uzs',
va: 'eur',
vc: 'xcd',
ve: 'vef',
vg: 'usd',
vi: 'usd',
vn: 'vnd',
vu: 'vuv',
wf: 'xpf',
ws: 'wst',
ye: 'yer',
yt: 'eur',
za: 'zar',
zm: 'zmw',
zw: 'zwl',
}
/**
* Best-guess currency formatting.
*
* Attempts to use `getLocales` from `expo-localization` if available,
* otherwise falls back to the `persisted.appLanguage` setting, and geolocation
* API for region.
*/
export function useFormatCurrency(
options?: Parameters<typeof Intl.NumberFormat>[1],
) {
const {geolocation} = useGeolocation()
const {appLanguage} = useLanguagePrefs()
return React.useMemo(() => {
const locale = deviceLocales.at(0)
const languageTag = locale?.languageTag || appLanguage || 'en-US'
const countryCode = (
locale?.regionCode ||
geolocation?.countryCode ||
'us'
).toLowerCase()
const currency = countryCodeToCurrency[countryCode] || 'usd'
const format = new Intl.NumberFormat(languageTag, {
...(options || {}),
style: 'currency',
currency: currency,
}).format
return {
format,
currency,
countryCode,
languageTag,
}
}, [geolocation, appLanguage, options])
}
+11 -1
View File
@@ -177,7 +177,14 @@ export function useNotificationsHandler() {
Notifications.setNotificationHandler({
handleNotification: async e => {
if (e.request.trigger.type !== 'push') return DEFAULT_HANDLER_OPTIONS
if (
e.request.trigger == null ||
typeof e.request.trigger !== 'object' ||
!('type' in e.request.trigger) ||
e.request.trigger.type !== 'push'
) {
return DEFAULT_HANDLER_OPTIONS
}
logger.debug(
'Notifications: received',
@@ -220,6 +227,9 @@ export function useNotificationsHandler() {
if (
e.actionIdentifier === Notifications.DEFAULT_ACTION_IDENTIFIER &&
e.notification.request.trigger != null &&
typeof e.notification.request.trigger === 'object' &&
'type' in e.notification.request.trigger &&
e.notification.request.trigger.type === 'push'
) {
logger.debug(
+10
View File
@@ -2,10 +2,13 @@ import {useCallback} from 'react'
import {Linking} from 'react-native'
import * as WebBrowser from 'expo-web-browser'
import {logEvent} from '#/lib/statsig/statsig'
import {
createBskyAppAbsoluteUrl,
isBskyAppUrl,
isBskyRSSUrl,
isRelativeUrl,
toNiceDomain,
} from '#/lib/strings/url-helpers'
import {isNative} from '#/platform/detection'
import {useModalControls} from '#/state/modals'
@@ -25,6 +28,13 @@ export function useOpenLink() {
url = createBskyAppAbsoluteUrl(url)
}
if (!isBskyAppUrl(url)) {
logEvent('link:clicked', {
domain: toNiceDomain(url),
url,
})
}
if (isNative && !url.startsWith('mailto:')) {
if (override === undefined && enabled === undefined) {
openModal({
+2 -2
View File
@@ -1,5 +1,5 @@
import {Linking} from 'react-native'
import {Camera} from 'expo-camera/legacy' // TODO: Migrate to the new one.
import {useCameraPermissions as useExpoCameraPermissions} from 'expo-camera'
import * as MediaLibrary from 'expo-media-library'
import {isWeb} from '#/platform/detection'
@@ -78,7 +78,7 @@ export function useVideoLibraryPermission() {
}
export function useCameraPermission() {
const [res, requestPermission] = Camera.useCameraPermissions()
const [res, requestPermission] = useExpoCameraPermissions()
const requestCameraAccessIfNeeded = async () => {
if (res?.granted) {
-31
View File
@@ -1,31 +0,0 @@
import {useEffect} from 'react'
import {isWeb} from '#/platform/detection'
let refCount = 0
function incrementRefCount() {
if (refCount === 0) {
document.body.style.overflow = 'hidden'
document.documentElement.style.scrollbarGutter = 'auto'
}
refCount++
}
function decrementRefCount() {
refCount--
if (refCount === 0) {
document.body.style.overflow = ''
document.documentElement.style.scrollbarGutter = ''
}
}
export function useWebBodyScrollLock(isLockActive: boolean) {
useEffect(() => {
if (!isWeb || !isLockActive) {
return
}
incrementRefCount()
return () => decrementRefCount()
})
}
+4 -6
View File
@@ -80,12 +80,6 @@ export type LogEvents = {
feedUrl: string
feedType: string
index: number
reason:
| 'focus'
| 'tabbar-click'
| 'pager-swipe'
| 'desktop-sidebar-click'
| 'starter-pack-initial-feed'
}
'feed:endReached': {
feedUrl: string
@@ -217,6 +211,10 @@ export type LogEvents = {
'starterPack:opened': {
starterPack: string
}
'link:clicked': {
url: string
domain: string
}
'feed:interstitial:profileCard:press': {}
'feed:interstitial:feedCard:press': {}
+2 -2
View File
@@ -43,7 +43,7 @@ interface AppLanguageConfig {
export const APP_LANGUAGES: AppLanguageConfig[] = [
{code2: AppLanguage.en, name: 'English'},
{code2: AppLanguage.an, name: 'Aragonés Aragonese'},
{code2: AppLanguage.ast, name: 'Asturianu - Asturian'},
{code2: AppLanguage.ast, name: 'Asturianu Asturian'},
{code2: AppLanguage.ca, name: 'Català Catalan'},
{code2: AppLanguage.de, name: 'Deutsch German'},
{code2: AppLanguage.en_GB, name: 'English (UK)'},
@@ -51,7 +51,7 @@ export const APP_LANGUAGES: AppLanguageConfig[] = [
{code2: AppLanguage.fi, name: 'Suomi Finnish'},
{code2: AppLanguage.fr, name: 'Français French'},
{code2: AppLanguage.ga, name: 'Gaeilge Irish'},
{code2: AppLanguage.gl, name: 'Galego - Galician'},
{code2: AppLanguage.gl, name: 'Galego Galician'},
{code2: AppLanguage.hi, name: 'हिंदी Hindi'},
{code2: AppLanguage.hu, name: 'magyar Hungarian'},
{code2: AppLanguage.id, name: 'Bahasa Indonesia Indonesian'},
+7 -14
View File
@@ -17,13 +17,13 @@ import {
} from '#/state/session'
import {useSetMinimalShellMode} from '#/state/shell'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {ScrollView} from '#/view/com/util/Views'
import {Logo} from '#/view/icons/Logo'
import {atoms as a, useTheme} from '#/alf'
import {AccountList} from '#/components/AccountList'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {Divider} from '#/components/Divider'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import * as Layout from '#/components/Layout'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
@@ -104,24 +104,17 @@ export function Deactivated() {
}, [_, agent, setPending, setError, queryClient])
return (
<View style={[a.util_screen_outer, a.flex_1, t.atoms.bg]}>
<ScrollView
style={[
a.h_full,
a.w_full,
<View style={[a.util_screen_outer, a.flex_1]}>
<Layout.Content
contentContainerStyle={[
a.px_2xl,
{
paddingTop: isWeb ? 64 : insets.top + 16,
paddingBottom: isWeb ? 64 : insets.bottom,
},
]}
contentContainerStyle={[
a.w_full,
a.flex_row,
a.justify_center,
{borderWidth: 0},
]}>
<View style={[a.w_full, {maxWidth: COL_WIDTH}]}>
<View
style={[a.w_full, {marginHorizontal: 'auto', maxWidth: COL_WIDTH}]}>
<View style={[a.w_full, a.justify_center, a.align_center, a.pb_5xl]}>
<Logo width={40} />
</View>
@@ -218,7 +211,7 @@ export function Deactivated() {
</>
)}
</View>
</ScrollView>
</Layout.Content>
</View>
)
}
+30 -41
View File
@@ -1,5 +1,5 @@
import React from 'react'
import {ListRenderItemInfo, Pressable, View} from 'react-native'
import {ListRenderItemInfo, View} from 'react-native'
import {PostView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -13,16 +13,15 @@ import {shareUrl} from '#/lib/sharing'
import {cleanError} from '#/lib/strings/errors'
import {sanitizeHandle} from '#/lib/strings/handles'
import {enforceLen} from '#/lib/strings/helpers'
import {isNative, isWeb} from '#/platform/detection'
import {useSearchPostsQuery} from '#/state/queries/search-posts'
import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell'
import {Pager} from '#/view/com/pager/Pager'
import {TabBar} from '#/view/com/pager/TabBar'
import {Post} from '#/view/com/post/Post'
import {List} from '#/view/com/util/List'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {CenteredView} from '#/view/com/util/Views'
import {ArrowOutOfBox_Stroke2_Corner0_Rounded} from '#/components/icons/ArrowOutOfBox'
import {atoms as a, web} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
import * as Layout from '#/components/Layout'
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
@@ -110,46 +109,36 @@ export default function HashtagScreen({
return (
<Layout.Screen>
<CenteredView sideBorders={true}>
<ViewHeader
showOnDesktop
title={headerTitle}
subtitle={author ? _(msg`From @${sanitizedAuthor}`) : undefined}
canGoBack
renderButton={
isNative
? () => (
<Pressable
accessibilityRole="button"
onPress={onShare}
hitSlop={HITSLOP_10}>
<ArrowOutOfBox_Stroke2_Corner0_Rounded
size="lg"
onPress={onShare}
/>
</Pressable>
)
: undefined
}
/>
</CenteredView>
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>{headerTitle}</Layout.Header.TitleText>
{author && (
<Layout.Header.SubtitleText>
{_(msg`From @${sanitizedAuthor}`)}
</Layout.Header.SubtitleText>
)}
</Layout.Header.Content>
<Layout.Header.Slot>
<Button
label={_(msg`Share`)}
size="small"
variant="ghost"
color="primary"
shape="round"
onPress={onShare}
hitSlop={HITSLOP_10}
style={[{right: -3}]}>
<ButtonIcon icon={Share} size="md" />
</Button>
</Layout.Header.Slot>
</Layout.Header.Outer>
<Pager
onPageSelected={onPageSelected}
renderTabBar={props => (
<CenteredView
sideBorders={true}
// @ts-ignore web only
style={
isWeb
? {
position: isWeb ? 'sticky' : '',
top: 0,
zIndex: 1,
}
: undefined
}>
<Layout.Center style={web([a.sticky, a.z_10, {top: 0}])}>
<TabBar items={sections.map(section => section.title)} {...props} />
</CenteredView>
</Layout.Center>
)}
initialPage={0}>
{sections.map((section, i) => (
+55 -105
View File
@@ -16,8 +16,6 @@ import {MESSAGE_SCREEN_POLL_INTERVAL} from '#/state/messages/convo/const'
import {useMessagesEventBus} from '#/state/messages/events'
import {useListConvosQuery} from '#/state/queries/messages/list-converations'
import {List} from '#/view/com/util/List'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {CenteredView} from '#/view/com/util/Views'
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {DialogControlProps, useDialogControl} from '#/components/Dialog'
@@ -49,7 +47,6 @@ export function MessagesScreen({navigation, route}: Props) {
const {_} = useLingui()
const t = useTheme()
const newChatControl = useDialogControl()
const {gtMobile} = useBreakpoints()
const pushToConversation = route.params?.pushToConversation
// Whenever we have `pushToConversation` set, it means we pressed a notification for a chat without being on
@@ -81,21 +78,6 @@ export function MessagesScreen({navigation, route}: Props) {
}, [messagesBus, isActive]),
)
const renderButton = useCallback(() => {
return (
<Link
to="/messages/settings"
label={_(msg`Chat settings`)}
size="small"
variant="ghost"
color="secondary"
shape="square"
style={[a.justify_center]}>
<SettingsSlider size="md" style={[t.atoms.text_contrast_medium]} />
</Link>
)
}, [_, t])
const initialNumToRender = useInitialNumToRender({minItemHeight: 80})
const [isPTRing, setIsPTRing] = useState(false)
@@ -144,28 +126,11 @@ export function MessagesScreen({navigation, route}: Props) {
[navigation],
)
const onNavigateToSettings = useCallback(() => {
navigation.navigate('MessagesSettings')
}, [navigation])
if (conversations.length < 1) {
return (
<Layout.Screen>
<CenteredView sideBorders={gtMobile} style={[a.h_full_vh]}>
{gtMobile ? (
<DesktopHeader
newChatControl={newChatControl}
onNavigateToSettings={onNavigateToSettings}
/>
) : (
<ViewHeader
title={_(msg`Messages`)}
renderButton={renderButton}
showBorder
canGoBack={false}
/>
)}
<Header newChatControl={newChatControl} />
<Layout.Center>
{isLoading ? (
<View style={[a.align_center, a.pt_3xl, web({paddingTop: '10vh'})]}>
<Loader size="xl" />
@@ -227,7 +192,7 @@ export function MessagesScreen({navigation, route}: Props) {
)}
</>
)}
</CenteredView>
</Layout.Center>
{!isLoading && !isError && (
<NewChat onNewChat={onNewChat} control={newChatControl} />
@@ -238,14 +203,7 @@ export function MessagesScreen({navigation, route}: Props) {
return (
<Layout.Screen testID="messagesScreen">
{!gtMobile && (
<ViewHeader
title={_(msg`Messages`)}
renderButton={renderButton}
showBorder
canGoBack={false}
/>
)}
<Header newChatControl={newChatControl} />
<NewChat onNewChat={onNewChat} control={newChatControl} />
<List
data={conversations}
@@ -254,12 +212,6 @@ export function MessagesScreen({navigation, route}: Props) {
refreshing={isPTRing}
onRefresh={onRefresh}
onEndReached={onEndReached}
ListHeaderComponent={
<DesktopHeader
newChatControl={newChatControl}
onNavigateToSettings={onNavigateToSettings}
/>
}
ListFooterComponent={
<ListFooter
isFetchingNextPage={isFetchingNextPage}
@@ -276,67 +228,65 @@ export function MessagesScreen({navigation, route}: Props) {
windowSize={11}
// @ts-ignore our .web version only -sfn
desktopFixedHeight
sideBorders={false}
/>
</Layout.Screen>
)
}
function DesktopHeader({
newChatControl,
onNavigateToSettings,
}: {
newChatControl: DialogControlProps
onNavigateToSettings: () => void
}) {
const t = useTheme()
function Header({newChatControl}: {newChatControl: DialogControlProps}) {
const {_} = useLingui()
const {gtMobile, gtTablet} = useBreakpoints()
const {gtMobile} = useBreakpoints()
if (!gtMobile) {
return null
}
const settingsLink = (
<Link
to="/messages/settings"
label={_(msg`Chat settings`)}
size="small"
variant="ghost"
color="secondary"
shape="square"
style={[a.justify_center]}>
<ButtonIcon icon={SettingsSlider} size="md" />
</Link>
)
return (
<View
style={[
t.atoms.bg,
a.flex_row,
a.align_center,
a.justify_between,
a.gap_lg,
a.px_lg,
a.pr_md,
a.py_sm,
a.border_b,
t.atoms.border_contrast_low,
]}>
<Text style={[a.text_2xl, a.font_bold]}>
<Trans>Messages</Trans>
</Text>
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
<Button
label={_(msg`Message settings`)}
color="secondary"
size="small"
variant="ghost"
shape="square"
onPress={onNavigateToSettings}>
<SettingsSlider size="md" style={[t.atoms.text_contrast_medium]} />
</Button>
{gtTablet && (
<Button
label={_(msg`New chat`)}
color="primary"
size="small"
variant="solid"
onPress={newChatControl.open}>
<ButtonIcon icon={Plus} position="left" />
<ButtonText>
<Trans>New chat</Trans>
</ButtonText>
</Button>
)}
</View>
</View>
<Layout.Header.Outer>
{gtMobile ? (
<>
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>Messages</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
{settingsLink}
<Button
label={_(msg`New chat`)}
color="primary"
size="small"
variant="solid"
onPress={newChatControl.open}>
<ButtonIcon icon={Plus} position="left" />
<ButtonText>
<Trans>New chat</Trans>
</ButtonText>
</Button>
</View>
</>
) : (
<>
<Layout.Header.MenuButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>Messages</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
<Layout.Header.Slot>{settingsLink}</Layout.Header.Slot>
</>
)}
</Layout.Header.Outer>
)
}
+4 -5
View File
@@ -17,7 +17,6 @@ import {useCurrentConvoId} from '#/state/messages/current-convo-id'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useProfileQuery} from '#/state/queries/profile'
import {useSetMinimalShellMode} from '#/state/shell'
import {CenteredView} from '#/view/com/util/Views'
import {MessagesList} from '#/screens/Messages/components/MessagesList'
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
import {useDialogControl} from '#/components/Dialog'
@@ -97,7 +96,7 @@ function Inner() {
if (convoState.status === ConvoStatus.Error) {
return (
<CenteredView style={[a.flex_1]} sideBorders>
<Layout.Center style={[a.flex_1]}>
<MessagesListHeader />
<Error
title={_(msg`Something went wrong`)}
@@ -105,12 +104,12 @@ function Inner() {
onRetry={() => convoState.error.retry()}
sideBorders={false}
/>
</CenteredView>
</Layout.Center>
)
}
return (
<CenteredView style={[a.flex_1]} sideBorders>
<Layout.Center style={[a.flex_1]}>
{!readyToShow && <MessagesListHeader />}
<View style={[a.flex_1]}>
{moderationOpts && recipient ? (
@@ -140,7 +139,7 @@ function Inner() {
</View>
)}
</View>
</CenteredView>
</Layout.Center>
)
}
+11 -5
View File
@@ -10,8 +10,6 @@ import {useUpdateActorDeclaration} from '#/state/queries/messages/actor-declarat
import {useProfileQuery} from '#/state/queries/profile'
import {useSession} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {ScrollView} from '#/view/com/util/Views'
import {atoms as a} from '#/alf'
import {Admonition} from '#/components/Admonition'
import {Divider} from '#/components/Divider'
@@ -57,8 +55,16 @@ export function MessagesSettingsScreen({}: Props) {
return (
<Layout.Screen testID="messagesSettingsScreen">
<ScrollView stickyHeaderIndices={[0]}>
<ViewHeader title={_(msg`Chat Settings`)} showOnDesktop showBorder />
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>Chat Settings</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content>
<View style={[a.p_lg, a.gap_md]}>
<Text style={[a.text_lg, a.font_bold]}>
<Trans>Allow new messages from</Trans>
@@ -142,7 +148,7 @@ export function MessagesSettingsScreen({}: Props) {
</>
)}
</View>
</ScrollView>
</Layout.Content>
</Layout.Screen>
)
}
+13 -34
View File
@@ -1,6 +1,5 @@
import React from 'react'
import {Fragment, useCallback} from 'react'
import {Linking, View} from 'react-native'
import {useSafeAreaFrame} from 'react-native-safe-area-context'
import {LABELS} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -19,8 +18,6 @@ import {
import {isNonConfigurableModerationAuthority} from '#/state/session/additional-moderation-authorities'
import {useSetMinimalShellMode} from '#/state/shell'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {CenteredView} from '#/view/com/util/Views'
import {ScrollView} from '#/view/com/util/Views'
import {atoms as a, useBreakpoints, useTheme, ViewStyleProp} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
@@ -37,6 +34,7 @@ import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Perso
import * as LabelingService from '#/components/LabelingServiceCard'
import * as Layout from '#/components/Layout'
import {InlineLinkText, Link} from '#/components/Link'
import {ListMaybePlaceholder} from '#/components/Lists'
import {Loader} from '#/components/Loader'
import {GlobalLabelPreference} from '#/components/moderation/LabelPreference'
import {Text} from '#/components/Typography'
@@ -75,35 +73,22 @@ function ErrorState({error}: {error: string}) {
export function ModerationScreen(
_props: NativeStackScreenProps<CommonNavigatorParams, 'Moderation'>,
) {
const t = useTheme()
const {_} = useLingui()
const {
isLoading: isPreferencesLoading,
error: preferencesError,
data: preferences,
} = usePreferencesQuery()
const {gtMobile} = useBreakpoints()
const {height} = useSafeAreaFrame()
const isLoading = isPreferencesLoading
const error = preferencesError
return (
<Layout.Screen testID="moderationScreen">
<CenteredView
testID="moderationScreen"
style={[
t.atoms.border_contrast_low,
t.atoms.bg,
{minHeight: height},
...(gtMobile ? [a.border_l, a.border_r] : []),
]}>
<ViewHeader title={_(msg`Moderation`)} showOnDesktop />
<ViewHeader title={_(msg`Moderation`)} showOnDesktop />
<Layout.Content>
{isLoading ? (
<View style={[a.w_full, a.align_center, a.pt_2xl]}>
<Loader size="xl" fill={t.atoms.text.color} />
</View>
<ListMaybePlaceholder isLoading={true} sideBorders={false} />
) : error || !preferences ? (
<ErrorState
error={
@@ -114,7 +99,7 @@ export function ModerationScreen(
) : (
<ModerationScreenInner preferences={preferences} />
)}
</CenteredView>
</Layout.Content>
</Layout.Screen>
)
}
@@ -169,7 +154,7 @@ export function ModerationScreenInner({
} = useMyLabelersQuery()
useFocusEffect(
React.useCallback(() => {
useCallback(() => {
setMinimalShellMode(false)
}, [setMinimalShellMode]),
)
@@ -183,7 +168,7 @@ export function ModerationScreenInner({
const ageNotSet = !preferences.userAge
const isUnderage = (preferences.userAge || 0) < 18
const onToggleAdultContentEnabled = React.useCallback(
const onToggleAdultContentEnabled = useCallback(
async (selected: boolean) => {
try {
await setAdultContentPref({
@@ -201,13 +186,7 @@ export function ModerationScreenInner({
const disabledOnIOS = isIOS && !adultContentEnabled
return (
<ScrollView
contentContainerStyle={[
a.border_0,
a.pt_2xl,
a.px_lg,
gtMobile && a.px_2xl,
]}>
<View style={[a.pt_2xl, a.px_lg, gtMobile && a.px_2xl]}>
<Text
style={[a.text_md, a.font_bold, a.pb_md, t.atoms.text_contrast_high]}>
<Trans>Moderation tools</Trans>
@@ -420,7 +399,7 @@ export function ModerationScreenInner({
<View style={[a.rounded_sm, t.atoms.bg_contrast_25]}>
{labelers.map((labeler, i) => {
return (
<React.Fragment key={labeler.creator.did}>
<Fragment key={labeler.creator.did}>
{i !== 0 && <Divider />}
<LabelingService.Link labeler={labeler}>
{state => (
@@ -457,12 +436,12 @@ export function ModerationScreenInner({
</LabelingService.Outer>
)}
</LabelingService.Link>
</React.Fragment>
</Fragment>
)
})}
</View>
)}
<View style={{height: 200}} />
</ScrollView>
<View style={{height: 150}} />
</View>
)
}
+2 -4
View File
@@ -1,13 +1,11 @@
import React from 'react'
import {View} from 'react-native'
import Animated from 'react-native-reanimated'
import {ScrollView, View} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {isWeb} from '#/platform/detection'
import {useOnboardingDispatch} from '#/state/shell'
import {ScrollView} from '#/view/com/util/Views'
import {Context} from '#/screens/Onboarding/state'
import {
atoms as a,
@@ -36,7 +34,7 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
const {gtMobile} = useBreakpoints()
const onboardDispatch = useOnboardingDispatch()
const {state, dispatch} = React.useContext(Context)
const scrollview = React.useRef<Animated.ScrollView>(null)
const scrollview = React.useRef<ScrollView>(null)
const prevActiveStep = React.useRef<string>(state.activeStep)
React.useEffect(() => {
@@ -14,7 +14,6 @@ import {Lab_Stroke2_Corner0_Rounded as Lab} from '#/components/icons/Lab'
import {Leaf_Stroke2_Corner0_Rounded as Leaf} from '#/components/icons/Leaf'
import {MusicNote_Stroke2_Corner0_Rounded as MusicNote} from '#/components/icons/MusicNote'
import {PiggyBank_Stroke2_Corner0_Rounded as PiggyBank} from '#/components/icons/PiggyBank'
import {Pizza_Stroke2_Corner0_Rounded as Pizza} from '#/components/icons/Pizza'
import {Poop_Stroke2_Corner0_Rounded as Poop} from '#/components/icons/Poop'
import {Rose_Stroke2_Corner0_Rounded as Rose} from '#/components/icons/Rose'
import {Shaka_Stroke2_Corner0_Rounded as Shaka} from '#/components/icons/Shaka'
@@ -38,7 +37,6 @@ export const emojiNames = [
'gameController',
'leaf',
'musicNote',
'pizza',
'rose',
'shaka',
'ufo',
@@ -99,10 +97,6 @@ export const emojiItems: Record<EmojiName, Emoji> = {
name: 'musicNote',
component: MusicNote,
},
pizza: {
name: 'pizza',
component: Pizza,
},
rose: {
name: 'rose',
component: Rose,
+2 -8
View File
@@ -5,13 +5,10 @@ import {useFocusEffect} from '@react-navigation/native'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {makeRecordUri} from '#/lib/strings/url-helpers'
import {isWeb} from '#/platform/detection'
import {useSetMinimalShellMode} from '#/state/shell'
import {PostLikedBy as PostLikedByComponent} from '#/view/com/post-thread/PostLikedBy'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {CenteredView} from '#/view/com/util/Views'
import * as Layout from '#/components/Layout'
import {ListHeaderDesktop} from '#/components/Lists'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostLikedBy'>
export const PostLikedByScreen = ({route}: Props) => {
@@ -28,11 +25,8 @@ export const PostLikedByScreen = ({route}: Props) => {
return (
<Layout.Screen>
<CenteredView sideBorders={true}>
<ListHeaderDesktop title={_(msg`Liked By`)} />
<ViewHeader title={_(msg`Liked By`)} showBorder={!isWeb} />
<PostLikedByComponent uri={uri} />
</CenteredView>
<ViewHeader title={_(msg`Liked By`)} />
<PostLikedByComponent uri={uri} />
</Layout.Screen>
)
}
-2
View File
@@ -11,7 +11,6 @@ import {PostQuotes as PostQuotesComponent} from '#/view/com/post-thread/PostQuot
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {CenteredView} from '#/view/com/util/Views'
import * as Layout from '#/components/Layout'
import {ListHeaderDesktop} from '#/components/Lists'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostQuotes'>
export const PostQuotesScreen = ({route}: Props) => {
@@ -29,7 +28,6 @@ export const PostQuotesScreen = ({route}: Props) => {
return (
<Layout.Screen>
<CenteredView sideBorders={true}>
<ListHeaderDesktop title={_(msg`Quotes`)} />
<ViewHeader title={_(msg`Quotes`)} showBorder={!isWeb} />
<PostQuotesComponent uri={uri} />
</CenteredView>
-2
View File
@@ -11,7 +11,6 @@ import {PostRepostedBy as PostRepostedByComponent} from '#/view/com/post-thread/
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {CenteredView} from '#/view/com/util/Views'
import * as Layout from '#/components/Layout'
import {ListHeaderDesktop} from '#/components/Lists'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostRepostedBy'>
export const PostRepostedByScreen = ({route}: Props) => {
@@ -29,7 +28,6 @@ export const PostRepostedByScreen = ({route}: Props) => {
return (
<Layout.Screen>
<CenteredView sideBorders={true}>
<ListHeaderDesktop title={_(msg`Reposted By`)} />
<ViewHeader title={_(msg`Reposted By`)} showBorder={!isWeb} />
<PostRepostedByComponent uri={uri} />
</CenteredView>
+19 -10
View File
@@ -15,14 +15,22 @@ import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
import {List} from '#/view/com/util/List'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import * as Layout from '#/components/Layout'
import {
ListFooter,
ListHeaderDesktop,
ListMaybePlaceholder,
} from '#/components/Lists'
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
function renderItem({item}: {item: AppBskyActorDefs.ProfileViewBasic}) {
return <ProfileCardWithFollowBtn key={item.did} profile={item} />
function renderItem({
item,
index,
}: {
item: AppBskyActorDefs.ProfileViewBasic
index: number
}) {
return (
<ProfileCardWithFollowBtn
key={item.did}
profile={item}
noBorder={index === 0}
/>
)
}
function keyExtractor(item: AppBskyActorDefs.ProfileViewBasic) {
@@ -93,6 +101,7 @@ export const ProfileKnownFollowersScreen = ({route}: Props) => {
if (followers.length < 1) {
return (
<Layout.Screen>
<ViewHeader title={_(msg`Followers you know`)} />
<ListMaybePlaceholder
isLoading={isDidLoading || isFollowersLoading}
isError={isError}
@@ -100,6 +109,8 @@ export const ProfileKnownFollowersScreen = ({route}: Props) => {
emptyMessage={_(msg`You don't follow any users who follow @${name}.`)}
errorMessage={cleanError(resolveError || error)}
onRetry={isError ? refetch : undefined}
topBorder={false}
sideBorders={false}
/>
</Layout.Screen>
)
@@ -116,9 +127,6 @@ export const ProfileKnownFollowersScreen = ({route}: Props) => {
onRefresh={onRefresh}
onEndReached={onEndReached}
onEndReachedThreshold={4}
ListHeaderComponent={
<ListHeaderDesktop title={_(msg`Followers you know`)} />
}
ListFooterComponent={
<ListFooter
isFetchingNextPage={isFetchingNextPage}
@@ -130,6 +138,7 @@ export const ProfileKnownFollowersScreen = ({route}: Props) => {
desktopFixedHeight
initialNumToRender={initialNumToRender}
windowSize={11}
sideBorders={false}
/>
</Layout.Screen>
)
+4 -3
View File
@@ -15,10 +15,11 @@ import {isLabelerSubscribed, lookupLabelValueDefinition} from '#/lib/moderation'
import {useScrollHandlers} from '#/lib/ScrollContext'
import {isNative} from '#/platform/detection'
import {ListRef} from '#/view/com/util/List'
import {CenteredView, ScrollView} from '#/view/com/util/Views'
import {ScrollView} from '#/view/com/util/Views'
import {atoms as a, useTheme} from '#/alf'
import {Divider} from '#/components/Divider'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import * as Layout from '#/components/Layout'
import {Loader} from '#/components/Loader'
import {LabelerLabelPreference} from '#/components/moderation/LabelPreference'
import {Text} from '#/components/Typography'
@@ -75,7 +76,7 @@ export const ProfileLabelsSection = React.forwardRef<
}, [isFocused, scrollElRef, setScrollViewTag])
return (
<CenteredView style={{flex: 1, minHeight}} sideBorders>
<Layout.Center style={{flex: 1, minHeight}}>
{isLabelerLoading ? (
<View style={[a.w_full, a.align_center]}>
<Loader size="xl" />
@@ -95,7 +96,7 @@ export const ProfileLabelsSection = React.forwardRef<
headerHeight={headerHeight}
/>
)}
</CenteredView>
</Layout.Center>
)
})
+9 -1
View File
@@ -21,7 +21,15 @@ export function AboutSettingsScreen({}: Props) {
return (
<Layout.Screen>
<Layout.Header title={_(msg`About`)} />
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>About</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content>
<SettingsList.Container>
<SettingsList.LinkItem
@@ -39,7 +39,15 @@ export function AccessibilitySettingsScreen({}: Props) {
return (
<Layout.Screen>
<Layout.Header title={_(msg`Accessibility`)} />
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>Accessibility</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content>
<SettingsList.Container>
<SettingsList.Group contentContainerStyle={[a.gap_sm]}>
+9 -1
View File
@@ -38,7 +38,15 @@ export function AccountSettingsScreen({}: Props) {
return (
<Layout.Screen>
<Layout.Header title={_(msg`Account`)} />
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>Account</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content>
<SettingsList.Container>
<SettingsList.Item>
+10 -2
View File
@@ -1,7 +1,7 @@
import React from 'react'
import {Alert, View} from 'react-native'
import {Image} from 'expo-image'
import {msg} from '@lingui/macro'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import * as AppIcon from '@mozzius/expo-dynamic-app-icon'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
@@ -20,7 +20,15 @@ export function AppIconSettingsScreen({}: Props) {
return (
<Layout.Screen>
<Layout.Header title={_('App Icon')} />
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>App Icon</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content
contentContainerStyle={[a.py_2xl, a.px_xl, {paddingBottom: 100}]}>
<Text style={[a.text_lg, a.font_heavy]}>Defaults</Text>
+9 -1
View File
@@ -44,7 +44,15 @@ export function AppPasswordsScreen({}: Props) {
return (
<Layout.Screen testID="AppPasswordsScreen">
<Layout.Header title={_(msg`App Passwords`)} />
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>App Passwords</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content>
{error ? (
<ErrorScreen
+9 -1
View File
@@ -79,7 +79,15 @@ export function AppearanceSettingsScreen({}: Props) {
return (
<LayoutAnimationConfig skipExiting skipEntering>
<Layout.Screen testID="preferencesThreadsScreen">
<Layout.Header title={_(msg`Appearance`)} />
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>Appearance</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content>
<SettingsList.Container>
<AppearanceToggleButtonGroup
@@ -32,7 +32,15 @@ export function ContentAndMediaSettingsScreen({}: Props) {
return (
<Layout.Screen>
<Layout.Header title={_(msg`Content and Media`)} />
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>Content & Media</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content>
<SettingsList.Container>
<SettingsList.LinkItem
@@ -1,7 +1,6 @@
import {Fragment} from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/macro'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {
@@ -23,10 +22,17 @@ type Props = NativeStackScreenProps<
'PreferencesExternalEmbeds'
>
export function ExternalMediaPreferencesScreen({}: Props) {
const {_} = useLingui()
return (
<Layout.Screen testID="externalMediaPreferencesScreen">
<Layout.Header title={_(msg`External Media Preferences`)} />
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>External Media Preferences</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content>
<SettingsList.Container>
<SettingsList.Item>
@@ -46,7 +46,15 @@ export function FollowingFeedPreferencesScreen({}: Props) {
return (
<Layout.Screen testID="followingFeedPreferencesScreen">
<Layout.Header title={_(msg`Following Feed Preferences`)} />
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>Following Feed Preferences</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content>
<SettingsList.Container>
<SettingsList.Item>
+9 -1
View File
@@ -64,7 +64,15 @@ export function LanguageSettingsScreen({}: Props) {
return (
<Layout.Screen testID="PreferencesLanguagesScreen">
<Layout.Header title={_(msg`Languages`)} />
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>Languages</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content>
<SettingsList.Container>
<SettingsList.Group iconInset={false}>
@@ -33,7 +33,15 @@ export function NotificationSettingsScreen({}: Props) {
return (
<Layout.Screen>
<Layout.Header title={_(msg`Notification Settings`)} />
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>Notification Settings</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content>
{isQueryError ? (
<Error
@@ -29,7 +29,15 @@ export function PrivacyAndSecuritySettingsScreen({}: Props) {
return (
<Layout.Screen>
<Layout.Header title={_(msg`Privacy and Security`)} />
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>Privacy and Security</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content>
<SettingsList.Container>
<SettingsList.Item>
+9 -1
View File
@@ -73,7 +73,15 @@ export function SettingsScreen({}: Props) {
return (
<Layout.Screen>
<Layout.Header title={_(msg`Settings`)} />
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>Settings</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content>
<SettingsList.Container>
<View

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