merge main

This commit is contained in:
Hailey
2024-06-11 23:42:59 -07:00
101 changed files with 9138 additions and 7743 deletions
+15 -4
View File
@@ -36,12 +36,15 @@ appId: xyz.blueskyweb.app
id: "viewHeaderDrawerBtn"
- tapOn:
id: "menuItemButton-Feeds"
- tapOn: "Edit Saved Feeds"
- tapOn:
id: "editFeedsBtn"
- tapOn:
label: "Tap on down arrow"
point: "79%,23%"
- tapOn:
id: "bottomBarHomeBtn"
id: "viewHeaderDrawerBtn"
- tapOn:
id: "viewHeaderDrawerBtn"
- assertVisible:
id: "homeScreenFeedTabs-selector-0"
text: "alice-favs"
@@ -54,11 +57,15 @@ appId: xyz.blueskyweb.app
id: "viewHeaderDrawerBtn"
- tapOn:
id: "menuItemButton-Feeds"
- tapOn:
id: "editFeedsBtn"
- tapOn:
label: "Tap on down arrow"
point: "79%,23%"
- tapOn:
id: "bottomBarHomeBtn"
id: "viewHeaderDrawerBtn"
- tapOn:
id: "viewHeaderDrawerBtn"
- assertVisible:
id: "homeScreenFeedTabs-selector-0"
text: "Following"
@@ -71,11 +78,15 @@ appId: xyz.blueskyweb.app
id: "viewHeaderDrawerBtn"
- tapOn:
id: "menuItemButton-Feeds"
- tapOn:
id: "editFeedsBtn"
- tapOn:
label: "Tap on unpin"
point: "91%,23%"
- tapOn:
id: "bottomBarHomeBtn"
id: "viewHeaderDrawerBtn"
- tapOn:
id: "viewHeaderDrawerBtn"
- assertVisible:
id: "homeScreenFeedTabs-selector-0"
text: "alice-favs"
+2
View File
@@ -28,6 +28,8 @@ appId: xyz.blueskyweb.app
- tapOn: "Pin to Home"
- tapOn:
id: "bottomBarHomeBtn"
- tapOn:
id: "viewHeaderDrawerBtn"
- assertNotVisible: "Feeds ✨"
- tapOn:
+2 -2
View File
@@ -58,7 +58,7 @@ appId: xyz.blueskyweb.app
id: "repostBtn"
childOf:
id: "postThreadItem-by-bob.test"
- tapOn: "Undo repost"
- tapOn: "Remove repost"
- assertNotVisible:
id: "repostCount-expanded"
@@ -77,7 +77,7 @@ appId: xyz.blueskyweb.app
id: "repostBtn"
childOf:
id: "postThreadItem-by-carla.test"
- tapOn: "Undo repost"
- tapOn: "Remove repost"
- assertNotVisible:
id: "repostCount"
childOf:
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M4 5a1 1 0 0 0 0 2h16a1 1 0 1 0 0-2H4Zm0 12a1 1 0 1 0 0 2h3a1 1 0 1 0 0-2H4Zm-1-5a1 1 0 0 1 1-1h5a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1Zm14-3c.552 0 1 .41 1 .917V13.5h3.583c.507 0 .917.448.917 1s-.41 1-.917 1H18v3.583c0 .507-.448.917-1 .917s-1-.41-1-.917V15.5h-3.583c-.507 0-.917-.448-.917-1s.41-1 .917-1H16V9.917C16 9.41 16.448 9 17 9Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 471 B

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

After

Width:  |  Height:  |  Size: 299 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M6.5 3a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v3.997a6.25 6.25 0 0 0 1.83 4.42l.377.376A1 1 0 0 1 20 12.5V15a1 1 0 0 1-1 1h-6v5a1 1 0 1 1-2 0v-5H5a1 1 0 0 1-1-1v-2.5a1 1 0 0 1 .293-.707l.376-.377A6.25 6.25 0 0 0 6.5 6.996V3.001Zm2 1v2.997a8.25 8.25 0 0 1-2.416 5.834L6 12.914V14h12v-1.086l-.084-.083A8.25 8.25 0 0 1 15.5 6.997V4h-7Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 465 B

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

After

Width:  |  Height:  |  Size: 590 B

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

After

Width:  |  Height:  |  Size: 335 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 1a1 1 0 0 1 .902.568l2.643 5.517 6.085.799a1 1 0 0 1 .557 1.718l-4.45 4.207 1.118 6.008a1 1 0 0 1-1.46 1.063L12 17.962 6.604 20.88a1 1 0 0 1-1.459-1.063l1.117-6.008-4.45-4.207a1 1 0 0 1 .558-1.718l6.085-.8 2.643-5.516A1 1 0 0 1 12 1Zm0 3.315-1.975 4.123a1 1 0 0 1-.772.56l-4.538.595 3.317 3.137a1 1 0 0 1 .296.91l-.834 4.485 4.03-2.179a1 1 0 0 1 .952 0l4.03 2.179-.834-4.485a1 1 0 0 1 .296-.91l3.317-3.137-4.538-.596a1 1 0 0 1-.772-.56L12 4.316Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 592 B

+1
View File
@@ -207,6 +207,7 @@ func serve(cctx *cli.Context) error {
e.GET("/profile/:handleOrDID", server.WebProfile)
e.GET("/profile/:handleOrDID/follows", server.WebGeneric)
e.GET("/profile/:handleOrDID/followers", server.WebGeneric)
e.GET("/profile/:handleOrDID/known-followers", server.WebGeneric)
e.GET("/profile/:handleOrDID/lists/:rkey", server.WebGeneric)
e.GET("/profile/:handleOrDID/feed/:rkey", server.WebGeneric)
e.GET("/profile/:handleOrDID/feed/:rkey/liked-by", server.WebGeneric)
@@ -1,6 +0,0 @@
{
"platforms": ["ios"],
"ios": {
"modules": ["ExpoBlueskyTranslateModule"]
}
}
-6
View File
@@ -1,6 +0,0 @@
export {
isAvailable,
isLanguageSupported,
NativeTranslationModule,
NativeTranslationView,
} from './src/ExpoBlueskyTranslateView'
@@ -1,20 +0,0 @@
import ExpoModulesCore
import SwiftUI
// Thanks to Andrew Levy for this code snippet
// https://github.com/andrew-levy/swiftui-react-native/blob/d3fbb2abf07601ff0d4b83055e7717bb980910d6/ios/Common/ExpoView%2BUIHostingController.swift
extension ExpoView {
func setupHostingController(_ hostingController: UIHostingController<some View>) {
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
hostingController.view.backgroundColor = .clear
addSubview(hostingController.view)
NSLayoutConstraint.activate([
hostingController.view.topAnchor.constraint(equalTo: self.topAnchor),
hostingController.view.bottomAnchor.constraint(equalTo: self.bottomAnchor),
hostingController.view.leftAnchor.constraint(equalTo: self.leftAnchor),
hostingController.view.rightAnchor.constraint(equalTo: self.rightAnchor),
])
}
}
@@ -1,21 +0,0 @@
Pod::Spec.new do |s|
s.name = 'ExpoBlueskyTranslate'
s.version = '1.0.0'
s.summary = 'Uses SwiftUI translation to translate text.'
s.description = 'Uses SwiftUI translation to translate text.'
s.author = ''
s.homepage = 'https://docs.expo.dev/modules/'
s.platforms = { :ios => '13.4' }
s.source = { git: '' }
s.static_framework = true
s.dependency 'ExpoModulesCore'
# Swift/Objective-C compatibility
s.pod_target_xcconfig = {
'DEFINES_MODULE' => 'YES',
'SWIFT_COMPILATION_MODE' => 'wholemodule'
}
s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
end
@@ -1,18 +0,0 @@
import ExpoModulesCore
import Foundation
import SwiftUI
public class ExpoBlueskyTranslateModule: Module {
public func definition() -> ModuleDefinition {
Name("ExpoBlueskyTranslate")
AsyncFunction("presentAsync") { (text: String) in
DispatchQueue.main.async { [weak state = TranslateViewState.shared] in
state?.isPresented = true
state?.text = text
}
}
View(ExpoBlueskyTranslateView.self) {}
}
}
@@ -1,22 +0,0 @@
import ExpoModulesCore
import Foundation
import SwiftUI
class TranslateViewState: ObservableObject {
static var shared = TranslateViewState()
@Published var isPresented = false
@Published var text = ""
}
class ExpoBlueskyTranslateView: ExpoView {
required init(appContext: AppContext? = nil) {
if #available(iOS 14.0, *) {
let hostingController = UIHostingController(rootView: TranslateView())
super.init(appContext: appContext)
setupHostingController(hostingController)
} else {
super.init(appContext: appContext)
}
}
}
@@ -1,31 +0,0 @@
import SwiftUI
// conditionally import the Translation module
#if canImport(Translation)
import Translation
#endif
struct TranslateView: View {
@ObservedObject var state = TranslateViewState.shared
var body: some View {
if #available(iOS 17.4, *) {
VStack {
UIViewRepresentableWrapper(view: UIView(frame: .zero))
}
.translationPresentation(
isPresented: $state.isPresented,
text: state.text
)
}
}
}
struct UIViewRepresentableWrapper: UIViewRepresentable {
let view: UIView
func makeUIView(context: Context) -> UIView {
return view
}
func updateUIView(_ uiView: UIView, context: Context) {}
}
@@ -1,3 +0,0 @@
export type ExpoBlueskyTranslateModule = {
presentAsync: (text: string) => Promise<void>
}
@@ -1,51 +0,0 @@
import React from 'react'
import {Platform} from 'react-native'
import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core'
import {ExpoBlueskyTranslateModule} from './ExpoBlueskyTranslate.types'
export const NativeTranslationModule =
requireNativeModule<ExpoBlueskyTranslateModule>('ExpoBlueskyTranslate')
const NativeView: React.ComponentType = requireNativeViewManager(
'ExpoBlueskyTranslate',
)
export function NativeTranslationView() {
return <NativeView />
}
// can be something like "17.5.1", so just take the first two parts
const version = String(Platform.Version).split('.').slice(0, 2).join('.')
export const isAvailable = Number(version) >= 17.4
// https://en.wikipedia.org/wiki/Translate_(Apple)#Languages
const SUPPORTED_LANGUAGES = [
'ar',
'zh',
'zh',
'nl',
'en',
'en',
'fr',
'de',
'id',
'it',
'ja',
'ko',
'pl',
'pt',
'ru',
'es',
'th',
'tr',
'uk',
'vi',
]
export function isLanguageSupported(lang?: string) {
// If the language is not provided, we assume it is supported
if (!lang) return true
return SUPPORTED_LANGUAGES.includes(lang)
}
@@ -1,13 +0,0 @@
export const NativeTranslationModule = {
presentAsync: async (_: string) => {},
}
export function NativeTranslationView() {
return null
}
export const isAvailable = false
export function isLanguageSupported(_lang?: string) {
return false
}
+28 -3
View File
@@ -1,5 +1,5 @@
diff --git a/node_modules/expo-notifications/android/build.gradle b/node_modules/expo-notifications/android/build.gradle
index d233e1f..cc2f856 100644
index b863077..8b5209e 100644
--- a/node_modules/expo-notifications/android/build.gradle
+++ b/node_modules/expo-notifications/android/build.gradle
@@ -32,6 +32,7 @@ dependencies {
@@ -10,6 +10,31 @@ index d233e1f..cc2f856 100644
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
@@ -46,7 +71,7 @@ index 0af7fe0..8f2c8d8 100644
try {
return payload.getString(TITLE_KEY);
diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java
index f1fed19..80afe9e 100644
index f1fed19..012757b 100644
--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java
+++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java
@@ -20,6 +20,7 @@ import expo.modules.notifications.notifications.enums.NotificationPriority;
@@ -63,7 +88,7 @@ index f1fed19..80afe9e 100644
+ @Nullable
+ public String getChannelId() {
+ return mTitle;
+ return mChannelId;
+ }
+
@Nullable
@@ -7,3 +7,8 @@ in `onMessageReceived` are sent to the module for handling.
It also allows us to set the Android notification channel ID from the notification `data`, rather
than the `notification` object in the payload.
### `setBadgeCountAsync` fix on Android
`ShortcutBadger`'s `setCount` doesn't work for clearing the badge on Android for some reason. Instead, let's use the
Android API for clearing the badge.
+1 -1
View File
@@ -143,7 +143,7 @@ function App() {
* that is set up in the InnerApp component above.
*/
return (
<KeyboardProvider enabled={true} statusBarTranslucent={true}>
<KeyboardProvider enabled={false} statusBarTranslucent={true}>
<SessionProvider>
<ShellStateProvider>
<PrefsStateProvider>
+9 -26
View File
@@ -22,7 +22,6 @@ import {buildStateObject} from 'lib/routes/helpers'
import {
AllNavigatorParams,
BottomTabNavigatorParams,
FeedsTabNavigatorParams,
FlatNavigatorParams,
HomeTabNavigatorParams,
MessagesTabNavigatorParams,
@@ -42,6 +41,7 @@ import {PreferencesThreads} from 'view/screens/PreferencesThreads'
import {SavedFeeds} from 'view/screens/SavedFeeds'
import HashtagScreen from '#/screens/Hashtag'
import {ModerationScreen} from '#/screens/Moderation'
import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers'
import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
import {LandingScreen} from '#/screens/StarterPack/Landing'
import {StarterPackScreen} from '#/screens/StarterPack/Main'
@@ -94,7 +94,6 @@ const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
const HomeTab = createNativeStackNavigatorWithAuth<HomeTabNavigatorParams>()
const SearchTab = createNativeStackNavigatorWithAuth<SearchTabNavigatorParams>()
const FeedsTab = createNativeStackNavigatorWithAuth<FeedsTabNavigatorParams>()
const NotificationsTab =
createNativeStackNavigatorWithAuth<NotificationsTabNavigatorParams>()
const MyProfileTab =
@@ -174,6 +173,13 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
title: title(msg`People followed by @${route.params.name}`),
})}
/>
<Stack.Screen
name="ProfileKnownFollowers"
getComponent={() => ProfileKnownFollowersScreen}
options={({route}) => ({
title: title(msg`Followers of @${route.params.name} that you know`),
})}
/>
<Stack.Screen
name="ProfileList"
getComponent={() => ProfileListScreen}
@@ -309,6 +315,7 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
getComponent={() => MessagesSettingsScreen}
options={{title: title(msg`Chat settings`), requireAuth: true}}
/>
<Stack.Screen name="Feeds" getComponent={() => FeedsScreen} />
<Stack.Screen
// @ts-ignore What the fuck?
name="StarterPack"
@@ -349,7 +356,6 @@ function TabsNavigator() {
tabBar={tabBar}>
<Tab.Screen name="HomeTab" getComponent={() => HomeTabNavigator} />
<Tab.Screen name="SearchTab" getComponent={() => SearchTabNavigator} />
<Tab.Screen name="FeedsTab" getComponent={() => FeedsTabNavigator} />
<Tab.Screen
name="NotificationsTab"
getComponent={() => NotificationsTabNavigator}
@@ -403,24 +409,6 @@ function SearchTabNavigator() {
)
}
function FeedsTabNavigator() {
const pal = usePalette('default')
return (
<FeedsTab.Navigator
screenOptions={{
animation: isAndroid ? 'ios' : undefined,
animationDuration: 285,
gestureEnabled: true,
fullScreenGestureEnabled: true,
headerShown: false,
contentStyle: pal.view,
}}>
<FeedsTab.Screen name="Feeds" getComponent={() => FeedsScreen} />
{commonScreens(FeedsTab as typeof HomeTab)}
</FeedsTab.Navigator>
)
}
function NotificationsTabNavigator() {
const pal = usePalette('default')
return (
@@ -524,11 +512,6 @@ const FlatNavigator = () => {
getComponent={() => SearchScreen}
options={{title: title(msg`Search`)}}
/>
<Flat.Screen
name="Feeds"
getComponent={() => FeedsScreen}
options={{title: title(msg`Feeds`)}}
/>
<Flat.Screen
name="Notifications"
getComponent={() => NotificationsScreen}
@@ -5,7 +5,7 @@ import Animated, {
useSharedValue,
} from 'react-native-reanimated'
export function KeyboardPadding({maxHeight}: {maxHeight?: number}) {
export function KeyboardControllerPadding({maxHeight}: {maxHeight?: number}) {
const keyboardHeight = useSharedValue(0)
useKeyboardHandler(
@@ -0,0 +1,7 @@
export function KeyboardControllerPadding({
maxHeight: _,
}: {
maxHeight?: number
}) {
return null
}
-3
View File
@@ -1,3 +0,0 @@
export function KeyboardPadding({maxHeight: _}: {maxHeight?: number}) {
return null
}
+200
View File
@@ -0,0 +1,200 @@
import React from 'react'
import {View} from 'react-native'
import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
import {msg, plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme} from '#/alf'
import {Link} from '#/components/Link'
import {Text} from '#/components/Typography'
const AVI_SIZE = 30
const AVI_BORDER = 1
/**
* Shared logic to determine if `KnownFollowers` should be shown.
*
* Checks the # of actual returned users instead of the `count` value, because
* `count` includes blocked users and `followers` does not.
*/
export function shouldShowKnownFollowers(
knownFollowers?: AppBskyActorDefs.KnownFollowers,
) {
return knownFollowers && knownFollowers.followers.length > 0
}
export function KnownFollowers({
profile,
moderationOpts,
}: {
profile: AppBskyActorDefs.ProfileViewDetailed
moderationOpts: ModerationOpts
}) {
const cache = React.useRef<Map<string, AppBskyActorDefs.KnownFollowers>>(
new Map(),
)
/*
* Results for `knownFollowers` are not sorted consistently, so when
* revalidating we can see a flash of this data updating. This cache prevents
* this happening for screens that remain in memory. When pushing a new
* screen, or once this one is popped, this cache is empty, so new data is
* displayed.
*/
if (profile.viewer?.knownFollowers && !cache.current.has(profile.did)) {
cache.current.set(profile.did, profile.viewer.knownFollowers)
}
const cachedKnownFollowers = cache.current.get(profile.did)
if (cachedKnownFollowers && shouldShowKnownFollowers(cachedKnownFollowers)) {
return (
<KnownFollowersInner
profile={profile}
cachedKnownFollowers={cachedKnownFollowers}
moderationOpts={moderationOpts}
/>
)
}
return null
}
function KnownFollowersInner({
profile,
moderationOpts,
cachedKnownFollowers,
}: {
profile: AppBskyActorDefs.ProfileViewDetailed
moderationOpts: ModerationOpts
cachedKnownFollowers: AppBskyActorDefs.KnownFollowers
}) {
const t = useTheme()
const {_} = useLingui()
const textStyle = [
a.flex_1,
a.text_sm,
a.leading_snug,
t.atoms.text_contrast_medium,
]
// list of users, minus blocks
const returnedCount = cachedKnownFollowers.followers.length
// db count, includes blocks
const fullCount = cachedKnownFollowers.count
// knownFollowers can return up to 5 users, but will exclude blocks
// therefore, if we have less 5 users, use whichever count is lower
const count =
returnedCount < 5 ? Math.min(fullCount, returnedCount) : fullCount
const slice = cachedKnownFollowers.followers.slice(0, 3).map(f => {
const moderation = moderateProfile(f, moderationOpts)
return {
profile: {
...f,
displayName: sanitizeDisplayName(
f.displayName || f.handle,
moderation.ui('displayName'),
),
},
moderation,
}
})
return (
<Link
label={_(
msg`Press to view followers of this account that you also follow`,
)}
to={makeProfileLink(profile, 'known-followers')}
style={[
a.flex_1,
a.flex_row,
a.gap_md,
a.align_center,
{marginLeft: -AVI_BORDER},
]}>
{({hovered, pressed}) => (
<>
<View
style={[
{
height: AVI_SIZE,
width: AVI_SIZE + (slice.length - 1) * a.gap_md.gap,
},
pressed && {
opacity: 0.5,
},
]}>
{slice.map(({profile: prof, moderation}, i) => (
<View
key={prof.did}
style={[
a.absolute,
a.rounded_full,
{
borderWidth: AVI_BORDER,
borderColor: t.atoms.bg.backgroundColor,
width: AVI_SIZE + AVI_BORDER * 2,
height: AVI_SIZE + AVI_BORDER * 2,
left: i * a.gap_md.gap,
zIndex: AVI_BORDER - i,
},
]}>
<UserAvatar
size={AVI_SIZE}
avatar={prof.avatar}
moderation={moderation.ui('avatar')}
/>
</View>
))}
</View>
<Text
style={[
textStyle,
hovered && {
textDecorationLine: 'underline',
textDecorationColor: t.atoms.text_contrast_medium.color,
},
pressed && {
opacity: 0.5,
},
]}
numberOfLines={2}>
<Trans>Followed by</Trans>{' '}
{count > 2 ? (
<>
{slice.slice(0, 2).map(({profile: prof}, i) => (
<Text key={prof.did} style={textStyle}>
{prof.displayName}
{i === 0 && ', '}
</Text>
))}
{', '}
{plural(count - 2, {
one: 'and # other',
other: 'and # others',
})}
</>
) : count === 2 ? (
slice.map(({profile: prof}, i) => (
<Text key={prof.did} style={textStyle}>
{prof.displayName} {i === 0 ? _(msg`and`) + ' ' : ''}
</Text>
))
) : (
<Text key={slice[0].profile.did} style={textStyle}>
{slice[0].profile.displayName}
</Text>
)}
</Text>
</>
)}
</Link>
)
}
@@ -15,7 +15,6 @@ import * as Dialog from '#/components/Dialog'
import * as Toggle from '#/components/forms/Toggle'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
import {KeyboardPadding} from '#/components/KeyboardPadding'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import {ReportDialogProps} from './types'
@@ -222,7 +221,6 @@ export function SubmitView({
{submitting && <ButtonIcon icon={Loader} />}
</Button>
</View>
<KeyboardPadding />
</View>
)
}
-2
View File
@@ -28,7 +28,6 @@ import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Has
import {PageText_Stroke2_Corner0_Rounded as PageText} from '#/components/icons/PageText'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {KeyboardPadding} from '#/components/KeyboardPadding'
import {Loader} from '#/components/Loader'
import * as Prompt from '#/components/Prompt'
import {Text} from '#/components/Typography'
@@ -257,7 +256,6 @@ function MutedWordsInner() {
</View>
<Dialog.Close />
<KeyboardPadding maxHeight={100} />
</Dialog.ScrollableInner>
)
}
+2
View File
@@ -16,6 +16,7 @@ import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {KeyboardControllerPadding} from '#/components/KeyboardControllerPadding'
import {Button, ButtonIcon, ButtonText} from '../Button'
import {Divider} from '../Divider'
import {ChevronLeft_Stroke2_Corner0_Rounded as Chevron} from '../icons/Chevron'
@@ -47,6 +48,7 @@ let ReportDialog = ({
<Dialog.ScrollableInner label={_(msg`Report this message`)}>
<DialogInner params={params} />
<Dialog.Close />
<KeyboardControllerPadding />
</Dialog.ScrollableInner>
</Dialog.Outer>
)
+9
View File
@@ -0,0 +1,9 @@
import {createSinglePathSVG} from './TEMPLATE'
/*
* This icon is off-menu, not part of the icon set
*/
export const ListPlus_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M4 5a1 1 0 0 0 0 2h16a1 1 0 1 0 0-2H4Zm0 12a1 1 0 1 0 0 2h3a1 1 0 1 0 0-2H4Zm-1-5a1 1 0 0 1 1-1h5a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1Zm14-3c.552 0 1 .41 1 .917V13.5h3.583c.507 0 .917.448.917 1s-.41 1-.917 1H18v3.583c0 .507-.448.917-1 .917s-1-.41-1-.917V15.5h-3.583c-.507 0-.917-.448-.917-1s.41-1 .917-1H16V9.917C16 9.41 16.448 9 17 9Z',
})
+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Menu_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M2 6a1 1 0 0 1 1-1h18a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1Zm0 6a1 1 0 0 1 1-1h18a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1Zm0 6a1 1 0 0 1 1-1h18a1 1 0 1 1 0 2H3a1 1 0 0 1-1-1Z',
})
+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Pin_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M6.5 3a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v3.997a6.25 6.25 0 0 0 1.83 4.42l.377.376A1 1 0 0 1 20 12.5V15a1 1 0 0 1-1 1h-6v5a1 1 0 1 1-2 0v-5H5a1 1 0 0 1-1-1v-2.5a1 1 0 0 1 .293-.707l.376-.377A6.25 6.25 0 0 0 6.5 6.996V3.001Zm2 1v2.997a8.25 8.25 0 0 1-2.416 5.834L6 12.914V14h12v-1.086l-.084-.083A8.25 8.25 0 0 1 15.5 6.997V4h-7Z',
})
+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Shapes_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M7 3a1 1 0 0 1 1 1v2h2a1 1 0 1 1 0 2H8v2a1 1 0 1 1-2 0V8H4a1 1 0 0 1 0-2h2V4a1 1 0 0 1 1-1Zm6 4a4 4 0 1 1 8 0 4 4 0 0 1-8 0Zm4-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4ZM3 14a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-6Zm2 1v4h4v-4H5Zm9.171-.829a1 1 0 0 1 1.415 0L17 15.585l1.414-1.414a1 1 0 1 1 1.414 1.414L18.414 17l1.414 1.414a1 1 0 0 1-1.414 1.414L17 18.414l-1.415 1.414a1 1 0 0 1-1.414-1.414l1.415-1.415-1.415-1.414a1 1 0 0 1 0-1.414Z',
})
+9
View File
@@ -0,0 +1,9 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Star_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M12 1a1 1 0 0 1 .902.568l2.643 5.517 6.085.799a1 1 0 0 1 .557 1.718l-4.45 4.207 1.118 6.008a1 1 0 0 1-1.46 1.063L12 17.962 6.604 20.88a1 1 0 0 1-1.459-1.063l1.117-6.008-4.45-4.207a1 1 0 0 1 .558-1.718l6.085-.8 2.643-5.516A1 1 0 0 1 12 1Zm0 3.315-1.975 4.123a1 1 0 0 1-.772.56l-4.538.595 3.317 3.137a1 1 0 0 1 .296.91l-.834 4.485 4.03-2.179a1 1 0 0 1 .952 0l4.03 2.179-.834-4.485a1 1 0 0 1 .296-.91l3.317-3.137-4.538-.596a1 1 0 0 1-.772-.56L12 4.316Z',
})
export const Star_Filled_Corner0_Rounded = createSinglePathSVG({
path: 'M12.902 1.568a1 1 0 0 0-1.804 0L8.455 7.085l-6.085.799a1 1 0 0 0-.557 1.718l4.45 4.207-1.117 6.008a1 1 0 0 0 1.458 1.063L12 17.962l5.396 2.918a1 1 0 0 0 1.459-1.063l-1.117-6.008 4.45-4.207a1 1 0 0 0-.558-1.718l-6.085-.8-2.643-5.516Z',
})
@@ -14,7 +14,6 @@ import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {KeyboardPadding} from '#/components/KeyboardPadding'
import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography'
import {Divider} from '../Divider'
@@ -110,7 +109,6 @@ function LabelsOnMeDialogInner(props: LabelsOnMeDialogProps) {
</>
)}
<Dialog.Close />
<KeyboardPadding />
</Dialog.ScrollableInner>
)
}
+1
View File
@@ -7,6 +7,7 @@ export function useNavigationTabState() {
const res = {
isAtHome: getTabState(state, 'Home') !== TabState.Outside,
isAtSearch: getTabState(state, 'Search') !== TabState.Outside,
// FeedsTab no longer exists, but this check works for `Feeds` screen as well
isAtFeeds: getTabState(state, 'Feeds') !== TabState.Outside,
isAtNotifications:
getTabState(state, 'Notifications') !== TabState.Outside,
+32 -3
View File
@@ -6,7 +6,7 @@ import {BskyAgent} from '@atproto/api'
import {logger} from '#/logger'
import {SessionAccount, useAgent, useSession} from '#/state/session'
import {logEvent, useGate} from 'lib/statsig/statsig'
import {devicePlatform, isNative} from 'platform/detection'
import {devicePlatform, isAndroid, isNative} from 'platform/detection'
import BackgroundNotificationHandler from '../../../modules/expo-background-notification-handler'
const SERVICE_DID = (serviceUrl?: string) =>
@@ -43,7 +43,7 @@ async function getPushToken(skipPermissionCheck = false) {
const granted =
skipPermissionCheck || (await Notifications.getPermissionsAsync()).granted
if (granted) {
Notifications.getDevicePushTokenAsync()
return Notifications.getDevicePushTokenAsync()
}
}
@@ -56,7 +56,22 @@ export function useNotificationsRegistration() {
return
}
// HACK - see https://github.com/bluesky-social/social-app/pull/4467
// An apparent regression in expo-notifications causes `addPushTokenListener` to not fire on Android whenever the
// token changes by calling `getPushToken()`. This is a workaround to ensure we register the token once it is
// generated on Android.
if (isAndroid) {
;(async () => {
const token = await getPushToken()
// Token will be undefined if we don't have notifications permission
if (token) {
registerPushToken(agent, currentAccount, token)
}
})()
} else {
getPushToken()
}
// According to the Expo docs, there is a chance that the token will change while the app is open in some rare
// cases. This will fire `registerPushToken` whenever that happens.
@@ -73,6 +88,7 @@ export function useNotificationsRegistration() {
export function useRequestNotificationsPermission() {
const gate = useGate()
const {currentAccount} = useSession()
const agent = useAgent()
return async (
context: 'StartOnboarding' | 'AfterOnboarding' | 'Login' | 'Home',
@@ -110,7 +126,20 @@ export function useRequestNotificationsPermission() {
if (res.granted) {
// This will fire a pushTokenEvent, which will handle registration of the token
getPushToken(true)
const token = await getPushToken(true)
// Same hack as above. We cannot rely on the `addPushTokenListener` to fire on Android due to an Expo bug, so we
// will manually register it here. Note that this will occur only:
// 1. right after the user signs in, leading to no `currentAccount` account being available - this will be instead
// picked up from the useEffect above on `currentAccount` change
// 2. right after onboarding. In this case, we _need_ this registration, since `currentAccount` will not change
// and we need to ensure the token is registered right after permission is granted. `currentAccount` will already
// be available in this case, so the registration will succeed.
// We should remove this once expo-notifications (and possibly FCMv1) is fixed and the `addPushTokenListener` is
// working again. See https://github.com/expo/expo/issues/28656
if (isAndroid && currentAccount && token) {
registerPushToken(agent, currentAccount, token)
}
}
}
}
+1 -1
View File
@@ -35,7 +35,7 @@ export function isStateAtTabRoot(state: State | undefined) {
return (
isTab(currentRoute.name, 'Home') ||
isTab(currentRoute.name, 'Search') ||
isTab(currentRoute.name, 'Feeds') ||
isTab(currentRoute.name, 'Messages') ||
isTab(currentRoute.name, 'Notifications') ||
isTab(currentRoute.name, 'MyProfile')
)
+2 -6
View File
@@ -15,6 +15,7 @@ export type CommonNavigatorParams = {
Profile: {name: string; hideBackButton?: boolean}
ProfileFollowers: {name: string}
ProfileFollows: {name: string}
ProfileKnownFollowers: {name: string}
ProfileList: {name: string; rkey: string}
PostThread: {name: string; rkey: string}
PostLikedBy: {name: string; rkey: string}
@@ -40,6 +41,7 @@ export type CommonNavigatorParams = {
Hashtag: {tag: string; author?: string}
MessagesConversation: {conversation: string; embed?: string}
MessagesSettings: undefined
Feeds: undefined
StarterPack: {name: string; rkey: string}
StarterPackLanding: {name: string; rkey: string}
StarterPackWizard: {
@@ -51,7 +53,6 @@ export type CommonNavigatorParams = {
export type BottomTabNavigatorParams = CommonNavigatorParams & {
HomeTab: undefined
SearchTab: undefined
FeedsTab: undefined
NotificationsTab: undefined
MyProfileTab: undefined
MessagesTab: undefined
@@ -65,10 +66,6 @@ export type SearchTabNavigatorParams = CommonNavigatorParams & {
Search: {q?: string}
}
export type FeedsTabNavigatorParams = CommonNavigatorParams & {
Feeds: undefined
}
export type NotificationsTabNavigatorParams = CommonNavigatorParams & {
Notifications: undefined
}
@@ -95,7 +92,6 @@ export type AllNavigatorParams = CommonNavigatorParams & {
Home: undefined
SearchTab: undefined
Search: {q?: string}
FeedsTab: undefined
Feeds: undefined
NotificationsTab: undefined
Notifications: undefined
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1
View File
@@ -15,6 +15,7 @@ export const router = new Router({
Profile: ['/profile/:name', '/profile/:name/rss'],
ProfileFollowers: '/profile/:name/followers',
ProfileFollows: '/profile/:name/follows',
ProfileKnownFollowers: '/profile/:name/known-followers',
ProfileList: '/profile/:name/lists/:rkey',
PostThread: '/profile/:name/post/:rkey',
PostLikedBy: '/profile/:name/post/:rkey/liked-by',
-14
View File
@@ -3,13 +3,10 @@ import {View} from 'react-native'
import {TID} from '@atproto/common-web'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {DISCOVER_SAVED_FEED, TIMELINE_SAVED_FEED} from '#/lib/constants'
import {isNative} from '#/platform/detection'
import {useOverwriteSavedFeedsMutation} from '#/state/queries/preferences'
import {UsePreferencesQueryResponse} from '#/state/queries/preferences'
import {NavigationProp} from 'lib/routes/types'
import {CenteredView} from '#/view/com/util/Views'
import {atoms as a} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
@@ -26,7 +23,6 @@ export function NoFeedsPinned({
}) {
const {_} = useLingui()
const headerOffset = useHeaderOffset()
const navigation = useNavigation<NavigationProp>()
const {isPending, mutateAsync: overwriteSavedFeeds} =
useOverwriteSavedFeedsMutation()
@@ -66,15 +62,6 @@ export function NoFeedsPinned({
await overwriteSavedFeeds(toSave)
}, [overwriteSavedFeeds, preferences.savedFeeds])
const onPressFeedsLink = React.useCallback(() => {
if (isNative) {
// Hack that's necessary due to how our navigators are set up.
navigation.navigate('FeedsTab')
navigation.popToTop()
return false
}
}, [navigation])
return (
<CenteredView sideBorders style={[a.h_full_vh]}>
<View
@@ -115,7 +102,6 @@ export function NoFeedsPinned({
<Link
label={_(msg`Browse other feeds`)}
to="/feeds"
onPress={onPressFeedsLink}
size="medium"
variant="solid"
color="secondary">
@@ -1,5 +1,6 @@
import React, {useCallback} from 'react'
import {View} from 'react-native'
import {useKeyboardController} from 'react-native-keyboard-controller'
import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -34,6 +35,17 @@ export function MessagesConversationScreen({route}: Props) {
const convoId = route.params.conversation
const {setCurrentConvoId} = useCurrentConvoId()
const {setEnabled} = useKeyboardController()
useFocusEffect(
useCallback(() => {
if (isWeb) return
setEnabled(true)
return () => {
setEnabled(false)
}
}, [setEnabled]),
)
useFocusEffect(
useCallback(() => {
setCurrentConvoId(convoId)
+9 -11
View File
@@ -20,6 +20,7 @@ import {useProfileShadow} from '#/state/cache/profile-shadow'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useSession} from '#/state/session'
import {useHaptics} from 'lib/haptics'
import {decrementBadgeCount} from 'lib/notifications/notifications'
import {logEvent} from 'lib/statsig/statsig'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {TimeElapsed} from '#/view/com/util/TimeElapsed'
@@ -177,14 +178,16 @@ function ChatListItemReady({
const onPress = useCallback(
(e: GestureResponderEvent) => {
decrementBadgeCount(convo.unreadCount)
if (isDeletedAccount) {
e.preventDefault()
menuControl.open()
return false
} else {
logEvent('chat:open', {logContext: 'ChatsList'})
}
},
[isDeletedAccount],
[convo.unreadCount, isDeletedAccount, menuControl],
)
const onLongPress = useCallback(() => {
@@ -206,7 +209,9 @@ function ChatListItemReady({
accessibilityHint={
!isDeletedAccount
? _(msg`Go to conversation with ${profile.handle}`)
: undefined
: _(
msg`This conversation is with a deleted or a deactivated account. Press for options.`,
)
}
accessibilityActions={
isNative
@@ -218,12 +223,7 @@ function ChatListItemReady({
}
onPress={onPress}
onLongPress={isNative ? onLongPress : undefined}
onAccessibilityAction={onLongPress}
style={[
web({
cursor: isDeletedAccount ? 'default' : 'pointer',
}),
]}>
onAccessibilityAction={onLongPress}>
{({hovered, pressed, focused}) => (
<View
style={[
@@ -233,9 +233,7 @@ function ChatListItemReady({
a.px_lg,
a.py_md,
a.gap_md,
(hovered || pressed || focused) &&
!isDeletedAccount &&
t.atoms.bg_contrast_25,
(hovered || pressed || focused) && t.atoms.bg_contrast_25,
t.atoms.border_contrast_low,
]}>
<UserAvatar
@@ -30,6 +30,10 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {MessageProfileButton} from '#/components/dms/MessageProfileButton'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {
KnownFollowers,
shouldShowKnownFollowers,
} from '#/components/KnownFollowers'
import * as Prompt from '#/components/Prompt'
import {RichText} from '#/components/RichText'
import {ProfileHeaderDisplayName} from './DisplayName'
@@ -268,6 +272,16 @@ let ProfileHeaderStandard = ({
/>
</View>
) : undefined}
{!isMe &&
shouldShowKnownFollowers(profile.viewer?.knownFollowers) && (
<View style={[a.flex_row, a.align_center, a.gap_sm, a.pt_md]}>
<KnownFollowers
profile={profile}
moderationOpts={moderationOpts}
/>
</View>
)}
</>
)}
</View>
+1 -1
View File
@@ -83,7 +83,7 @@ let ProfileHeaderShell = ({
{!isPlaceholderProfile && (
<View
style={[a.px_lg, a.pb_sm]}
style={[a.px_lg, a.py_xs]}
pointerEvents={isIOS ? 'auto' : 'box-none'}>
{isMe ? (
<LabelsOnMe details={{did: profile.did}} labels={profile.labels} />
+134
View File
@@ -0,0 +1,134 @@
import React from 'react'
import {View} from 'react-native'
import {AppBskyActorDefs} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {useProfileKnownFollowersQuery} from '#/state/queries/known-followers'
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {useSetMinimalShellMode} from '#/state/shell'
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
import {List} from '#/view/com/util/List'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {
ListFooter,
ListHeaderDesktop,
ListMaybePlaceholder,
} from '#/components/Lists'
function renderItem({item}: {item: AppBskyActorDefs.ProfileViewBasic}) {
return <ProfileCardWithFollowBtn key={item.did} profile={item} />
}
function keyExtractor(item: AppBskyActorDefs.ProfileViewBasic) {
return item.did
}
type Props = NativeStackScreenProps<
CommonNavigatorParams,
'ProfileKnownFollowers'
>
export const ProfileKnownFollowersScreen = ({route}: Props) => {
const {_} = useLingui()
const setMinimalShellMode = useSetMinimalShellMode()
const initialNumToRender = useInitialNumToRender()
const {name} = route.params
const [isPTRing, setIsPTRing] = React.useState(false)
const {
data: resolvedDid,
isLoading: isDidLoading,
error: resolveError,
} = useResolveDidQuery(route.params.name)
const {
data,
isLoading: isFollowersLoading,
isFetchingNextPage,
hasNextPage,
fetchNextPage,
error,
refetch,
} = useProfileKnownFollowersQuery(resolvedDid)
const onRefresh = React.useCallback(async () => {
setIsPTRing(true)
try {
await refetch()
} catch (err) {
logger.error('Failed to refresh followers', {message: err})
}
setIsPTRing(false)
}, [refetch, setIsPTRing])
const onEndReached = React.useCallback(async () => {
if (isFetchingNextPage || !hasNextPage || !!error) return
try {
await fetchNextPage()
} catch (err) {
logger.error('Failed to load more followers', {message: err})
}
}, [isFetchingNextPage, hasNextPage, error, fetchNextPage])
const followers = React.useMemo(() => {
if (data?.pages) {
return data.pages.flatMap(page => page.followers)
}
return []
}, [data])
const isError = Boolean(resolveError || error)
useFocusEffect(
React.useCallback(() => {
setMinimalShellMode(false)
}, [setMinimalShellMode]),
)
if (followers.length < 1) {
return (
<ListMaybePlaceholder
isLoading={isDidLoading || isFollowersLoading}
isError={isError}
emptyType="results"
emptyMessage={_(msg`You don't follow any users who follow @${name}.`)}
errorMessage={cleanError(resolveError || error)}
onRetry={isError ? refetch : undefined}
/>
)
}
return (
<View style={{flex: 1}}>
<ViewHeader title={_(msg`Followers you know`)} />
<List
data={followers}
renderItem={renderItem}
keyExtractor={keyExtractor}
refreshing={isPTRing}
onRefresh={onRefresh}
onEndReached={onEndReached}
onEndReachedThreshold={4}
ListHeaderComponent={
<ListHeaderDesktop title={_(msg`Followers you know`)} />
}
ListFooterComponent={
<ListFooter
isFetchingNextPage={isFetchingNextPage}
error={cleanError(error)}
onRetry={fetchNextPage}
/>
}
// @ts-ignore our .web version only -prf
desktopFixedHeight
initialNumToRender={initialNumToRender}
windowSize={11}
/>
</View>
)
}
+1 -1
View File
@@ -56,7 +56,7 @@ export const ProfileFeedSection = React.forwardRef<
}))
const renderPostsEmpty = React.useCallback(() => {
return <EmptyState icon="feed" message={_(msg`This feed is empty!`)} />
return <EmptyState icon="growth" message={_(msg`No posts yet.`)} />
}, [_])
React.useEffect(() => {
+4 -2
View File
@@ -169,11 +169,11 @@ const ModalContext = React.createContext<{
const ModalControlContext = React.createContext<{
openModal: (modal: Modal) => void
closeModal: () => boolean
closeAllModals: () => void
closeAllModals: () => boolean
}>({
openModal: () => {},
closeModal: () => false,
closeAllModals: () => {},
closeAllModals: () => false,
})
/**
@@ -206,7 +206,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
})
const closeAllModals = useNonReactiveCallback(() => {
let wasActive = activeModals.length > 0
setActiveModals([])
return wasActive
})
unstable__openModal = openModal
+1 -1
View File
@@ -28,7 +28,7 @@ export async function read(): Promise<Schema | undefined> {
code: e.code,
// @ts-ignore exists on some types
expected: e?.expected,
path: e.path,
path: e.path?.join('.'),
})) || []
logger.error(`persisted store: data failed validation on read`, {errors})
return undefined
+34
View File
@@ -0,0 +1,34 @@
import {AppBskyGraphGetKnownFollowers} from '@atproto/api'
import {InfiniteData, QueryKey, useInfiniteQuery} from '@tanstack/react-query'
import {useAgent} from '#/state/session'
const PAGE_SIZE = 50
type RQPageParam = string | undefined
const RQKEY_ROOT = 'profile-known-followers'
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
export function useProfileKnownFollowersQuery(did: string | undefined) {
const agent = useAgent()
return useInfiniteQuery<
AppBskyGraphGetKnownFollowers.OutputSchema,
Error,
InfiniteData<AppBskyGraphGetKnownFollowers.OutputSchema>,
QueryKey,
RQPageParam
>({
queryKey: RQKEY(did || ''),
async queryFn({pageParam}: {pageParam: RQPageParam}) {
const res = await agent.app.bsky.graph.getKnownFollowers({
actor: did!,
limit: PAGE_SIZE,
cursor: pageParam,
})
return res.data
},
initialPageParam: undefined,
getNextPageParam: lastPage => lastPage.cursor,
enabled: !!did,
})
}
+50 -2
View File
@@ -41,6 +41,8 @@ export interface ThreadCtx {
hasMore?: boolean
isParentLoading?: boolean
isChildLoading?: boolean
isSelfThread?: boolean
hasMoreSelfThread?: boolean
}
export type ThreadPost = {
@@ -88,9 +90,11 @@ export function usePostThreadQuery(uri: string | undefined) {
gcTime: 0,
queryKey: RQKEY(uri || ''),
async queryFn() {
const res = await agent.getPostThread({uri: uri!})
const res = await agent.getPostThread({uri: uri!, depth: 10})
if (res.success) {
return responseToThreadNodes(res.data.thread)
const thread = responseToThreadNodes(res.data.thread)
annotateSelfThread(thread)
return thread
}
return {type: 'unknown', uri: uri!}
},
@@ -234,6 +238,8 @@ function responseToThreadNodes(
isHighlightedPost: depth === 0,
hasMore:
direction === 'down' && !node.replies?.length && !!node.replyCount,
isSelfThread: false, // populated `annotateSelfThread`
hasMoreSelfThread: false, // populated in `annotateSelfThread`
},
}
} else if (AppBskyFeedDefs.isBlockedPost(node)) {
@@ -245,6 +251,48 @@ function responseToThreadNodes(
}
}
function annotateSelfThread(thread: ThreadNode) {
if (thread.type !== 'post') {
return
}
const selfThreadNodes: ThreadPost[] = [thread]
let parent: ThreadNode | undefined = thread.parent
while (parent) {
if (
parent.type !== 'post' ||
parent.post.author.did !== thread.post.author.did
) {
// not a self-thread
return
}
selfThreadNodes.unshift(parent)
parent = parent.parent
}
let node = thread
for (let i = 0; i < 10; i++) {
const reply = node.replies?.find(
r => r.type === 'post' && r.post.author.did === thread.post.author.did,
)
if (reply?.type !== 'post') {
break
}
selfThreadNodes.push(reply)
node = reply
}
if (selfThreadNodes.length > 1) {
for (const selfThreadNode of selfThreadNodes) {
selfThreadNode.ctx.isSelfThread = true
}
const last = selfThreadNodes[selfThreadNodes.length - 1]
if (last && last.post.replyCount && !last.replies?.length) {
last.ctx.hasMoreSelfThread = true
}
}
}
function findPostInQueryData(
queryClient: QueryClient,
uri: string,
+4 -1
View File
@@ -3,7 +3,7 @@ import {InfiniteData, QueryKey, useInfiniteQuery} from '@tanstack/react-query'
import {useAgent} from '#/state/session'
const PAGE_SIZE = 30
const PAGE_SIZE = 50
type RQPageParam = string | undefined
// TODO refactor invalidate on mutate?
@@ -30,6 +30,9 @@ export function useProfileFeedgensQuery(
limit: PAGE_SIZE,
cursor: pageParam,
})
res.data.feeds.sort((a, b) => {
return (b.likeCount || 0) - (a.likeCount || 0)
})
return res.data
},
initialPageParam: undefined,
+4 -3
View File
@@ -1,9 +1,10 @@
import {useCallback} from 'react'
import {useDialogStateControlContext} from '#/state/dialogs'
import {useLightboxControls} from './lightbox'
import {useModalControls} from './modals'
import {useComposerControls} from './shell/composer'
import {useSetDrawerOpen} from './shell/drawer-open'
import {useDialogStateControlContext} from '#/state/dialogs'
/**
* returns true if something was closed
@@ -22,10 +23,10 @@ export function useCloseAnyActiveElement() {
if (closeModal()) {
return true
}
if (closeComposer()) {
if (closeAllDialogs()) {
return true
}
if (closeAllDialogs()) {
if (closeComposer()) {
return true
}
setDrawerOpen(false)
+35 -69
View File
@@ -8,17 +8,14 @@ import React, {
} from 'react'
import {
ActivityIndicator,
BackHandler,
Keyboard,
KeyboardAvoidingView,
LayoutChangeEvent,
StyleSheet,
TouchableOpacity,
View,
} from 'react-native'
import {
KeyboardAvoidingView,
KeyboardStickyView,
useKeyboardContext,
} from 'react-native-keyboard-controller'
import Animated, {
interpolateColor,
useAnimatedStyle,
@@ -42,6 +39,7 @@ import {LikelyType} from '#/lib/link-meta/link-meta'
import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {emitPostCreated} from '#/state/events'
import {useModalControls} from '#/state/modals'
import {useModals} from '#/state/modals'
import {useRequireAltTextEnabled} from '#/state/preferences'
import {
@@ -108,9 +106,7 @@ export const ComposePost = observer(function ComposePost({
text: initText,
imageUris: initImageUris,
cancelRef,
isModalReady,
}: Props & {
isModalReady: boolean
cancelRef?: React.RefObject<CancelRef>
}) {
const {currentAccount} = useSession()
@@ -128,19 +124,9 @@ export const ComposePost = observer(function ComposePost({
const textInput = useRef<TextInputRef>(null)
const discardPromptControl = Prompt.usePromptControl()
const {closeAllDialogs} = useDialogStateControlContext()
const {closeAllModals} = useModalControls()
const t = useTheme()
// Disable this in the composer to prevent any extra keyboard height being applied.
// See https://github.com/bluesky-social/social-app/pull/4399
const {setEnabled} = useKeyboardContext()
React.useEffect(() => {
if (!isAndroid) return
setEnabled(false)
return () => {
setEnabled(true)
}
}, [setEnabled])
const [isKeyboardVisible] = useIsKeyboardVisible({iosUseWillEvents: true})
const [isProcessing, setIsProcessing] = useState(false)
const [processingState, setProcessingState] = useState('')
@@ -180,6 +166,7 @@ export const ComposePost = observer(function ComposePost({
const insets = useSafeAreaInsets()
const viewStyles = useMemo(
() => ({
paddingTop: isAndroid ? insets.top : 0,
paddingBottom:
isAndroid || (isIOS && !isKeyboardVisible) ? insets.bottom : 0,
}),
@@ -205,6 +192,26 @@ export const ComposePost = observer(function ComposePost({
useImperativeHandle(cancelRef, () => ({onPressCancel}))
// On Android, pressing Back should ask confirmation.
useEffect(() => {
if (!isAndroid) {
return
}
const backHandler = BackHandler.addEventListener(
'hardwareBackPress',
() => {
if (closeAllDialogs() || closeAllModals()) {
return true
}
onPressCancel()
return true
},
)
return () => {
backHandler.remove()
}
}, [onPressCancel, closeAllDialogs, closeAllModals])
// listen to escape key on desktop web
const onEscape = useCallback(
(e: KeyboardEvent) => {
@@ -408,48 +415,13 @@ export const ComposePost = observer(function ComposePost({
bottomBarAnimatedStyle,
} = useAnimatedBorders()
// Backup focus on android, if the keyboard *still* refuses to show
useEffect(() => {
if (!isAndroid) return
if (!isModalReady) return
function tryFocus() {
if (!Keyboard.isVisible()) {
textInput.current?.blur()
textInput.current?.focus()
}
}
tryFocus()
// Retry with enough gap to avoid interrupting the previous attempt.
// Unfortunately we don't know which attempt will succeed.
const retryInterval = setInterval(tryFocus, 500)
function stopTrying() {
clearInterval(retryInterval)
}
// Deactivate this fallback as soon as anything happens.
const sub1 = Keyboard.addListener('keyboardDidShow', stopTrying)
const sub2 = Keyboard.addListener('keyboardDidHide', stopTrying)
return () => {
clearInterval(retryInterval)
sub1.remove()
sub2.remove()
}
}, [isModalReady])
return (
<>
<KeyboardAvoidingView
testID="composePostView"
behavior="padding"
style={a.flex_1}
keyboardVerticalOffset={replyTo ? 115 : isAndroid ? 180 : 162}>
<View
style={[a.flex_1, viewStyles]}
aria-modal
accessibilityViewIsModal>
behavior={isIOS ? 'padding' : 'height'}
keyboardVerticalOffset={isIOS ? 70 : 0}
style={[a.flex_1]}>
<View style={[a.flex_1, viewStyles]} aria-modal accessibilityViewIsModal>
<Animated.View style={topBarAnimatedStyle}>
<View style={styles.topbarInner}>
<TouchableOpacity
@@ -567,11 +539,7 @@ export const ComposePost = observer(function ComposePost({
ref={textInput}
richtext={richtext}
placeholder={selectTextInputPlaceholder}
// fixes autofocus on android
key={
isAndroid ? (isModalReady ? 'ready' : 'animating') : 'static'
}
autoFocus={isAndroid ? isModalReady : true}
autoFocus
setRichText={setRichText}
onPhotoPasted={onPhotoPasted}
onPressPublish={onPressPublish}
@@ -603,8 +571,9 @@ export const ComposePost = observer(function ComposePost({
/>
</View>
)}
{quote ? (
<View style={[s.mt5, isWeb && s.mb10]}>
<View style={[s.mt5, s.mb2, isWeb && s.mb10]}>
<View style={{pointerEvents: 'none'}}>
<QuoteEmbed quote={quote} />
</View>
@@ -615,10 +584,7 @@ export const ComposePost = observer(function ComposePost({
) : undefined}
</Animated.ScrollView>
<SuggestedLanguage text={richtext.text} />
</View>
</KeyboardAvoidingView>
<KeyboardStickyView
offset={{closed: isIOS ? -insets.bottom : 0, opened: 0}}>
{replyTo ? null : (
<ThreadgateBtn
threadgate={threadgate}
@@ -657,7 +623,7 @@ export const ComposePost = observer(function ComposePost({
<SelectLangBtn />
<CharProgress count={graphemeLength} />
</View>
</KeyboardStickyView>
</View>
<Prompt.Basic
control={discardPromptControl}
title={_(msg`Discard draft?`)}
@@ -666,7 +632,7 @@ export const ComposePost = observer(function ComposePost({
confirmButtonCta={_(msg`Discard`)}
confirmButtonColor="negative"
/>
</>
</KeyboardAvoidingView>
)
})
-2
View File
@@ -20,7 +20,6 @@ import * as Dialog from '#/components/Dialog'
import * as TextField from '#/components/forms/TextField'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {KeyboardPadding} from '#/components/KeyboardPadding'
import {Text} from '#/components/Typography'
import {GifEmbed} from '../util/post-embeds/GifEmbed'
import {AltTextReminder} from './photos/Gallery'
@@ -181,7 +180,6 @@ function AltTextInner({
</View>
</View>
<Dialog.Close />
<KeyboardPadding />
</Dialog.ScrollableInner>
)
}
-6
View File
@@ -5,7 +5,6 @@ import {usePalette} from '#/lib/hooks/usePalette'
import {FeedSourceInfo} from '#/state/queries/feed'
import {useSession} from '#/state/session'
import {NavigationProp} from 'lib/routes/types'
import {isWeb} from 'platform/detection'
import {RenderTabBarFnProps} from 'view/com/pager/Pager'
import {TabBar} from '../pager/TabBar'
import {HomeHeaderLayout} from './HomeHeaderLayout'
@@ -39,12 +38,7 @@ export function HomeHeader(
}, [hasPinnedCustom, feeds])
const onPressFeedsLink = React.useCallback(() => {
if (isWeb) {
navigation.navigate('Feeds')
} else {
navigation.navigate('FeedsTab')
navigation.popToTop()
}
}, [navigation])
const onSelect = React.useCallback(
+41 -30
View File
@@ -1,22 +1,18 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import Animated from 'react-native-reanimated'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {CogIcon} from '#/lib/icons'
import {useSession} from '#/state/session'
import {useShellLayout} from '#/state/shell/shell-layout'
import {useMinimalShellHeaderTransform} from 'lib/hooks/useMinimalShellTransform'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {Logo} from '#/view/icons/Logo'
import {atoms as a, useTheme} from '#/alf'
import {Hashtag_Stroke2_Corner0_Rounded as FeedsIcon} from '#/components/icons/Hashtag'
import {Link} from '#/components/Link'
import {useKawaiiMode} from '../../../state/preferences/kawaii'
import {Link} from '../util/Link'
import {HomeHeaderLayoutMobile} from './HomeHeaderLayoutMobile'
export function HomeHeaderLayout(props: {
@@ -38,7 +34,7 @@ function HomeHeaderLayoutDesktopAndTablet({
children: React.ReactNode
tabBarAnchor: JSX.Element | null | undefined
}) {
const pal = usePalette('default')
const t = useTheme()
const headerMinimalShellTransform = useMinimalShellHeaderTransform()
const {headerHeight} = useShellLayout()
const {hasSession} = useSession()
@@ -51,31 +47,46 @@ function HomeHeaderLayoutDesktopAndTablet({
{hasSession && (
<View
style={[
pal.view,
pal.border,
a.relative,
a.flex_row,
a.justify_end,
a.align_center,
a.pt_lg,
a.px_md,
a.pb_2xs,
t.atoms.bg,
t.atoms.border_contrast_low,
styles.bar,
styles.topBar,
kawaii && {paddingTop: 4, paddingBottom: 0},
]}>
<Link
href="/settings/following-feed"
hitSlop={10}
accessibilityRole="button"
accessibilityLabel={_(msg`Following Feed Preferences`)}
accessibilityHint="">
<FontAwesomeIcon
icon="sliders"
style={pal.textLight as FontAwesomeIconStyle}
/>
</Link>
<View
style={[
a.absolute,
a.inset_0,
a.pt_lg,
a.m_auto,
kawaii && {paddingTop: 4, paddingBottom: 0},
{
width: kawaii ? 60 : 28,
},
]}>
<Logo width={kawaii ? 60 : 28} />
</View>
<Link
href="/settings/saved-feeds"
to="/feeds"
hitSlop={10}
accessibilityRole="button"
accessibilityLabel={_(msg`Edit Saved Feeds`)}
accessibilityHint={_(msg`Opens screen to edit Saved Feeds`)}>
<CogIcon size={22} strokeWidth={2} style={pal.textLight} />
label={_(msg`View your feeds and explore more`)}
size="small"
variant="ghost"
color="secondary"
shape="square"
style={[
a.justify_center,
{
marginTop: -4,
},
]}>
<FeedsIcon size="md" fill={t.atoms.text_contrast_medium.color} />
</Link>
</View>
)}
@@ -85,8 +96,8 @@ function HomeHeaderLayoutDesktopAndTablet({
headerHeight.value = e.nativeEvent.layout.height
}}
style={[
pal.view,
pal.border,
t.atoms.bg,
t.atoms.border_contrast_low,
styles.bar,
styles.tabBar,
headerMinimalShellTransform,
+25 -21
View File
@@ -1,8 +1,6 @@
import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
import Animated from 'react-native-reanimated'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -15,10 +13,13 @@ import {usePalette} from 'lib/hooks/usePalette'
import {isWeb} from 'platform/detection'
import {Logo} from '#/view/icons/Logo'
import {atoms} from '#/alf'
import {useTheme} from '#/alf'
import {atoms as a} from '#/alf'
import {ColorPalette_Stroke2_Corner0_Rounded as ColorPalette} from '#/components/icons/ColorPalette'
import {Link as Link2} from '#/components/Link'
import {Hashtag_Stroke2_Corner0_Rounded as FeedsIcon} from '#/components/icons/Hashtag'
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
import {Link} from '#/components/Link'
import {IS_DEV} from '#/env'
import {Link} from '../util/Link'
export function HomeHeaderLayoutMobile({
children,
@@ -26,6 +27,7 @@ export function HomeHeaderLayoutMobile({
children: React.ReactNode
tabBarAnchor: JSX.Element | null | undefined
}) {
const t = useTheme()
const pal = usePalette('default')
const {_} = useLingui()
const setDrawerOpen = useSetDrawerOpen()
@@ -54,11 +56,7 @@ export function HomeHeaderLayoutMobile({
msg`Access profile and other navigation links`,
)}
hitSlop={HITSLOP_10}>
<FontAwesomeIcon
icon="bars"
size={18}
color={pal.colors.textLight}
/>
<Menu size="lg" fill={t.atoms.text_contrast_medium.color} />
</TouchableOpacity>
</View>
<View>
@@ -74,22 +72,28 @@ export function HomeHeaderLayoutMobile({
{width: 100},
]}>
{IS_DEV && (
<Link2 to="/sys/debug">
<Link to="/sys/debug">
<ColorPalette size="md" />
</Link2>
</Link>
)}
{hasSession && (
<Link
testID="viewHeaderHomeFeedPrefsBtn"
href="/settings/following-feed"
to="/feeds"
hitSlop={HITSLOP_10}
accessibilityRole="button"
accessibilityLabel={_(msg`Following Feed Preferences`)}
accessibilityHint="">
<FontAwesomeIcon
icon="sliders"
style={pal.textLight as FontAwesomeIconStyle}
/>
label={_(msg`View your feeds and explore more`)}
size="small"
variant="ghost"
color="secondary"
shape="square"
style={[
a.justify_center,
{
marginTop: 2,
marginRight: -6,
},
]}>
<FeedsIcon size="lg" fill={t.atoms.text_contrast_medium.color} />
</Link>
)}
</View>
@@ -113,8 +117,8 @@ const styles = StyleSheet.create({
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 18,
paddingVertical: 8,
paddingHorizontal: 16,
paddingVertical: 5,
width: '100%',
},
title: {
-2
View File
@@ -22,7 +22,6 @@ import {Text} from '#/view/com/util/text/Text'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a} from '#/alf'
import * as Toggle from '#/components/forms/Toggle'
import {KeyboardPadding} from '#/components/KeyboardPadding'
export const snapPoints = ['90%']
@@ -246,7 +245,6 @@ export function Component({}: {}) {
onPress={!appPassword ? createAppPassword : onDone}
/>
</View>
<KeyboardPadding />
</View>
)
}
-2
View File
@@ -6,7 +6,6 @@ import BottomSheet from '@discord/bottom-sheet/src'
import {useModalControls, useModals} from '#/state/modals'
import {usePalette} from 'lib/hooks/usePalette'
import {FullWindowOverlay} from '#/components/FullWindowOverlay'
import {KeyboardPadding} from '#/components/KeyboardPadding'
import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop'
import * as AddAppPassword from './AddAppPasswords'
import * as AltImageModal from './AltImage'
@@ -147,7 +146,6 @@ export function ModalsContainer() {
handleStyle={[styles.handle, pal.view]}
onChange={onBottomSheetChange}>
{element}
<KeyboardPadding />
</BottomSheet>
</Container>
)
+4
View File
@@ -34,6 +34,7 @@ import {ComposePrompt} from '../composer/Prompt'
import {List, ListMethods} from '../util/List'
import {ViewHeader} from '../util/ViewHeader'
import {PostThreadItem} from './PostThreadItem'
import {PostThreadLoadMore} from './PostThreadLoadMore'
import {PostThreadShowHiddenReplies} from './PostThreadShowHiddenReplies'
// FlatList maintainVisibleContentPosition breaks if too many items
@@ -364,6 +365,9 @@ export function PostThread({
</View>
)
} else if (isThreadPost(item)) {
if (!treeView && item.ctx.hasMoreSelfThread) {
return <PostThreadLoadMore post={item.post} />
}
const prev = isThreadPost(posts[index - 1])
? (posts[index - 1] as ThreadPost)
: undefined
+1 -18
View File
@@ -30,11 +30,6 @@ import {useSession} from 'state/session'
import {PostThreadFollowBtn} from 'view/com/post-thread/PostThreadFollowBtn'
import {atoms as a} from '#/alf'
import {RichText} from '#/components/RichText'
import {
isAvailable as isNativeTranslationAvailable,
isLanguageSupported,
NativeTranslationModule,
} from '../../../../modules/expo-bluesky-translate'
import {ContentHider} from '../../../components/moderation/ContentHider'
import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe'
import {PostAlerts} from '../../../components/moderation/PostAlerts'
@@ -344,7 +339,6 @@ let PostThreadItemLoaded = ({
</ContentHider>
<ExpandedPostDetails
post={post}
record={record}
translatorUrl={translatorUrl}
needsTranslation={needsTranslation}
/>
@@ -653,12 +647,10 @@ function PostOuterWrapper({
function ExpandedPostDetails({
post,
record,
needsTranslation,
translatorUrl,
}: {
post: AppBskyFeedDefs.PostView
record?: AppBskyFeedPost.Record
needsTranslation: boolean
translatorUrl: string
}) {
@@ -666,18 +658,9 @@ function ExpandedPostDetails({
const {_} = useLingui()
const openLink = useOpenLink()
const text = record?.text || ''
const onTranslatePress = React.useCallback(() => {
if (
isNativeTranslationAvailable &&
isLanguageSupported(record?.langs?.at(0))
) {
NativeTranslationModule.presentAsync(text)
} else {
openLink(translatorUrl)
}
}, [openLink, text, translatorUrl, record])
}, [openLink, translatorUrl])
return (
<View style={[s.flexRow, s.mt2, s.mb10]}>
@@ -0,0 +1,57 @@
import * as React from 'react'
import {View} from 'react-native'
import {AppBskyFeedDefs, AtUri} from '@atproto/api'
import {Trans} from '@lingui/macro'
import {makeProfileLink} from '#/lib/routes/links'
import {atoms as a, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
import {Link} from '../util/Link'
import {UserAvatar} from '../util/UserAvatar'
export function PostThreadLoadMore({post}: {post: AppBskyFeedDefs.PostView}) {
const t = useTheme()
const postHref = React.useMemo(() => {
const urip = new AtUri(post.uri)
return makeProfileLink(post.author, 'post', urip.rkey)
}, [post.uri, post.author])
return (
<Link
href={postHref}
style={[a.flex_row, a.align_center, a.py_md, {paddingHorizontal: 14}]}
hoverStyle={[t.atoms.bg_contrast_25]}>
<View style={[a.flex_row]}>
<View
style={{
alignItems: 'center',
justifyContent: 'center',
width: 34,
height: 34,
borderRadius: 18,
backgroundColor: t.atoms.bg.backgroundColor,
marginRight: -20,
}}>
<UserAvatar avatar={post.author.avatar} size={30} />
</View>
<View
style={{
alignItems: 'center',
justifyContent: 'center',
width: 34,
height: 34,
borderRadius: 18,
backgroundColor: t.atoms.bg.backgroundColor,
}}>
<UserAvatar avatar={post.author.avatar} size={30} />
</View>
</View>
<View style={[a.px_sm]}>
<Text style={[{color: t.palette.primary_500}, a.text_md]}>
<Trans>Continue thread...</Trans>
</Text>
</View>
</Link>
)
}
+6 -10
View File
@@ -1,18 +1,19 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import {useNavigation} from '@react-navigation/native'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {Text} from '../util/text/Text'
import {Button} from '../util/forms/Button'
import {Trans} from '@lingui/macro'
import {useNavigation} from '@react-navigation/native'
import {usePalette} from 'lib/hooks/usePalette'
import {MagnifyingGlassIcon} from 'lib/icons'
import {NavigationProp} from 'lib/routes/types'
import {usePalette} from 'lib/hooks/usePalette'
import {s} from 'lib/styles'
import {isWeb} from 'platform/detection'
import {Trans} from '@lingui/macro'
import {Button} from '../util/forms/Button'
import {Text} from '../util/text/Text'
export function FollowingEmptyState() {
const pal = usePalette('default')
@@ -29,12 +30,7 @@ export function FollowingEmptyState() {
}, [navigation])
const onPressDiscoverFeeds = React.useCallback(() => {
if (isWeb) {
navigation.navigate('Feeds')
} else {
navigation.navigate('FeedsTab')
navigation.popToTop()
}
}, [navigation])
return (
+7 -11
View File
@@ -1,17 +1,18 @@
import React from 'react'
import {StyleSheet, View, Dimensions} from 'react-native'
import {useNavigation} from '@react-navigation/native'
import {Dimensions, StyleSheet, View} from 'react-native'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {Text} from '../util/text/Text'
import {Button} from '../util/forms/Button'
import {NavigationProp} from 'lib/routes/types'
import {Trans} from '@lingui/macro'
import {useNavigation} from '@react-navigation/native'
import {usePalette} from 'lib/hooks/usePalette'
import {NavigationProp} from 'lib/routes/types'
import {s} from 'lib/styles'
import {isWeb} from 'platform/detection'
import {Trans} from '@lingui/macro'
import {Button} from '../util/forms/Button'
import {Text} from '../util/text/Text'
export function FollowingEndOfFeed() {
const pal = usePalette('default')
@@ -28,12 +29,7 @@ export function FollowingEndOfFeed() {
}, [navigation])
const onPressDiscoverFeeds = React.useCallback(() => {
if (isWeb) {
navigation.navigate('Feeds')
} else {
navigation.navigate('FeedsTab')
navigation.popToTop()
}
}, [navigation])
return (
@@ -23,6 +23,7 @@ import {CenteredView} from '../util/Views'
import hairlineWidth = StyleSheet.hairlineWidth
import {AppBskyGraphDefs} from '@atproto/api'
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
import {StarterPackAvatarsIcon} from '#/components/StarterPack/StarterPackAvatarsIcon'
export function ProfileSubpageHeader({
@@ -108,11 +109,7 @@ export function ProfileSubpageHeader({
style={[styles.backIcon, pal.text]}
/>
) : (
<FontAwesomeIcon
size={18}
icon="bars"
style={[styles.backIcon, pal.textLight]}
/>
<Menu size="lg" style={[{marginTop: 4}, pal.textLight]} />
)}
</Pressable>
<View style={{flex: 1}} />
@@ -203,7 +200,7 @@ const styles = StyleSheet.create({
backBtnWide: {
width: 20,
height: 30,
paddingHorizontal: 6,
marginRight: 4,
},
backIcon: {
marginTop: 6,
+1 -1
View File
@@ -90,7 +90,7 @@ export function TestCtrls() {
/>
<Pressable
testID="e2eGotoFeeds"
onPress={() => navigate('FeedsTab')}
onPress={() => navigate('Feeds')}
accessibilityRole="button"
style={BTN}
/>
+37 -18
View File
@@ -5,10 +5,13 @@ import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {Text} from './text/Text'
import {UserGroupIcon} from 'lib/icons'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {usePalette} from 'lib/hooks/usePalette'
import {UserGroupIcon} from 'lib/icons'
import {isWeb} from 'platform/detection'
import {Growth_Stroke2_Corner0_Rounded as Growth} from '#/components/icons/Growth'
import {Text} from './text/Text'
export function EmptyState({
testID,
@@ -17,32 +20,41 @@ export function EmptyState({
style,
}: {
testID?: string
icon: IconProp | 'user-group'
icon: IconProp | 'user-group' | 'growth'
message: string
style?: StyleProp<ViewStyle>
}) {
const pal = usePalette('default')
const {isTabletOrDesktop} = useWebMediaQueries()
const iconSize = isTabletOrDesktop ? 80 : 64
return (
<View
testID={testID}
style={[styles.container, isWeb && pal.border, style]}>
<View style={styles.iconContainer}>
style={[
styles.container,
isWeb && pal.border,
isTabletOrDesktop && {paddingRight: 20},
style,
]}>
<View
style={[
styles.iconContainer,
isTabletOrDesktop && styles.iconContainerBig,
pal.viewLight,
]}>
{icon === 'user-group' ? (
<UserGroupIcon size="64" style={styles.icon} />
<UserGroupIcon size={iconSize} />
) : icon === 'growth' ? (
<Growth width={iconSize} fill={pal.colors.emptyStateIcon} />
) : (
<FontAwesomeIcon
icon={icon}
size={64}
style={[
styles.icon,
{color: pal.colors.emptyStateIcon} as FontAwesomeIconStyle,
]}
size={iconSize}
style={[{color: pal.colors.emptyStateIcon} as FontAwesomeIconStyle]}
/>
)}
</View>
<Text
type="xl-medium"
style={[{color: pal.colors.textVeryLight}, styles.text]}>
<Text type="xl" style={[{color: pal.colors.textLight}, styles.text]}>
{message}
</Text>
</View>
@@ -51,16 +63,23 @@ export function EmptyState({
const styles = StyleSheet.create({
container: {
paddingVertical: 24,
paddingHorizontal: 36,
borderTopWidth: isWeb ? 1 : undefined,
},
iconContainer: {
flexDirection: 'row',
},
icon: {
alignItems: 'center',
justifyContent: 'center',
height: 100,
width: 100,
marginLeft: 'auto',
marginRight: 'auto',
borderRadius: 80,
marginTop: 30,
},
iconContainerBig: {
width: 140,
height: 140,
marginTop: 50,
},
text: {
textAlign: 'center',
+9 -10
View File
@@ -8,13 +8,15 @@ import {
} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {useNavigation} from '@react-navigation/native'
import {CenteredView} from './Views'
import {isWeb} from '#/platform/detection'
import {useSetDrawerOpen} from '#/state/shell'
import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {useAnalytics} from 'lib/analytics/analytics'
import {NavigationProp} from 'lib/routes/types'
import {useSetDrawerOpen} from '#/state/shell'
import {isWeb} from '#/platform/detection'
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
import {CenteredView} from './Views'
const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20}
@@ -72,11 +74,7 @@ export function SimpleViewHeader({
style={[styles.backIcon, pal.text]}
/>
) : (
<FontAwesomeIcon
size={18}
icon="bars"
style={[styles.backIcon, pal.textLight]}
/>
<Menu size="lg" style={[{marginTop: 4}, pal.textLight]} />
)}
</TouchableOpacity>
) : null}
@@ -110,7 +108,8 @@ const styles = StyleSheet.create({
backBtnWide: {
width: 30,
height: 30,
paddingHorizontal: 6,
paddingLeft: 4,
marginRight: 4,
},
backIcon: {
marginTop: 6,
+4 -6
View File
@@ -16,6 +16,7 @@ import {useTheme} from '#/alf'
import {Text} from './text/Text'
import {CenteredView} from './Views'
import hairlineWidth = StyleSheet.hairlineWidth
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20}
@@ -104,11 +105,7 @@ export function ViewHeader({
style={[styles.backIcon, pal.text]}
/>
) : !isTablet ? (
<FontAwesomeIcon
size={18}
icon="bars"
style={[styles.backIcon, pal.textLight]}
/>
<Menu size="lg" style={[{marginTop: 3}, pal.textLight]} />
) : null}
</TouchableOpacity>
) : null}
@@ -275,7 +272,8 @@ const styles = StyleSheet.create({
backBtnWide: {
width: 30,
height: 30,
paddingHorizontal: 6,
paddingLeft: 4,
marginRight: 4,
},
backIcon: {
marginTop: 6,
+1 -14
View File
@@ -57,11 +57,6 @@ import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/War
import * as Menu from '#/components/Menu'
import * as Prompt from '#/components/Prompt'
import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
import {
isAvailable as isNativeTranslationAvailable,
isLanguageSupported,
NativeTranslationModule,
} from '../../../../../modules/expo-bluesky-translate'
import {EventStopper} from '../EventStopper'
import * as Toast from '../Toast'
@@ -188,16 +183,8 @@ let PostDropdownBtn = ({
}, [_, richText])
const onPressTranslate = React.useCallback(() => {
if (
isNativeTranslationAvailable &&
isLanguageSupported(record?.langs?.at(0))
) {
const text = richTextToString(richText, true)
NativeTranslationModule.presentAsync(text)
} else {
openLink(translatorUrl)
}
}, [openLink, record?.langs, richText, translatorUrl])
}, [openLink, translatorUrl])
const onHidePost = React.useCallback(() => {
hidePost({uri: postUri})
@@ -3,6 +3,7 @@ import {View} from 'react-native'
import {msg, plural} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {HITSLOP_10, HITSLOP_20} from '#/lib/constants'
import {useHaptics} from '#/lib/haptics'
import {useRequireAuth} from '#/state/session'
import {atoms as a, useTheme} from '#/alf'
@@ -64,7 +65,8 @@ let RepostButton = ({
} (${plural(repostCount || 0, {one: '# repost', other: '# reposts'})})`}
shape="round"
variant="ghost"
color="secondary">
color="secondary"
hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
<Repost style={color} width={big ? 22 : 18} />
{typeof repostCount !== 'undefined' && repostCount > 0 ? (
<Text
+21 -48
View File
@@ -1,11 +1,5 @@
import React from 'react'
import {
ActivityIndicator,
type FlatList,
Pressable,
StyleSheet,
View,
} from 'react-native'
import {ActivityIndicator, type FlatList, StyleSheet, View} from 'react-native'
import {AppBskyActorDefs} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
@@ -25,18 +19,17 @@ import {usePreferencesQuery} from '#/state/queries/preferences'
import {useSession} from '#/state/session'
import {useSetMinimalShellMode} from '#/state/shell'
import {useComposerControls} from '#/state/shell/composer'
import {HITSLOP_10} from 'lib/constants'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {CogIcon, ComposeIcon2, MagnifyingGlassIcon2} from 'lib/icons'
import {FeedsTabNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
import {ComposeIcon2} from 'lib/icons'
import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
import {cleanError} from 'lib/strings/errors'
import {s} from 'lib/styles'
import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
import {FAB} from 'view/com/util/fab/FAB'
import {SearchInput, SearchInputRef} from 'view/com/util/forms/SearchInput'
import {Link} from 'view/com/util/Link'
import {SearchInput} from 'view/com/util/forms/SearchInput'
import {Link, TextLink} from 'view/com/util/Link'
import {List} from 'view/com/util/List'
import {
FeedFeedLoadingPlaceholder,
@@ -54,7 +47,7 @@ import {ListMagnifyingGlass_Stroke2_Corner0_Rounded} from '#/components/icons/Li
import {ListSparkle_Stroke2_Corner0_Rounded} from '#/components/icons/ListSparkle'
import hairlineWidth = StyleSheet.hairlineWidth
type Props = NativeStackScreenProps<FeedsTabNavigatorParams, 'Feeds'>
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Feeds'>
type FlatlistSlice =
| {
@@ -159,7 +152,6 @@ export function FeedsScreen(_props: Props) {
} = useSearchPopularFeedsMutation()
const {hasSession} = useSession()
const listRef = React.useRef<FlatList>(null)
const searchInputRef = React.useRef<SearchInputRef>(null)
/**
* A search query is present. We may not have search results yet.
@@ -399,24 +391,15 @@ export function FeedsScreen(_props: Props) {
const renderHeaderBtn = React.useCallback(() => {
return (
<View style={styles.headerBtnGroup}>
<Pressable
accessibilityRole="button"
hitSlop={HITSLOP_10}
onPress={searchInputRef.current?.focus}>
<MagnifyingGlassIcon2
size={22}
strokeWidth={2}
style={pal.textLight}
/>
</Pressable>
<Link
<TextLink
testID="editFeedsBtn"
type="lg-medium"
href="/settings/saved-feeds"
hitSlop={10}
accessibilityRole="button"
accessibilityLabel={_(msg`Edit Saved Feeds`)}
accessibilityHint={_(msg`Opens screen to edit Saved Feeds`)}>
<CogIcon size={22} strokeWidth={2} style={pal.textLight} />
</Link>
accessibilityLabel={_(msg`Edit My Feeds`)}
accessibilityHint=""
text={_(msg`Edit`)}
style={[pal.link, a.pr_xs]}
/>
</View>
)
}, [pal, _])
@@ -480,22 +463,14 @@ export function FeedsScreen(_props: Props) {
<Trans>Feeds</Trans>
</Text>
<View style={styles.headerBtnGroup}>
<Pressable
accessibilityRole="button"
hitSlop={HITSLOP_10}
onPress={searchInputRef.current?.focus}>
<MagnifyingGlassIcon2
size={22}
strokeWidth={2}
style={pal.icon}
/>
</Pressable>
<Link
<TextLink
type="lg"
href="/settings/saved-feeds"
accessibilityLabel={_(msg`Edit My Feeds`)}
accessibilityHint="">
<CogIcon strokeWidth={1.5} style={pal.icon} size={28} />
</Link>
accessibilityHint=""
text={_(msg`Edit`)}
style={[pal.link]}
/>
</View>
</View>
)}
@@ -522,7 +497,6 @@ export function FeedsScreen(_props: Props) {
<FeedsAboutHeader />
<View style={{paddingHorizontal: 12, paddingBottom: 12}}>
<SearchInput
ref={searchInputRef}
query={query}
onChangeQuery={onChangeQuery}
onPressCancelSearch={onPressCancelSearch}
@@ -577,8 +551,8 @@ export function FeedsScreen(_props: Props) {
pal.view,
pal.border,
pal.text,
pal.icon,
pal.textLight,
pal.link,
_,
query,
onChangeQuery,
@@ -594,7 +568,6 @@ export function FeedsScreen(_props: Props) {
{isMobile && (
<ViewHeader
title={_(msg`Feeds`)}
canGoBack={false}
renderButton={renderHeaderBtn}
showBorder
/>
+20 -4
View File
@@ -30,6 +30,7 @@ import {TextLink} from 'view/com/util/Link'
import {ListMethods} from 'view/com/util/List'
import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
import {CenteredView} from 'view/com/util/Views'
import {Loader} from '#/components/Loader'
import {Feed} from '../com/notifications/Feed'
import {FAB} from '../com/util/fab/FAB'
import {MainScrollProvider} from '../com/util/MainScrollProvider'
@@ -43,6 +44,7 @@ export function NotificationsScreen({}: Props) {
const {_} = useLingui()
const setMinimalShellMode = useSetMinimalShellMode()
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const [isLoadingLatest, setIsLoadingLatest] = React.useState(false)
const scrollElRef = React.useRef<ListMethods>(null)
const {screen} = useAnalytics()
const pal = usePalette('default')
@@ -68,9 +70,13 @@ export function NotificationsScreen({}: Props) {
truncateAndInvalidate(queryClient, NOTIFS_RQKEY())
} else {
// check with the server
unreadApi.checkUnread({invalidate: true})
setIsLoadingLatest(true)
unreadApi
.checkUnread({invalidate: true})
.catch(() => undefined)
.then(() => setIsLoadingLatest(false))
}
}, [scrollToTop, queryClient, unreadApi, hasNew])
}, [scrollToTop, queryClient, unreadApi, hasNew, setIsLoadingLatest])
const onFocusCheckLatest = useNonReactiveCallback(() => {
// on focus, check for latest, but only invalidate if the user
@@ -139,21 +145,31 @@ export function NotificationsScreen({}: Props) {
}
onPress={emitSoftReset}
/>
{isLoadingLatest ? <Loader size="md" /> : <></>}
</View>
)
}
return <></>
}, [isDesktop, pal, hasNew])
}, [isDesktop, pal, hasNew, isLoadingLatest])
const renderHeaderSpinner = React.useCallback(() => {
return (
<View style={{width: 30, height: 20, alignItems: 'flex-end'}}>
{isLoadingLatest ? <Loader width={20} /> : <></>}
</View>
)
}, [isLoadingLatest])
return (
<CenteredView
testID="notificationsScreen"
style={s.hContentRegion}
style={[s.hContentRegion, {paddingTop: 2}]}
sideBorders={true}>
<ViewHeader
title={_(msg`Notifications`)}
canGoBack={false}
showBorder={true}
renderButton={renderHeaderSpinner}
/>
<MainScrollProvider>
<Feed
+1 -1
View File
@@ -468,7 +468,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
}, [onScrollToTop, isScreenFocused])
const renderPostsEmpty = useCallback(() => {
return <EmptyState icon="feed" message={_(msg`This feed is empty!`)} />
return <EmptyState icon="hashtag" message={_(msg`This feed is empty.`)} />
}, [_])
return (
+1 -1
View File
@@ -726,7 +726,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
}, [onScrollToTop, isScreenFocused])
const renderPostsEmpty = useCallback(() => {
return <EmptyState icon="feed" message={_(msg`This feed is empty!`)} />
return <EmptyState icon="hashtag" message={_(msg`This feed is empty.`)} />
}, [_])
return (
+4 -7
View File
@@ -59,6 +59,7 @@ import {CenteredView, ScrollView} from '#/view/com/util/Views'
import {SearchLinkCard, SearchProfileCard} from '#/view/shell/desktop/Search'
import {ProfileCardFeedLoadingPlaceholder} from 'view/com/util/LoadingPlaceholder'
import {atoms as a} from '#/alf'
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
function Loader() {
const pal = usePalette('default')
@@ -712,11 +713,7 @@ export function SearchScreen(
accessibilityRole="button"
accessibilityLabel={_(msg`Menu`)}
accessibilityHint={_(msg`Access navigation links and settings`)}>
<FontAwesomeIcon
icon="bars"
size={18}
color={pal.colors.textLight}
/>
<Menu size="lg" fill={pal.colors.textLight} />
</Pressable>
)}
<SearchInputBox
@@ -1073,13 +1070,14 @@ function scrollToTopWeb() {
}
}
const HEADER_HEIGHT = 50
const HEADER_HEIGHT = 46
const styles = StyleSheet.create({
header: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 12,
paddingLeft: 13,
paddingVertical: 4,
height: HEADER_HEIGHT,
// @ts-ignore web only
@@ -1092,7 +1090,6 @@ const styles = StyleSheet.create({
height: 30,
borderRadius: 30,
marginRight: 6,
paddingBottom: 2,
alignItems: 'center',
justifyContent: 'center',
},
+80
View File
@@ -0,0 +1,80 @@
import React, {useLayoutEffect} from 'react'
import {Modal, View} from 'react-native'
import {StatusBar} from 'expo-status-bar'
import * as SystemUI from 'expo-system-ui'
import {observer} from 'mobx-react-lite'
import {useComposerState} from '#/state/shell/composer'
import {atoms as a, useTheme} from '#/alf'
import {getBackgroundColor, useThemeName} from '#/alf/util/useColorModeTheme'
import {ComposePost, useComposerCancelRef} from '../com/composer/Composer'
export const Composer = observer(function ComposerImpl({}: {
winHeight: number
}) {
const t = useTheme()
const state = useComposerState()
const ref = useComposerCancelRef()
const open = !!state
return (
<Modal
aria-modal
accessibilityViewIsModal
visible={open}
presentationStyle="pageSheet"
animationType="slide"
onRequestClose={() => ref.current?.onPressCancel()}>
<View style={[t.atoms.bg, a.flex_1]}>
<Providers open={open}>
<ComposePost
cancelRef={ref}
replyTo={state?.replyTo}
onPost={state?.onPost}
quote={state?.quote}
mention={state?.mention}
text={state?.text}
imageUris={state?.imageUris}
/>
</Providers>
</View>
</Modal>
)
})
function Providers({
children,
open,
}: {
children: React.ReactNode
open: boolean
}) {
// on iOS, it's a native formSheet. We use FullWindowOverlay to make
// the dialogs appear over it
return (
<>
{children}
<IOSModalBackground active={open} />
</>
)
}
// Generally, the backdrop of the app is the theme color, but when this is open
// we want it to be black due to the modal being a form sheet.
function IOSModalBackground({active}: {active: boolean}) {
const theme = useThemeName()
useLayoutEffect(() => {
SystemUI.setBackgroundColorAsync('black')
return () => {
SystemUI.setBackgroundColorAsync(getBackgroundColor(theme))
}
}, [theme])
// Set the status bar to light - however, only if the modal is active
// If we rely on this component being mounted to set this,
// there'll be a delay before it switches back to default.
return active ? <StatusBar style="light" animated /> : null
}
+56 -99
View File
@@ -1,116 +1,73 @@
import React, {useLayoutEffect, useState} from 'react'
import {Modal, View} from 'react-native'
import {GestureHandlerRootView} from 'react-native-gesture-handler'
import {RootSiblingParent} from 'react-native-root-siblings'
import {StatusBar} from 'expo-status-bar'
import * as SystemUI from 'expo-system-ui'
import React, {useEffect} from 'react'
import {Animated, Easing, StyleSheet, View} from 'react-native'
import {observer} from 'mobx-react-lite'
import {isIOS} from '#/platform/detection'
import {Provider as LegacyModalProvider} from '#/state/modals'
import {useComposerState} from '#/state/shell/composer'
import {ModalsContainer as LegacyModalsContainer} from '#/view/com/modals/Modal'
import {atoms as a, useTheme} from '#/alf'
import {getBackgroundColor, useThemeName} from '#/alf/util/useColorModeTheme'
import {
Outlet as PortalOutlet,
Provider as PortalProvider,
} from '#/components/Portal'
import {ComposePost, useComposerCancelRef} from '../com/composer/Composer'
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
import {usePalette} from 'lib/hooks/usePalette'
import {useComposerState} from 'state/shell/composer'
import {ComposePost} from '../com/composer/Composer'
export const Composer = observer(function ComposerImpl({}: {
export const Composer = observer(function ComposerImpl({
winHeight,
}: {
winHeight: number
}) {
const t = useTheme()
const state = useComposerState()
const ref = useComposerCancelRef()
const [isModalReady, setIsModalReady] = useState(false)
const pal = usePalette('default')
const initInterp = useAnimatedValue(0)
const open = !!state
const [prevOpen, setPrevOpen] = useState(open)
if (open !== prevOpen) {
setPrevOpen(open)
if (!open) {
setIsModalReady(false)
useEffect(() => {
if (state) {
Animated.timing(initInterp, {
toValue: 1,
duration: 300,
easing: Easing.out(Easing.exp),
useNativeDriver: true,
}).start()
} else {
initInterp.setValue(0)
}
}, [initInterp, state])
const wrapperAnimStyle = {
transform: [
{
translateY: initInterp.interpolate({
inputRange: [0, 1],
outputRange: [winHeight, 0],
}),
},
],
}
// rendering
// =
if (!state) {
return <View />
}
return (
<Modal
<Animated.View
style={[styles.wrapper, pal.view, wrapperAnimStyle]}
aria-modal
accessibilityViewIsModal
visible={open}
presentationStyle="formSheet"
animationType="slide"
onShow={() => setIsModalReady(true)}
onRequestClose={() => ref.current?.onPressCancel()}>
<View style={[t.atoms.bg, a.flex_1]}>
<Providers open={open}>
accessibilityViewIsModal>
<ComposePost
isModalReady={isModalReady}
cancelRef={ref}
replyTo={state?.replyTo}
onPost={state?.onPost}
quote={state?.quote}
mention={state?.mention}
text={state?.text}
imageUris={state?.imageUris}
replyTo={state.replyTo}
onPost={state.onPost}
quote={state.quote}
mention={state.mention}
text={state.text}
imageUris={state.imageUris}
/>
</Providers>
</View>
</Modal>
</Animated.View>
)
})
function Providers({
children,
open,
}: {
children: React.ReactNode
open: boolean
}) {
// on iOS, it's a native formSheet. We use FullWindowOverlay to make
// the dialogs appear over it
if (isIOS) {
return (
<>
{children}
<IOSModalBackground active={open} />
</>
)
} else {
// on Android we just nest the dialogs within it
return (
<GestureHandlerRootView style={a.flex_1}>
<RootSiblingParent>
<LegacyModalProvider>
<PortalProvider>
{children}
<LegacyModalsContainer />
<PortalOutlet />
</PortalProvider>
</LegacyModalProvider>
</RootSiblingParent>
</GestureHandlerRootView>
)
}
}
// Generally, the backdrop of the app is the theme color, but when this is open
// we want it to be black due to the modal being a form sheet.
function IOSModalBackground({active}: {active: boolean}) {
const theme = useThemeName()
useLayoutEffect(() => {
SystemUI.setBackgroundColorAsync('black')
return () => {
SystemUI.setBackgroundColorAsync(getBackgroundColor(theme))
}
}, [theme])
// Set the status bar to light - however, only if the modal is active
// If we rely on this component being mounted to set this,
// there'll be a delay before it switches back to default.
return active ? <StatusBar style="light" animated /> : null
}
const styles = StyleSheet.create({
wrapper: {
position: 'absolute',
top: 0,
bottom: 0,
width: '100%',
},
})
-1
View File
@@ -56,7 +56,6 @@ export function Composer({}: {winHeight: number}) {
t.atoms.border_contrast_medium,
]}>
<ComposePost
isModalReady={true}
replyTo={state.replyTo}
quote={state.quote}
onPost={state.onPost}
+5 -4
View File
@@ -186,10 +186,11 @@ let DrawerContent = ({}: {}): React.ReactNode => {
onPressTab('MyProfile')
}, [onPressTab])
const onPressMyFeeds = React.useCallback(
() => onPressTab('Feeds'),
[onPressTab],
)
const onPressMyFeeds = React.useCallback(() => {
track('Menu:ItemClicked', {url: 'Feeds'})
navigation.navigate('Feeds')
setDrawerOpen(false)
}, [navigation, setDrawerOpen, track])
const onPressLists = React.useCallback(() => {
track('Menu:ItemClicked', {url: 'Lists'})

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