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" id: "viewHeaderDrawerBtn"
- tapOn: - tapOn:
id: "menuItemButton-Feeds" id: "menuItemButton-Feeds"
- tapOn: "Edit Saved Feeds" - tapOn:
id: "editFeedsBtn"
- tapOn: - tapOn:
label: "Tap on down arrow" label: "Tap on down arrow"
point: "79%,23%" point: "79%,23%"
- tapOn: - tapOn:
id: "bottomBarHomeBtn" id: "viewHeaderDrawerBtn"
- tapOn:
id: "viewHeaderDrawerBtn"
- assertVisible: - assertVisible:
id: "homeScreenFeedTabs-selector-0" id: "homeScreenFeedTabs-selector-0"
text: "alice-favs" text: "alice-favs"
@@ -54,11 +57,15 @@ appId: xyz.blueskyweb.app
id: "viewHeaderDrawerBtn" id: "viewHeaderDrawerBtn"
- tapOn: - tapOn:
id: "menuItemButton-Feeds" id: "menuItemButton-Feeds"
- tapOn:
id: "editFeedsBtn"
- tapOn: - tapOn:
label: "Tap on down arrow" label: "Tap on down arrow"
point: "79%,23%" point: "79%,23%"
- tapOn: - tapOn:
id: "bottomBarHomeBtn" id: "viewHeaderDrawerBtn"
- tapOn:
id: "viewHeaderDrawerBtn"
- assertVisible: - assertVisible:
id: "homeScreenFeedTabs-selector-0" id: "homeScreenFeedTabs-selector-0"
text: "Following" text: "Following"
@@ -71,11 +78,15 @@ appId: xyz.blueskyweb.app
id: "viewHeaderDrawerBtn" id: "viewHeaderDrawerBtn"
- tapOn: - tapOn:
id: "menuItemButton-Feeds" id: "menuItemButton-Feeds"
- tapOn:
id: "editFeedsBtn"
- tapOn: - tapOn:
label: "Tap on unpin" label: "Tap on unpin"
point: "91%,23%" point: "91%,23%"
- tapOn: - tapOn:
id: "bottomBarHomeBtn" id: "viewHeaderDrawerBtn"
- tapOn:
id: "viewHeaderDrawerBtn"
- assertVisible: - assertVisible:
id: "homeScreenFeedTabs-selector-0" id: "homeScreenFeedTabs-selector-0"
text: "alice-favs" text: "alice-favs"
+2
View File
@@ -28,6 +28,8 @@ appId: xyz.blueskyweb.app
- tapOn: "Pin to Home" - tapOn: "Pin to Home"
- tapOn: - tapOn:
id: "bottomBarHomeBtn" id: "bottomBarHomeBtn"
- tapOn:
id: "viewHeaderDrawerBtn"
- assertNotVisible: "Feeds ✨" - assertNotVisible: "Feeds ✨"
- tapOn: - tapOn:
+2 -2
View File
@@ -58,7 +58,7 @@ appId: xyz.blueskyweb.app
id: "repostBtn" id: "repostBtn"
childOf: childOf:
id: "postThreadItem-by-bob.test" id: "postThreadItem-by-bob.test"
- tapOn: "Undo repost" - tapOn: "Remove repost"
- assertNotVisible: - assertNotVisible:
id: "repostCount-expanded" id: "repostCount-expanded"
@@ -77,7 +77,7 @@ appId: xyz.blueskyweb.app
id: "repostBtn" id: "repostBtn"
childOf: childOf:
id: "postThreadItem-by-carla.test" id: "postThreadItem-by-carla.test"
- tapOn: "Undo repost" - tapOn: "Remove repost"
- assertNotVisible: - assertNotVisible:
id: "repostCount" id: "repostCount"
childOf: 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", server.WebProfile)
e.GET("/profile/:handleOrDID/follows", server.WebGeneric) e.GET("/profile/:handleOrDID/follows", server.WebGeneric)
e.GET("/profile/:handleOrDID/followers", 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/lists/:rkey", server.WebGeneric)
e.GET("/profile/:handleOrDID/feed/:rkey", server.WebGeneric) e.GET("/profile/:handleOrDID/feed/:rkey", server.WebGeneric)
e.GET("/profile/:handleOrDID/feed/:rkey/liked-by", 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 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 --- a/node_modules/expo-notifications/android/build.gradle
+++ b/node_modules/expo-notifications/android/build.gradle +++ b/node_modules/expo-notifications/android/build.gradle
@@ -32,6 +32,7 @@ dependencies { @@ -32,6 +32,7 @@ dependencies {
@@ -10,6 +10,31 @@ index d233e1f..cc2f856 100644
if (project.findProject(':expo-modules-test-core')) { if (project.findProject(':expo-modules-test-core')) {
testImplementation project(':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 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 index 0af7fe0..8f2c8d8 100644
--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/JSONNotificationContentBuilder.java --- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/JSONNotificationContentBuilder.java
@@ -46,7 +71,7 @@ index 0af7fe0..8f2c8d8 100644
try { try {
return payload.getString(TITLE_KEY); 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 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 --- 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 +++ 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; @@ -20,6 +20,7 @@ import expo.modules.notifications.notifications.enums.NotificationPriority;
@@ -63,7 +88,7 @@ index f1fed19..80afe9e 100644
+ @Nullable + @Nullable
+ public String getChannelId() { + public String getChannelId() {
+ return mTitle; + return mChannelId;
+ } + }
+ +
@Nullable @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 It also allows us to set the Android notification channel ID from the notification `data`, rather
than the `notification` object in the payload. 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. * that is set up in the InnerApp component above.
*/ */
return ( return (
<KeyboardProvider enabled={true} statusBarTranslucent={true}> <KeyboardProvider enabled={false} statusBarTranslucent={true}>
<SessionProvider> <SessionProvider>
<ShellStateProvider> <ShellStateProvider>
<PrefsStateProvider> <PrefsStateProvider>
+9 -26
View File
@@ -22,7 +22,6 @@ import {buildStateObject} from 'lib/routes/helpers'
import { import {
AllNavigatorParams, AllNavigatorParams,
BottomTabNavigatorParams, BottomTabNavigatorParams,
FeedsTabNavigatorParams,
FlatNavigatorParams, FlatNavigatorParams,
HomeTabNavigatorParams, HomeTabNavigatorParams,
MessagesTabNavigatorParams, MessagesTabNavigatorParams,
@@ -42,6 +41,7 @@ import {PreferencesThreads} from 'view/screens/PreferencesThreads'
import {SavedFeeds} from 'view/screens/SavedFeeds' import {SavedFeeds} from 'view/screens/SavedFeeds'
import HashtagScreen from '#/screens/Hashtag' import HashtagScreen from '#/screens/Hashtag'
import {ModerationScreen} from '#/screens/Moderation' import {ModerationScreen} from '#/screens/Moderation'
import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers'
import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy' import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
import {LandingScreen} from '#/screens/StarterPack/Landing' import {LandingScreen} from '#/screens/StarterPack/Landing'
import {StarterPackScreen} from '#/screens/StarterPack/Main' import {StarterPackScreen} from '#/screens/StarterPack/Main'
@@ -94,7 +94,6 @@ const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
const HomeTab = createNativeStackNavigatorWithAuth<HomeTabNavigatorParams>() const HomeTab = createNativeStackNavigatorWithAuth<HomeTabNavigatorParams>()
const SearchTab = createNativeStackNavigatorWithAuth<SearchTabNavigatorParams>() const SearchTab = createNativeStackNavigatorWithAuth<SearchTabNavigatorParams>()
const FeedsTab = createNativeStackNavigatorWithAuth<FeedsTabNavigatorParams>()
const NotificationsTab = const NotificationsTab =
createNativeStackNavigatorWithAuth<NotificationsTabNavigatorParams>() createNativeStackNavigatorWithAuth<NotificationsTabNavigatorParams>()
const MyProfileTab = const MyProfileTab =
@@ -174,6 +173,13 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
title: title(msg`People followed by @${route.params.name}`), 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 <Stack.Screen
name="ProfileList" name="ProfileList"
getComponent={() => ProfileListScreen} getComponent={() => ProfileListScreen}
@@ -309,6 +315,7 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
getComponent={() => MessagesSettingsScreen} getComponent={() => MessagesSettingsScreen}
options={{title: title(msg`Chat settings`), requireAuth: true}} options={{title: title(msg`Chat settings`), requireAuth: true}}
/> />
<Stack.Screen name="Feeds" getComponent={() => FeedsScreen} />
<Stack.Screen <Stack.Screen
// @ts-ignore What the fuck? // @ts-ignore What the fuck?
name="StarterPack" name="StarterPack"
@@ -349,7 +356,6 @@ function TabsNavigator() {
tabBar={tabBar}> tabBar={tabBar}>
<Tab.Screen name="HomeTab" getComponent={() => HomeTabNavigator} /> <Tab.Screen name="HomeTab" getComponent={() => HomeTabNavigator} />
<Tab.Screen name="SearchTab" getComponent={() => SearchTabNavigator} /> <Tab.Screen name="SearchTab" getComponent={() => SearchTabNavigator} />
<Tab.Screen name="FeedsTab" getComponent={() => FeedsTabNavigator} />
<Tab.Screen <Tab.Screen
name="NotificationsTab" name="NotificationsTab"
getComponent={() => NotificationsTabNavigator} 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() { function NotificationsTabNavigator() {
const pal = usePalette('default') const pal = usePalette('default')
return ( return (
@@ -524,11 +512,6 @@ const FlatNavigator = () => {
getComponent={() => SearchScreen} getComponent={() => SearchScreen}
options={{title: title(msg`Search`)}} options={{title: title(msg`Search`)}}
/> />
<Flat.Screen
name="Feeds"
getComponent={() => FeedsScreen}
options={{title: title(msg`Feeds`)}}
/>
<Flat.Screen <Flat.Screen
name="Notifications" name="Notifications"
getComponent={() => NotificationsScreen} getComponent={() => NotificationsScreen}
@@ -5,7 +5,7 @@ import Animated, {
useSharedValue, useSharedValue,
} from 'react-native-reanimated' } from 'react-native-reanimated'
export function KeyboardPadding({maxHeight}: {maxHeight?: number}) { export function KeyboardControllerPadding({maxHeight}: {maxHeight?: number}) {
const keyboardHeight = useSharedValue(0) const keyboardHeight = useSharedValue(0)
useKeyboardHandler( 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 * as Toggle from '#/components/forms/Toggle'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron' import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
import {KeyboardPadding} from '#/components/KeyboardPadding'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {ReportDialogProps} from './types' import {ReportDialogProps} from './types'
@@ -222,7 +221,6 @@ export function SubmitView({
{submitting && <ButtonIcon icon={Loader} />} {submitting && <ButtonIcon icon={Loader} />}
</Button> </Button>
</View> </View>
<KeyboardPadding />
</View> </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 {PageText_Stroke2_Corner0_Rounded as PageText} from '#/components/icons/PageText'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {KeyboardPadding} from '#/components/KeyboardPadding'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import * as Prompt from '#/components/Prompt' import * as Prompt from '#/components/Prompt'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
@@ -257,7 +256,6 @@ function MutedWordsInner() {
</View> </View>
<Dialog.Close /> <Dialog.Close />
<KeyboardPadding maxHeight={100} />
</Dialog.ScrollableInner> </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 * as Toast from '#/view/com/util/Toast'
import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
import {KeyboardControllerPadding} from '#/components/KeyboardControllerPadding'
import {Button, ButtonIcon, ButtonText} from '../Button' import {Button, ButtonIcon, ButtonText} from '../Button'
import {Divider} from '../Divider' import {Divider} from '../Divider'
import {ChevronLeft_Stroke2_Corner0_Rounded as Chevron} from '../icons/Chevron' import {ChevronLeft_Stroke2_Corner0_Rounded as Chevron} from '../icons/Chevron'
@@ -47,6 +48,7 @@ let ReportDialog = ({
<Dialog.ScrollableInner label={_(msg`Report this message`)}> <Dialog.ScrollableInner label={_(msg`Report this message`)}>
<DialogInner params={params} /> <DialogInner params={params} />
<Dialog.Close /> <Dialog.Close />
<KeyboardControllerPadding />
</Dialog.ScrollableInner> </Dialog.ScrollableInner>
</Dialog.Outer> </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 {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
import {KeyboardPadding} from '#/components/KeyboardPadding'
import {InlineLinkText} from '#/components/Link' import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {Divider} from '../Divider' import {Divider} from '../Divider'
@@ -110,7 +109,6 @@ function LabelsOnMeDialogInner(props: LabelsOnMeDialogProps) {
</> </>
)} )}
<Dialog.Close /> <Dialog.Close />
<KeyboardPadding />
</Dialog.ScrollableInner> </Dialog.ScrollableInner>
) )
} }
+1
View File
@@ -7,6 +7,7 @@ export function useNavigationTabState() {
const res = { const res = {
isAtHome: getTabState(state, 'Home') !== TabState.Outside, isAtHome: getTabState(state, 'Home') !== TabState.Outside,
isAtSearch: getTabState(state, 'Search') !== 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, isAtFeeds: getTabState(state, 'Feeds') !== TabState.Outside,
isAtNotifications: isAtNotifications:
getTabState(state, 'Notifications') !== TabState.Outside, getTabState(state, 'Notifications') !== TabState.Outside,
+33 -4
View File
@@ -6,7 +6,7 @@ import {BskyAgent} from '@atproto/api'
import {logger} from '#/logger' import {logger} from '#/logger'
import {SessionAccount, useAgent, useSession} from '#/state/session' import {SessionAccount, useAgent, useSession} from '#/state/session'
import {logEvent, useGate} from 'lib/statsig/statsig' 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' import BackgroundNotificationHandler from '../../../modules/expo-background-notification-handler'
const SERVICE_DID = (serviceUrl?: string) => const SERVICE_DID = (serviceUrl?: string) =>
@@ -43,7 +43,7 @@ async function getPushToken(skipPermissionCheck = false) {
const granted = const granted =
skipPermissionCheck || (await Notifications.getPermissionsAsync()).granted skipPermissionCheck || (await Notifications.getPermissionsAsync()).granted
if (granted) { if (granted) {
Notifications.getDevicePushTokenAsync() return Notifications.getDevicePushTokenAsync()
} }
} }
@@ -56,7 +56,22 @@ export function useNotificationsRegistration() {
return return
} }
getPushToken() // 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 // 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. // cases. This will fire `registerPushToken` whenever that happens.
@@ -73,6 +88,7 @@ export function useNotificationsRegistration() {
export function useRequestNotificationsPermission() { export function useRequestNotificationsPermission() {
const gate = useGate() const gate = useGate()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const agent = useAgent()
return async ( return async (
context: 'StartOnboarding' | 'AfterOnboarding' | 'Login' | 'Home', context: 'StartOnboarding' | 'AfterOnboarding' | 'Login' | 'Home',
@@ -110,7 +126,20 @@ export function useRequestNotificationsPermission() {
if (res.granted) { if (res.granted) {
// This will fire a pushTokenEvent, which will handle registration of the token // 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 ( return (
isTab(currentRoute.name, 'Home') || isTab(currentRoute.name, 'Home') ||
isTab(currentRoute.name, 'Search') || isTab(currentRoute.name, 'Search') ||
isTab(currentRoute.name, 'Feeds') || isTab(currentRoute.name, 'Messages') ||
isTab(currentRoute.name, 'Notifications') || isTab(currentRoute.name, 'Notifications') ||
isTab(currentRoute.name, 'MyProfile') isTab(currentRoute.name, 'MyProfile')
) )
+2 -6
View File
@@ -15,6 +15,7 @@ export type CommonNavigatorParams = {
Profile: {name: string; hideBackButton?: boolean} Profile: {name: string; hideBackButton?: boolean}
ProfileFollowers: {name: string} ProfileFollowers: {name: string}
ProfileFollows: {name: string} ProfileFollows: {name: string}
ProfileKnownFollowers: {name: string}
ProfileList: {name: string; rkey: string} ProfileList: {name: string; rkey: string}
PostThread: {name: string; rkey: string} PostThread: {name: string; rkey: string}
PostLikedBy: {name: string; rkey: string} PostLikedBy: {name: string; rkey: string}
@@ -40,6 +41,7 @@ export type CommonNavigatorParams = {
Hashtag: {tag: string; author?: string} Hashtag: {tag: string; author?: string}
MessagesConversation: {conversation: string; embed?: string} MessagesConversation: {conversation: string; embed?: string}
MessagesSettings: undefined MessagesSettings: undefined
Feeds: undefined
StarterPack: {name: string; rkey: string} StarterPack: {name: string; rkey: string}
StarterPackLanding: {name: string; rkey: string} StarterPackLanding: {name: string; rkey: string}
StarterPackWizard: { StarterPackWizard: {
@@ -51,7 +53,6 @@ export type CommonNavigatorParams = {
export type BottomTabNavigatorParams = CommonNavigatorParams & { export type BottomTabNavigatorParams = CommonNavigatorParams & {
HomeTab: undefined HomeTab: undefined
SearchTab: undefined SearchTab: undefined
FeedsTab: undefined
NotificationsTab: undefined NotificationsTab: undefined
MyProfileTab: undefined MyProfileTab: undefined
MessagesTab: undefined MessagesTab: undefined
@@ -65,10 +66,6 @@ export type SearchTabNavigatorParams = CommonNavigatorParams & {
Search: {q?: string} Search: {q?: string}
} }
export type FeedsTabNavigatorParams = CommonNavigatorParams & {
Feeds: undefined
}
export type NotificationsTabNavigatorParams = CommonNavigatorParams & { export type NotificationsTabNavigatorParams = CommonNavigatorParams & {
Notifications: undefined Notifications: undefined
} }
@@ -95,7 +92,6 @@ export type AllNavigatorParams = CommonNavigatorParams & {
Home: undefined Home: undefined
SearchTab: undefined SearchTab: undefined
Search: {q?: string} Search: {q?: string}
FeedsTab: undefined
Feeds: undefined Feeds: undefined
NotificationsTab: undefined NotificationsTab: undefined
Notifications: 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'], Profile: ['/profile/:name', '/profile/:name/rss'],
ProfileFollowers: '/profile/:name/followers', ProfileFollowers: '/profile/:name/followers',
ProfileFollows: '/profile/:name/follows', ProfileFollows: '/profile/:name/follows',
ProfileKnownFollowers: '/profile/:name/known-followers',
ProfileList: '/profile/:name/lists/:rkey', ProfileList: '/profile/:name/lists/:rkey',
PostThread: '/profile/:name/post/:rkey', PostThread: '/profile/:name/post/:rkey',
PostLikedBy: '/profile/:name/post/:rkey/liked-by', 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 {TID} from '@atproto/common-web'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {DISCOVER_SAVED_FEED, TIMELINE_SAVED_FEED} from '#/lib/constants' import {DISCOVER_SAVED_FEED, TIMELINE_SAVED_FEED} from '#/lib/constants'
import {isNative} from '#/platform/detection'
import {useOverwriteSavedFeedsMutation} from '#/state/queries/preferences' import {useOverwriteSavedFeedsMutation} from '#/state/queries/preferences'
import {UsePreferencesQueryResponse} from '#/state/queries/preferences' import {UsePreferencesQueryResponse} from '#/state/queries/preferences'
import {NavigationProp} from 'lib/routes/types'
import {CenteredView} from '#/view/com/util/Views' import {CenteredView} from '#/view/com/util/Views'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
@@ -26,7 +23,6 @@ export function NoFeedsPinned({
}) { }) {
const {_} = useLingui() const {_} = useLingui()
const headerOffset = useHeaderOffset() const headerOffset = useHeaderOffset()
const navigation = useNavigation<NavigationProp>()
const {isPending, mutateAsync: overwriteSavedFeeds} = const {isPending, mutateAsync: overwriteSavedFeeds} =
useOverwriteSavedFeedsMutation() useOverwriteSavedFeedsMutation()
@@ -66,15 +62,6 @@ export function NoFeedsPinned({
await overwriteSavedFeeds(toSave) await overwriteSavedFeeds(toSave)
}, [overwriteSavedFeeds, preferences.savedFeeds]) }, [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 ( return (
<CenteredView sideBorders style={[a.h_full_vh]}> <CenteredView sideBorders style={[a.h_full_vh]}>
<View <View
@@ -115,7 +102,6 @@ export function NoFeedsPinned({
<Link <Link
label={_(msg`Browse other feeds`)} label={_(msg`Browse other feeds`)}
to="/feeds" to="/feeds"
onPress={onPressFeedsLink}
size="medium" size="medium"
variant="solid" variant="solid"
color="secondary"> color="secondary">
@@ -1,5 +1,6 @@
import React, {useCallback} from 'react' import React, {useCallback} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {useKeyboardController} from 'react-native-keyboard-controller'
import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api' import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -34,6 +35,17 @@ export function MessagesConversationScreen({route}: Props) {
const convoId = route.params.conversation const convoId = route.params.conversation
const {setCurrentConvoId} = useCurrentConvoId() const {setCurrentConvoId} = useCurrentConvoId()
const {setEnabled} = useKeyboardController()
useFocusEffect(
useCallback(() => {
if (isWeb) return
setEnabled(true)
return () => {
setEnabled(false)
}
}, [setEnabled]),
)
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
setCurrentConvoId(convoId) 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 {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {useHaptics} from 'lib/haptics' import {useHaptics} from 'lib/haptics'
import {decrementBadgeCount} from 'lib/notifications/notifications'
import {logEvent} from 'lib/statsig/statsig' import {logEvent} from 'lib/statsig/statsig'
import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeDisplayName} from 'lib/strings/display-names'
import {TimeElapsed} from '#/view/com/util/TimeElapsed' import {TimeElapsed} from '#/view/com/util/TimeElapsed'
@@ -177,14 +178,16 @@ function ChatListItemReady({
const onPress = useCallback( const onPress = useCallback(
(e: GestureResponderEvent) => { (e: GestureResponderEvent) => {
decrementBadgeCount(convo.unreadCount)
if (isDeletedAccount) { if (isDeletedAccount) {
e.preventDefault() e.preventDefault()
menuControl.open()
return false return false
} else { } else {
logEvent('chat:open', {logContext: 'ChatsList'}) logEvent('chat:open', {logContext: 'ChatsList'})
} }
}, },
[isDeletedAccount], [convo.unreadCount, isDeletedAccount, menuControl],
) )
const onLongPress = useCallback(() => { const onLongPress = useCallback(() => {
@@ -206,7 +209,9 @@ function ChatListItemReady({
accessibilityHint={ accessibilityHint={
!isDeletedAccount !isDeletedAccount
? _(msg`Go to conversation with ${profile.handle}`) ? _(msg`Go to conversation with ${profile.handle}`)
: undefined : _(
msg`This conversation is with a deleted or a deactivated account. Press for options.`,
)
} }
accessibilityActions={ accessibilityActions={
isNative isNative
@@ -218,12 +223,7 @@ function ChatListItemReady({
} }
onPress={onPress} onPress={onPress}
onLongPress={isNative ? onLongPress : undefined} onLongPress={isNative ? onLongPress : undefined}
onAccessibilityAction={onLongPress} onAccessibilityAction={onLongPress}>
style={[
web({
cursor: isDeletedAccount ? 'default' : 'pointer',
}),
]}>
{({hovered, pressed, focused}) => ( {({hovered, pressed, focused}) => (
<View <View
style={[ style={[
@@ -233,9 +233,7 @@ function ChatListItemReady({
a.px_lg, a.px_lg,
a.py_md, a.py_md,
a.gap_md, a.gap_md,
(hovered || pressed || focused) && (hovered || pressed || focused) && t.atoms.bg_contrast_25,
!isDeletedAccount &&
t.atoms.bg_contrast_25,
t.atoms.border_contrast_low, t.atoms.border_contrast_low,
]}> ]}>
<UserAvatar <UserAvatar
@@ -30,6 +30,10 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {MessageProfileButton} from '#/components/dms/MessageProfileButton' import {MessageProfileButton} from '#/components/dms/MessageProfileButton'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {
KnownFollowers,
shouldShowKnownFollowers,
} from '#/components/KnownFollowers'
import * as Prompt from '#/components/Prompt' import * as Prompt from '#/components/Prompt'
import {RichText} from '#/components/RichText' import {RichText} from '#/components/RichText'
import {ProfileHeaderDisplayName} from './DisplayName' import {ProfileHeaderDisplayName} from './DisplayName'
@@ -268,6 +272,16 @@ let ProfileHeaderStandard = ({
/> />
</View> </View>
) : undefined} ) : 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> </View>
+1 -1
View File
@@ -83,7 +83,7 @@ let ProfileHeaderShell = ({
{!isPlaceholderProfile && ( {!isPlaceholderProfile && (
<View <View
style={[a.px_lg, a.pb_sm]} style={[a.px_lg, a.py_xs]}
pointerEvents={isIOS ? 'auto' : 'box-none'}> pointerEvents={isIOS ? 'auto' : 'box-none'}>
{isMe ? ( {isMe ? (
<LabelsOnMe details={{did: profile.did}} labels={profile.labels} /> <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(() => { 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(() => { React.useEffect(() => {
+4 -2
View File
@@ -169,11 +169,11 @@ const ModalContext = React.createContext<{
const ModalControlContext = React.createContext<{ const ModalControlContext = React.createContext<{
openModal: (modal: Modal) => void openModal: (modal: Modal) => void
closeModal: () => boolean closeModal: () => boolean
closeAllModals: () => void closeAllModals: () => boolean
}>({ }>({
openModal: () => {}, openModal: () => {},
closeModal: () => false, closeModal: () => false,
closeAllModals: () => {}, closeAllModals: () => false,
}) })
/** /**
@@ -206,7 +206,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
}) })
const closeAllModals = useNonReactiveCallback(() => { const closeAllModals = useNonReactiveCallback(() => {
let wasActive = activeModals.length > 0
setActiveModals([]) setActiveModals([])
return wasActive
}) })
unstable__openModal = openModal unstable__openModal = openModal
+1 -1
View File
@@ -28,7 +28,7 @@ export async function read(): Promise<Schema | undefined> {
code: e.code, code: e.code,
// @ts-ignore exists on some types // @ts-ignore exists on some types
expected: e?.expected, expected: e?.expected,
path: e.path, path: e.path?.join('.'),
})) || [] })) || []
logger.error(`persisted store: data failed validation on read`, {errors}) logger.error(`persisted store: data failed validation on read`, {errors})
return undefined 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 hasMore?: boolean
isParentLoading?: boolean isParentLoading?: boolean
isChildLoading?: boolean isChildLoading?: boolean
isSelfThread?: boolean
hasMoreSelfThread?: boolean
} }
export type ThreadPost = { export type ThreadPost = {
@@ -88,9 +90,11 @@ export function usePostThreadQuery(uri: string | undefined) {
gcTime: 0, gcTime: 0,
queryKey: RQKEY(uri || ''), queryKey: RQKEY(uri || ''),
async queryFn() { async queryFn() {
const res = await agent.getPostThread({uri: uri!}) const res = await agent.getPostThread({uri: uri!, depth: 10})
if (res.success) { if (res.success) {
return responseToThreadNodes(res.data.thread) const thread = responseToThreadNodes(res.data.thread)
annotateSelfThread(thread)
return thread
} }
return {type: 'unknown', uri: uri!} return {type: 'unknown', uri: uri!}
}, },
@@ -234,6 +238,8 @@ function responseToThreadNodes(
isHighlightedPost: depth === 0, isHighlightedPost: depth === 0,
hasMore: hasMore:
direction === 'down' && !node.replies?.length && !!node.replyCount, direction === 'down' && !node.replies?.length && !!node.replyCount,
isSelfThread: false, // populated `annotateSelfThread`
hasMoreSelfThread: false, // populated in `annotateSelfThread`
}, },
} }
} else if (AppBskyFeedDefs.isBlockedPost(node)) { } 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( function findPostInQueryData(
queryClient: QueryClient, queryClient: QueryClient,
uri: string, uri: string,
+4 -1
View File
@@ -3,7 +3,7 @@ import {InfiniteData, QueryKey, useInfiniteQuery} from '@tanstack/react-query'
import {useAgent} from '#/state/session' import {useAgent} from '#/state/session'
const PAGE_SIZE = 30 const PAGE_SIZE = 50
type RQPageParam = string | undefined type RQPageParam = string | undefined
// TODO refactor invalidate on mutate? // TODO refactor invalidate on mutate?
@@ -30,6 +30,9 @@ export function useProfileFeedgensQuery(
limit: PAGE_SIZE, limit: PAGE_SIZE,
cursor: pageParam, cursor: pageParam,
}) })
res.data.feeds.sort((a, b) => {
return (b.likeCount || 0) - (a.likeCount || 0)
})
return res.data return res.data
}, },
initialPageParam: undefined, initialPageParam: undefined,
+4 -3
View File
@@ -1,9 +1,10 @@
import {useCallback} from 'react' import {useCallback} from 'react'
import {useDialogStateControlContext} from '#/state/dialogs'
import {useLightboxControls} from './lightbox' import {useLightboxControls} from './lightbox'
import {useModalControls} from './modals' import {useModalControls} from './modals'
import {useComposerControls} from './shell/composer' import {useComposerControls} from './shell/composer'
import {useSetDrawerOpen} from './shell/drawer-open' import {useSetDrawerOpen} from './shell/drawer-open'
import {useDialogStateControlContext} from '#/state/dialogs'
/** /**
* returns true if something was closed * returns true if something was closed
@@ -22,10 +23,10 @@ export function useCloseAnyActiveElement() {
if (closeModal()) { if (closeModal()) {
return true return true
} }
if (closeComposer()) { if (closeAllDialogs()) {
return true return true
} }
if (closeAllDialogs()) { if (closeComposer()) {
return true return true
} }
setDrawerOpen(false) setDrawerOpen(false)
+192 -226
View File
@@ -8,17 +8,14 @@ import React, {
} from 'react' } from 'react'
import { import {
ActivityIndicator, ActivityIndicator,
BackHandler,
Keyboard, Keyboard,
KeyboardAvoidingView,
LayoutChangeEvent, LayoutChangeEvent,
StyleSheet, StyleSheet,
TouchableOpacity, TouchableOpacity,
View, View,
} from 'react-native' } from 'react-native'
import {
KeyboardAvoidingView,
KeyboardStickyView,
useKeyboardContext,
} from 'react-native-keyboard-controller'
import Animated, { import Animated, {
interpolateColor, interpolateColor,
useAnimatedStyle, useAnimatedStyle,
@@ -42,6 +39,7 @@ import {LikelyType} from '#/lib/link-meta/link-meta'
import {logEvent} from '#/lib/statsig/statsig' import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger' import {logger} from '#/logger'
import {emitPostCreated} from '#/state/events' import {emitPostCreated} from '#/state/events'
import {useModalControls} from '#/state/modals'
import {useModals} from '#/state/modals' import {useModals} from '#/state/modals'
import {useRequireAltTextEnabled} from '#/state/preferences' import {useRequireAltTextEnabled} from '#/state/preferences'
import { import {
@@ -108,9 +106,7 @@ export const ComposePost = observer(function ComposePost({
text: initText, text: initText,
imageUris: initImageUris, imageUris: initImageUris,
cancelRef, cancelRef,
isModalReady,
}: Props & { }: Props & {
isModalReady: boolean
cancelRef?: React.RefObject<CancelRef> cancelRef?: React.RefObject<CancelRef>
}) { }) {
const {currentAccount} = useSession() const {currentAccount} = useSession()
@@ -128,19 +124,9 @@ export const ComposePost = observer(function ComposePost({
const textInput = useRef<TextInputRef>(null) const textInput = useRef<TextInputRef>(null)
const discardPromptControl = Prompt.usePromptControl() const discardPromptControl = Prompt.usePromptControl()
const {closeAllDialogs} = useDialogStateControlContext() const {closeAllDialogs} = useDialogStateControlContext()
const {closeAllModals} = useModalControls()
const t = useTheme() 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 [isKeyboardVisible] = useIsKeyboardVisible({iosUseWillEvents: true})
const [isProcessing, setIsProcessing] = useState(false) const [isProcessing, setIsProcessing] = useState(false)
const [processingState, setProcessingState] = useState('') const [processingState, setProcessingState] = useState('')
@@ -180,6 +166,7 @@ export const ComposePost = observer(function ComposePost({
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
const viewStyles = useMemo( const viewStyles = useMemo(
() => ({ () => ({
paddingTop: isAndroid ? insets.top : 0,
paddingBottom: paddingBottom:
isAndroid || (isIOS && !isKeyboardVisible) ? insets.bottom : 0, isAndroid || (isIOS && !isKeyboardVisible) ? insets.bottom : 0,
}), }),
@@ -205,6 +192,26 @@ export const ComposePost = observer(function ComposePost({
useImperativeHandle(cancelRef, () => ({onPressCancel})) 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 // listen to escape key on desktop web
const onEscape = useCallback( const onEscape = useCallback(
(e: KeyboardEvent) => { (e: KeyboardEvent) => {
@@ -408,217 +415,176 @@ export const ComposePost = observer(function ComposePost({
bottomBarAnimatedStyle, bottomBarAnimatedStyle,
} = useAnimatedBorders() } = 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 ( return (
<> <KeyboardAvoidingView
<KeyboardAvoidingView testID="composePostView"
testID="composePostView" behavior={isIOS ? 'padding' : 'height'}
behavior="padding" keyboardVerticalOffset={isIOS ? 70 : 0}
style={a.flex_1} style={[a.flex_1]}>
keyboardVerticalOffset={replyTo ? 115 : isAndroid ? 180 : 162}> <View style={[a.flex_1, viewStyles]} aria-modal accessibilityViewIsModal>
<View <Animated.View style={topBarAnimatedStyle}>
style={[a.flex_1, viewStyles]} <View style={styles.topbarInner}>
aria-modal <TouchableOpacity
accessibilityViewIsModal> testID="composerDiscardButton"
<Animated.View style={topBarAnimatedStyle}> onPress={onPressCancel}
<View style={styles.topbarInner}> onAccessibilityEscape={onPressCancel}
<TouchableOpacity accessibilityRole="button"
testID="composerDiscardButton" accessibilityLabel={_(msg`Cancel`)}
onPress={onPressCancel} accessibilityHint={_(
onAccessibilityEscape={onPressCancel} msg`Closes post composer and discards post draft`,
accessibilityRole="button" )}
accessibilityLabel={_(msg`Cancel`)} hitSlop={HITSLOP_10}>
accessibilityHint={_( <Text style={[pal.link, s.f18]}>
msg`Closes post composer and discards post draft`, <Trans>Cancel</Trans>
)} </Text>
hitSlop={HITSLOP_10}> </TouchableOpacity>
<Text style={[pal.link, s.f18]}> <View style={a.flex_1} />
<Trans>Cancel</Trans> {isProcessing ? (
</Text> <>
</TouchableOpacity> <Text style={pal.textLight}>{processingState}</Text>
<View style={a.flex_1} /> <View style={styles.postBtn}>
{isProcessing ? ( <ActivityIndicator />
<> </View>
<Text style={pal.textLight}>{processingState}</Text> </>
<View style={styles.postBtn}> ) : (
<ActivityIndicator /> <>
</View> <LabelsBtn
</> labels={labels}
) : ( onChange={setLabels}
<> hasMedia={hasMedia}
<LabelsBtn />
labels={labels} {canPost ? (
onChange={setLabels} <TouchableOpacity
hasMedia={hasMedia} testID="composerPublishBtn"
/> onPress={onPressPublish}
{canPost ? ( accessibilityRole="button"
<TouchableOpacity accessibilityLabel={
testID="composerPublishBtn" replyTo ? _(msg`Publish reply`) : _(msg`Publish post`)
onPress={onPressPublish} }
accessibilityRole="button" accessibilityHint="">
accessibilityLabel={ <LinearGradient
replyTo ? _(msg`Publish reply`) : _(msg`Publish post`) colors={[
} gradients.blueLight.start,
accessibilityHint=""> gradients.blueLight.end,
<LinearGradient ]}
colors={[ start={{x: 0, y: 0}}
gradients.blueLight.start, end={{x: 1, y: 1}}
gradients.blueLight.end, style={styles.postBtn}>
]} <Text style={[s.white, s.f16, s.bold]}>
start={{x: 0, y: 0}} {replyTo ? (
end={{x: 1, y: 1}} <Trans context="action">Reply</Trans>
style={styles.postBtn}> ) : (
<Text style={[s.white, s.f16, s.bold]}> <Trans context="action">Post</Trans>
{replyTo ? ( )}
<Trans context="action">Reply</Trans>
) : (
<Trans context="action">Post</Trans>
)}
</Text>
</LinearGradient>
</TouchableOpacity>
) : (
<View style={[styles.postBtn, pal.btn]}>
<Text style={[pal.textLight, s.f16, s.bold]}>
<Trans context="action">Post</Trans>
</Text> </Text>
</View> </LinearGradient>
)} </TouchableOpacity>
</> ) : (
<View style={[styles.postBtn, pal.btn]}>
<Text style={[pal.textLight, s.f16, s.bold]}>
<Trans context="action">Post</Trans>
</Text>
</View>
)}
</>
)}
</View>
{isAltTextRequiredAndMissing && (
<View style={[styles.reminderLine, pal.viewLight]}>
<View style={styles.errorIcon}>
<FontAwesomeIcon
icon="exclamation"
style={{color: colors.red4}}
size={10}
/>
</View>
<Text style={[pal.text, a.flex_1]}>
<Trans>One or more images is missing alt text.</Trans>
</Text>
</View>
)}
{error !== '' && (
<View style={styles.errorLine}>
<View style={styles.errorIcon}>
<FontAwesomeIcon
icon="exclamation"
style={{color: colors.red4}}
size={10}
/>
</View>
<Text style={[s.red4, a.flex_1]}>{error}</Text>
</View>
)}
</Animated.View>
<Animated.ScrollView
onScroll={scrollHandler}
style={styles.scrollView}
keyboardShouldPersistTaps="always"
onContentSizeChange={onScrollViewContentSizeChange}
onLayout={onScrollViewLayout}>
{replyTo ? <ComposerReplyTo replyTo={replyTo} /> : undefined}
<View
style={[
styles.textInputLayout,
isNative && styles.textInputLayoutMobile,
]}>
<UserAvatar
avatar={currentProfile?.avatar}
size={50}
type={currentProfile?.associated?.labeler ? 'labeler' : 'user'}
/>
<TextInput
ref={textInput}
richtext={richtext}
placeholder={selectTextInputPlaceholder}
autoFocus
setRichText={setRichText}
onPhotoPasted={onPhotoPasted}
onPressPublish={onPressPublish}
onNewLink={onNewLink}
onError={setError}
accessible={true}
accessibilityLabel={_(msg`Write post`)}
accessibilityHint={_(
msg`Compose posts up to ${MAX_GRAPHEME_LENGTH} characters in length`,
)}
/>
</View>
<Gallery gallery={gallery} />
{gallery.isEmpty && extLink && (
<View style={a.relative}>
<ExternalEmbed
link={extLink}
gif={extGif}
onRemove={() => {
setExtLink(undefined)
setExtGif(undefined)
}}
/>
<GifAltText
link={extLink}
gif={extGif}
onSubmit={handleChangeGifAltText}
/>
</View>
)}
{quote ? (
<View style={[s.mt5, s.mb2, isWeb && s.mb10]}>
<View style={{pointerEvents: 'none'}}>
<QuoteEmbed quote={quote} />
</View>
{quote.uri !== initQuote?.uri && (
<QuoteX onRemove={() => setQuote(undefined)} />
)} )}
</View> </View>
) : undefined}
</Animated.ScrollView>
<SuggestedLanguage text={richtext.text} />
{isAltTextRequiredAndMissing && (
<View style={[styles.reminderLine, pal.viewLight]}>
<View style={styles.errorIcon}>
<FontAwesomeIcon
icon="exclamation"
style={{color: colors.red4}}
size={10}
/>
</View>
<Text style={[pal.text, a.flex_1]}>
<Trans>One or more images is missing alt text.</Trans>
</Text>
</View>
)}
{error !== '' && (
<View style={styles.errorLine}>
<View style={styles.errorIcon}>
<FontAwesomeIcon
icon="exclamation"
style={{color: colors.red4}}
size={10}
/>
</View>
<Text style={[s.red4, a.flex_1]}>{error}</Text>
</View>
)}
</Animated.View>
<Animated.ScrollView
onScroll={scrollHandler}
style={styles.scrollView}
keyboardShouldPersistTaps="always"
onContentSizeChange={onScrollViewContentSizeChange}
onLayout={onScrollViewLayout}>
{replyTo ? <ComposerReplyTo replyTo={replyTo} /> : undefined}
<View
style={[
styles.textInputLayout,
isNative && styles.textInputLayoutMobile,
]}>
<UserAvatar
avatar={currentProfile?.avatar}
size={50}
type={currentProfile?.associated?.labeler ? 'labeler' : 'user'}
/>
<TextInput
ref={textInput}
richtext={richtext}
placeholder={selectTextInputPlaceholder}
// fixes autofocus on android
key={
isAndroid ? (isModalReady ? 'ready' : 'animating') : 'static'
}
autoFocus={isAndroid ? isModalReady : true}
setRichText={setRichText}
onPhotoPasted={onPhotoPasted}
onPressPublish={onPressPublish}
onNewLink={onNewLink}
onError={setError}
accessible={true}
accessibilityLabel={_(msg`Write post`)}
accessibilityHint={_(
msg`Compose posts up to ${MAX_GRAPHEME_LENGTH} characters in length`,
)}
/>
</View>
<Gallery gallery={gallery} />
{gallery.isEmpty && extLink && (
<View style={a.relative}>
<ExternalEmbed
link={extLink}
gif={extGif}
onRemove={() => {
setExtLink(undefined)
setExtGif(undefined)
}}
/>
<GifAltText
link={extLink}
gif={extGif}
onSubmit={handleChangeGifAltText}
/>
</View>
)}
{quote ? (
<View style={[s.mt5, isWeb && s.mb10]}>
<View style={{pointerEvents: 'none'}}>
<QuoteEmbed quote={quote} />
</View>
{quote.uri !== initQuote?.uri && (
<QuoteX onRemove={() => setQuote(undefined)} />
)}
</View>
) : undefined}
</Animated.ScrollView>
<SuggestedLanguage text={richtext.text} />
</View>
</KeyboardAvoidingView>
<KeyboardStickyView
offset={{closed: isIOS ? -insets.bottom : 0, opened: 0}}>
{replyTo ? null : ( {replyTo ? null : (
<ThreadgateBtn <ThreadgateBtn
threadgate={threadgate} threadgate={threadgate}
@@ -657,7 +623,7 @@ export const ComposePost = observer(function ComposePost({
<SelectLangBtn /> <SelectLangBtn />
<CharProgress count={graphemeLength} /> <CharProgress count={graphemeLength} />
</View> </View>
</KeyboardStickyView> </View>
<Prompt.Basic <Prompt.Basic
control={discardPromptControl} control={discardPromptControl}
title={_(msg`Discard draft?`)} title={_(msg`Discard draft?`)}
@@ -666,7 +632,7 @@ export const ComposePost = observer(function ComposePost({
confirmButtonCta={_(msg`Discard`)} confirmButtonCta={_(msg`Discard`)}
confirmButtonColor="negative" confirmButtonColor="negative"
/> />
</> </KeyboardAvoidingView>
) )
}) })
-2
View File
@@ -20,7 +20,6 @@ import * as Dialog from '#/components/Dialog'
import * as TextField from '#/components/forms/TextField' import * as TextField from '#/components/forms/TextField'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {KeyboardPadding} from '#/components/KeyboardPadding'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {GifEmbed} from '../util/post-embeds/GifEmbed' import {GifEmbed} from '../util/post-embeds/GifEmbed'
import {AltTextReminder} from './photos/Gallery' import {AltTextReminder} from './photos/Gallery'
@@ -181,7 +180,6 @@ function AltTextInner({
</View> </View>
</View> </View>
<Dialog.Close /> <Dialog.Close />
<KeyboardPadding />
</Dialog.ScrollableInner> </Dialog.ScrollableInner>
) )
} }
+1 -7
View File
@@ -5,7 +5,6 @@ import {usePalette} from '#/lib/hooks/usePalette'
import {FeedSourceInfo} from '#/state/queries/feed' import {FeedSourceInfo} from '#/state/queries/feed'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {NavigationProp} from 'lib/routes/types' import {NavigationProp} from 'lib/routes/types'
import {isWeb} from 'platform/detection'
import {RenderTabBarFnProps} from 'view/com/pager/Pager' import {RenderTabBarFnProps} from 'view/com/pager/Pager'
import {TabBar} from '../pager/TabBar' import {TabBar} from '../pager/TabBar'
import {HomeHeaderLayout} from './HomeHeaderLayout' import {HomeHeaderLayout} from './HomeHeaderLayout'
@@ -39,12 +38,7 @@ export function HomeHeader(
}, [hasPinnedCustom, feeds]) }, [hasPinnedCustom, feeds])
const onPressFeedsLink = React.useCallback(() => { const onPressFeedsLink = React.useCallback(() => {
if (isWeb) { navigation.navigate('Feeds')
navigation.navigate('Feeds')
} else {
navigation.navigate('FeedsTab')
navigation.popToTop()
}
}, [navigation]) }, [navigation])
const onSelect = React.useCallback( const onSelect = React.useCallback(
+42 -31
View File
@@ -1,22 +1,18 @@
import React from 'react' import React from 'react'
import {StyleSheet, View} from 'react-native' import {StyleSheet, View} from 'react-native'
import Animated from 'react-native-reanimated' import Animated from 'react-native-reanimated'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {CogIcon} from '#/lib/icons'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {useShellLayout} from '#/state/shell/shell-layout' import {useShellLayout} from '#/state/shell/shell-layout'
import {useMinimalShellHeaderTransform} from 'lib/hooks/useMinimalShellTransform' import {useMinimalShellHeaderTransform} from 'lib/hooks/useMinimalShellTransform'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {Logo} from '#/view/icons/Logo' 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 {useKawaiiMode} from '../../../state/preferences/kawaii'
import {Link} from '../util/Link'
import {HomeHeaderLayoutMobile} from './HomeHeaderLayoutMobile' import {HomeHeaderLayoutMobile} from './HomeHeaderLayoutMobile'
export function HomeHeaderLayout(props: { export function HomeHeaderLayout(props: {
@@ -38,7 +34,7 @@ function HomeHeaderLayoutDesktopAndTablet({
children: React.ReactNode children: React.ReactNode
tabBarAnchor: JSX.Element | null | undefined tabBarAnchor: JSX.Element | null | undefined
}) { }) {
const pal = usePalette('default') const t = useTheme()
const headerMinimalShellTransform = useMinimalShellHeaderTransform() const headerMinimalShellTransform = useMinimalShellHeaderTransform()
const {headerHeight} = useShellLayout() const {headerHeight} = useShellLayout()
const {hasSession} = useSession() const {hasSession} = useSession()
@@ -51,31 +47,46 @@ function HomeHeaderLayoutDesktopAndTablet({
{hasSession && ( {hasSession && (
<View <View
style={[ style={[
pal.view, a.relative,
pal.border, 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.bar,
styles.topBar,
kawaii && {paddingTop: 4, paddingBottom: 0},
]}> ]}>
<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 <Link
href="/settings/following-feed" to="/feeds"
hitSlop={10} hitSlop={10}
accessibilityRole="button" label={_(msg`View your feeds and explore more`)}
accessibilityLabel={_(msg`Following Feed Preferences`)} size="small"
accessibilityHint=""> variant="ghost"
<FontAwesomeIcon color="secondary"
icon="sliders" shape="square"
style={pal.textLight as FontAwesomeIconStyle} style={[
/> a.justify_center,
</Link> {
<Logo width={kawaii ? 60 : 28} /> marginTop: -4,
<Link },
href="/settings/saved-feeds" ]}>
hitSlop={10} <FeedsIcon size="md" fill={t.atoms.text_contrast_medium.color} />
accessibilityRole="button"
accessibilityLabel={_(msg`Edit Saved Feeds`)}
accessibilityHint={_(msg`Opens screen to edit Saved Feeds`)}>
<CogIcon size={22} strokeWidth={2} style={pal.textLight} />
</Link> </Link>
</View> </View>
)} )}
@@ -85,8 +96,8 @@ function HomeHeaderLayoutDesktopAndTablet({
headerHeight.value = e.nativeEvent.layout.height headerHeight.value = e.nativeEvent.layout.height
}} }}
style={[ style={[
pal.view, t.atoms.bg,
pal.border, t.atoms.border_contrast_low,
styles.bar, styles.bar,
styles.tabBar, styles.tabBar,
headerMinimalShellTransform, headerMinimalShellTransform,
+25 -21
View File
@@ -1,8 +1,6 @@
import React from 'react' import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native' import {StyleSheet, TouchableOpacity, View} from 'react-native'
import Animated from 'react-native-reanimated' 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 {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -15,10 +13,13 @@ import {usePalette} from 'lib/hooks/usePalette'
import {isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
import {Logo} from '#/view/icons/Logo' import {Logo} from '#/view/icons/Logo'
import {atoms} from '#/alf' 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 {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 {IS_DEV} from '#/env'
import {Link} from '../util/Link'
export function HomeHeaderLayoutMobile({ export function HomeHeaderLayoutMobile({
children, children,
@@ -26,6 +27,7 @@ export function HomeHeaderLayoutMobile({
children: React.ReactNode children: React.ReactNode
tabBarAnchor: JSX.Element | null | undefined tabBarAnchor: JSX.Element | null | undefined
}) { }) {
const t = useTheme()
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const setDrawerOpen = useSetDrawerOpen() const setDrawerOpen = useSetDrawerOpen()
@@ -54,11 +56,7 @@ export function HomeHeaderLayoutMobile({
msg`Access profile and other navigation links`, msg`Access profile and other navigation links`,
)} )}
hitSlop={HITSLOP_10}> hitSlop={HITSLOP_10}>
<FontAwesomeIcon <Menu size="lg" fill={t.atoms.text_contrast_medium.color} />
icon="bars"
size={18}
color={pal.colors.textLight}
/>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<View> <View>
@@ -74,22 +72,28 @@ export function HomeHeaderLayoutMobile({
{width: 100}, {width: 100},
]}> ]}>
{IS_DEV && ( {IS_DEV && (
<Link2 to="/sys/debug"> <Link to="/sys/debug">
<ColorPalette size="md" /> <ColorPalette size="md" />
</Link2> </Link>
)} )}
{hasSession && ( {hasSession && (
<Link <Link
testID="viewHeaderHomeFeedPrefsBtn" testID="viewHeaderHomeFeedPrefsBtn"
href="/settings/following-feed" to="/feeds"
hitSlop={HITSLOP_10} hitSlop={HITSLOP_10}
accessibilityRole="button" label={_(msg`View your feeds and explore more`)}
accessibilityLabel={_(msg`Following Feed Preferences`)} size="small"
accessibilityHint=""> variant="ghost"
<FontAwesomeIcon color="secondary"
icon="sliders" shape="square"
style={pal.textLight as FontAwesomeIconStyle} style={[
/> a.justify_center,
{
marginTop: 2,
marginRight: -6,
},
]}>
<FeedsIcon size="lg" fill={t.atoms.text_contrast_medium.color} />
</Link> </Link>
)} )}
</View> </View>
@@ -113,8 +117,8 @@ const styles = StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
paddingHorizontal: 18, paddingHorizontal: 16,
paddingVertical: 8, paddingVertical: 5,
width: '100%', width: '100%',
}, },
title: { 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 * as Toast from '#/view/com/util/Toast'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
import * as Toggle from '#/components/forms/Toggle' import * as Toggle from '#/components/forms/Toggle'
import {KeyboardPadding} from '#/components/KeyboardPadding'
export const snapPoints = ['90%'] export const snapPoints = ['90%']
@@ -246,7 +245,6 @@ export function Component({}: {}) {
onPress={!appPassword ? createAppPassword : onDone} onPress={!appPassword ? createAppPassword : onDone}
/> />
</View> </View>
<KeyboardPadding />
</View> </View>
) )
} }
-2
View File
@@ -6,7 +6,6 @@ import BottomSheet from '@discord/bottom-sheet/src'
import {useModalControls, useModals} from '#/state/modals' import {useModalControls, useModals} from '#/state/modals'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {FullWindowOverlay} from '#/components/FullWindowOverlay' import {FullWindowOverlay} from '#/components/FullWindowOverlay'
import {KeyboardPadding} from '#/components/KeyboardPadding'
import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop' import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop'
import * as AddAppPassword from './AddAppPasswords' import * as AddAppPassword from './AddAppPasswords'
import * as AltImageModal from './AltImage' import * as AltImageModal from './AltImage'
@@ -147,7 +146,6 @@ export function ModalsContainer() {
handleStyle={[styles.handle, pal.view]} handleStyle={[styles.handle, pal.view]}
onChange={onBottomSheetChange}> onChange={onBottomSheetChange}>
{element} {element}
<KeyboardPadding />
</BottomSheet> </BottomSheet>
</Container> </Container>
) )
+4
View File
@@ -34,6 +34,7 @@ import {ComposePrompt} from '../composer/Prompt'
import {List, ListMethods} from '../util/List' import {List, ListMethods} from '../util/List'
import {ViewHeader} from '../util/ViewHeader' import {ViewHeader} from '../util/ViewHeader'
import {PostThreadItem} from './PostThreadItem' import {PostThreadItem} from './PostThreadItem'
import {PostThreadLoadMore} from './PostThreadLoadMore'
import {PostThreadShowHiddenReplies} from './PostThreadShowHiddenReplies' import {PostThreadShowHiddenReplies} from './PostThreadShowHiddenReplies'
// FlatList maintainVisibleContentPosition breaks if too many items // FlatList maintainVisibleContentPosition breaks if too many items
@@ -364,6 +365,9 @@ export function PostThread({
</View> </View>
) )
} else if (isThreadPost(item)) { } else if (isThreadPost(item)) {
if (!treeView && item.ctx.hasMoreSelfThread) {
return <PostThreadLoadMore post={item.post} />
}
const prev = isThreadPost(posts[index - 1]) const prev = isThreadPost(posts[index - 1])
? (posts[index - 1] as ThreadPost) ? (posts[index - 1] as ThreadPost)
: undefined : undefined
+2 -19
View File
@@ -30,11 +30,6 @@ import {useSession} from 'state/session'
import {PostThreadFollowBtn} from 'view/com/post-thread/PostThreadFollowBtn' import {PostThreadFollowBtn} from 'view/com/post-thread/PostThreadFollowBtn'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
import {RichText} from '#/components/RichText' import {RichText} from '#/components/RichText'
import {
isAvailable as isNativeTranslationAvailable,
isLanguageSupported,
NativeTranslationModule,
} from '../../../../modules/expo-bluesky-translate'
import {ContentHider} from '../../../components/moderation/ContentHider' import {ContentHider} from '../../../components/moderation/ContentHider'
import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe' import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe'
import {PostAlerts} from '../../../components/moderation/PostAlerts' import {PostAlerts} from '../../../components/moderation/PostAlerts'
@@ -344,7 +339,6 @@ let PostThreadItemLoaded = ({
</ContentHider> </ContentHider>
<ExpandedPostDetails <ExpandedPostDetails
post={post} post={post}
record={record}
translatorUrl={translatorUrl} translatorUrl={translatorUrl}
needsTranslation={needsTranslation} needsTranslation={needsTranslation}
/> />
@@ -653,12 +647,10 @@ function PostOuterWrapper({
function ExpandedPostDetails({ function ExpandedPostDetails({
post, post,
record,
needsTranslation, needsTranslation,
translatorUrl, translatorUrl,
}: { }: {
post: AppBskyFeedDefs.PostView post: AppBskyFeedDefs.PostView
record?: AppBskyFeedPost.Record
needsTranslation: boolean needsTranslation: boolean
translatorUrl: string translatorUrl: string
}) { }) {
@@ -666,18 +658,9 @@ function ExpandedPostDetails({
const {_} = useLingui() const {_} = useLingui()
const openLink = useOpenLink() const openLink = useOpenLink()
const text = record?.text || ''
const onTranslatePress = React.useCallback(() => { const onTranslatePress = React.useCallback(() => {
if ( openLink(translatorUrl)
isNativeTranslationAvailable && }, [openLink, translatorUrl])
isLanguageSupported(record?.langs?.at(0))
) {
NativeTranslationModule.presentAsync(text)
} else {
openLink(translatorUrl)
}
}, [openLink, text, translatorUrl, record])
return ( return (
<View style={[s.flexRow, s.mt2, s.mb10]}> <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>
)
}
+7 -11
View File
@@ -1,18 +1,19 @@
import React from 'react' import React from 'react'
import {StyleSheet, View} from 'react-native' import {StyleSheet, View} from 'react-native'
import {useNavigation} from '@react-navigation/native'
import { import {
FontAwesomeIcon, FontAwesomeIcon,
FontAwesomeIconStyle, FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome' } from '@fortawesome/react-native-fontawesome'
import {Text} from '../util/text/Text' import {Trans} from '@lingui/macro'
import {Button} from '../util/forms/Button' import {useNavigation} from '@react-navigation/native'
import {usePalette} from 'lib/hooks/usePalette'
import {MagnifyingGlassIcon} from 'lib/icons' import {MagnifyingGlassIcon} from 'lib/icons'
import {NavigationProp} from 'lib/routes/types' import {NavigationProp} from 'lib/routes/types'
import {usePalette} from 'lib/hooks/usePalette'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {isWeb} from 'platform/detection' 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() { export function FollowingEmptyState() {
const pal = usePalette('default') const pal = usePalette('default')
@@ -29,12 +30,7 @@ export function FollowingEmptyState() {
}, [navigation]) }, [navigation])
const onPressDiscoverFeeds = React.useCallback(() => { const onPressDiscoverFeeds = React.useCallback(() => {
if (isWeb) { navigation.navigate('Feeds')
navigation.navigate('Feeds')
} else {
navigation.navigate('FeedsTab')
navigation.popToTop()
}
}, [navigation]) }, [navigation])
return ( return (
+8 -12
View File
@@ -1,17 +1,18 @@
import React from 'react' import React from 'react'
import {StyleSheet, View, Dimensions} from 'react-native' import {Dimensions, StyleSheet, View} from 'react-native'
import {useNavigation} from '@react-navigation/native'
import { import {
FontAwesomeIcon, FontAwesomeIcon,
FontAwesomeIconStyle, FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome' } from '@fortawesome/react-native-fontawesome'
import {Text} from '../util/text/Text' import {Trans} from '@lingui/macro'
import {Button} from '../util/forms/Button' import {useNavigation} from '@react-navigation/native'
import {NavigationProp} from 'lib/routes/types'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {NavigationProp} from 'lib/routes/types'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {isWeb} from 'platform/detection' 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() { export function FollowingEndOfFeed() {
const pal = usePalette('default') const pal = usePalette('default')
@@ -28,12 +29,7 @@ export function FollowingEndOfFeed() {
}, [navigation]) }, [navigation])
const onPressDiscoverFeeds = React.useCallback(() => { const onPressDiscoverFeeds = React.useCallback(() => {
if (isWeb) { navigation.navigate('Feeds')
navigation.navigate('Feeds')
} else {
navigation.navigate('FeedsTab')
navigation.popToTop()
}
}, [navigation]) }, [navigation])
return ( return (
@@ -23,6 +23,7 @@ import {CenteredView} from '../util/Views'
import hairlineWidth = StyleSheet.hairlineWidth import hairlineWidth = StyleSheet.hairlineWidth
import {AppBskyGraphDefs} from '@atproto/api' import {AppBskyGraphDefs} from '@atproto/api'
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
import {StarterPackAvatarsIcon} from '#/components/StarterPack/StarterPackAvatarsIcon' import {StarterPackAvatarsIcon} from '#/components/StarterPack/StarterPackAvatarsIcon'
export function ProfileSubpageHeader({ export function ProfileSubpageHeader({
@@ -108,11 +109,7 @@ export function ProfileSubpageHeader({
style={[styles.backIcon, pal.text]} style={[styles.backIcon, pal.text]}
/> />
) : ( ) : (
<FontAwesomeIcon <Menu size="lg" style={[{marginTop: 4}, pal.textLight]} />
size={18}
icon="bars"
style={[styles.backIcon, pal.textLight]}
/>
)} )}
</Pressable> </Pressable>
<View style={{flex: 1}} /> <View style={{flex: 1}} />
@@ -203,7 +200,7 @@ const styles = StyleSheet.create({
backBtnWide: { backBtnWide: {
width: 20, width: 20,
height: 30, height: 30,
paddingHorizontal: 6, marginRight: 4,
}, },
backIcon: { backIcon: {
marginTop: 6, marginTop: 6,
+1 -1
View File
@@ -90,7 +90,7 @@ export function TestCtrls() {
/> />
<Pressable <Pressable
testID="e2eGotoFeeds" testID="e2eGotoFeeds"
onPress={() => navigate('FeedsTab')} onPress={() => navigate('Feeds')}
accessibilityRole="button" accessibilityRole="button"
style={BTN} style={BTN}
/> />
+37 -18
View File
@@ -5,10 +5,13 @@ import {
FontAwesomeIcon, FontAwesomeIcon,
FontAwesomeIconStyle, FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome' } 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 {usePalette} from 'lib/hooks/usePalette'
import {UserGroupIcon} from 'lib/icons'
import {isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
import {Growth_Stroke2_Corner0_Rounded as Growth} from '#/components/icons/Growth'
import {Text} from './text/Text'
export function EmptyState({ export function EmptyState({
testID, testID,
@@ -17,32 +20,41 @@ export function EmptyState({
style, style,
}: { }: {
testID?: string testID?: string
icon: IconProp | 'user-group' icon: IconProp | 'user-group' | 'growth'
message: string message: string
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const {isTabletOrDesktop} = useWebMediaQueries()
const iconSize = isTabletOrDesktop ? 80 : 64
return ( return (
<View <View
testID={testID} testID={testID}
style={[styles.container, isWeb && pal.border, style]}> style={[
<View style={styles.iconContainer}> styles.container,
isWeb && pal.border,
isTabletOrDesktop && {paddingRight: 20},
style,
]}>
<View
style={[
styles.iconContainer,
isTabletOrDesktop && styles.iconContainerBig,
pal.viewLight,
]}>
{icon === 'user-group' ? ( {icon === 'user-group' ? (
<UserGroupIcon size="64" style={styles.icon} /> <UserGroupIcon size={iconSize} />
) : icon === 'growth' ? (
<Growth width={iconSize} fill={pal.colors.emptyStateIcon} />
) : ( ) : (
<FontAwesomeIcon <FontAwesomeIcon
icon={icon} icon={icon}
size={64} size={iconSize}
style={[ style={[{color: pal.colors.emptyStateIcon} as FontAwesomeIconStyle]}
styles.icon,
{color: pal.colors.emptyStateIcon} as FontAwesomeIconStyle,
]}
/> />
)} )}
</View> </View>
<Text <Text type="xl" style={[{color: pal.colors.textLight}, styles.text]}>
type="xl-medium"
style={[{color: pal.colors.textVeryLight}, styles.text]}>
{message} {message}
</Text> </Text>
</View> </View>
@@ -51,16 +63,23 @@ export function EmptyState({
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
paddingVertical: 24,
paddingHorizontal: 36,
borderTopWidth: isWeb ? 1 : undefined, borderTopWidth: isWeb ? 1 : undefined,
}, },
iconContainer: { iconContainer: {
flexDirection: 'row', flexDirection: 'row',
}, alignItems: 'center',
icon: { justifyContent: 'center',
height: 100,
width: 100,
marginLeft: 'auto', marginLeft: 'auto',
marginRight: 'auto', marginRight: 'auto',
borderRadius: 80,
marginTop: 30,
},
iconContainerBig: {
width: 140,
height: 140,
marginTop: 50,
}, },
text: { text: {
textAlign: 'center', textAlign: 'center',
+9 -10
View File
@@ -8,13 +8,15 @@ import {
} from 'react-native' } from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {useNavigation} from '@react-navigation/native' 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 {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {useAnalytics} from 'lib/analytics/analytics'
import {NavigationProp} from 'lib/routes/types' import {NavigationProp} from 'lib/routes/types'
import {useSetDrawerOpen} from '#/state/shell' import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
import {isWeb} from '#/platform/detection' import {CenteredView} from './Views'
const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20} const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20}
@@ -72,11 +74,7 @@ export function SimpleViewHeader({
style={[styles.backIcon, pal.text]} style={[styles.backIcon, pal.text]}
/> />
) : ( ) : (
<FontAwesomeIcon <Menu size="lg" style={[{marginTop: 4}, pal.textLight]} />
size={18}
icon="bars"
style={[styles.backIcon, pal.textLight]}
/>
)} )}
</TouchableOpacity> </TouchableOpacity>
) : null} ) : null}
@@ -110,7 +108,8 @@ const styles = StyleSheet.create({
backBtnWide: { backBtnWide: {
width: 30, width: 30,
height: 30, height: 30,
paddingHorizontal: 6, paddingLeft: 4,
marginRight: 4,
}, },
backIcon: { backIcon: {
marginTop: 6, marginTop: 6,
+4 -6
View File
@@ -16,6 +16,7 @@ import {useTheme} from '#/alf'
import {Text} from './text/Text' import {Text} from './text/Text'
import {CenteredView} from './Views' import {CenteredView} from './Views'
import hairlineWidth = StyleSheet.hairlineWidth 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} const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20}
@@ -104,11 +105,7 @@ export function ViewHeader({
style={[styles.backIcon, pal.text]} style={[styles.backIcon, pal.text]}
/> />
) : !isTablet ? ( ) : !isTablet ? (
<FontAwesomeIcon <Menu size="lg" style={[{marginTop: 3}, pal.textLight]} />
size={18}
icon="bars"
style={[styles.backIcon, pal.textLight]}
/>
) : null} ) : null}
</TouchableOpacity> </TouchableOpacity>
) : null} ) : null}
@@ -275,7 +272,8 @@ const styles = StyleSheet.create({
backBtnWide: { backBtnWide: {
width: 30, width: 30,
height: 30, height: 30,
paddingHorizontal: 6, paddingLeft: 4,
marginRight: 4,
}, },
backIcon: { backIcon: {
marginTop: 6, marginTop: 6,
+2 -15
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 Menu from '#/components/Menu'
import * as Prompt from '#/components/Prompt' import * as Prompt from '#/components/Prompt'
import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog' import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
import {
isAvailable as isNativeTranslationAvailable,
isLanguageSupported,
NativeTranslationModule,
} from '../../../../../modules/expo-bluesky-translate'
import {EventStopper} from '../EventStopper' import {EventStopper} from '../EventStopper'
import * as Toast from '../Toast' import * as Toast from '../Toast'
@@ -188,16 +183,8 @@ let PostDropdownBtn = ({
}, [_, richText]) }, [_, richText])
const onPressTranslate = React.useCallback(() => { const onPressTranslate = React.useCallback(() => {
if ( openLink(translatorUrl)
isNativeTranslationAvailable && }, [openLink, translatorUrl])
isLanguageSupported(record?.langs?.at(0))
) {
const text = richTextToString(richText, true)
NativeTranslationModule.presentAsync(text)
} else {
openLink(translatorUrl)
}
}, [openLink, record?.langs, richText, translatorUrl])
const onHidePost = React.useCallback(() => { const onHidePost = React.useCallback(() => {
hidePost({uri: postUri}) hidePost({uri: postUri})
@@ -3,6 +3,7 @@ import {View} from 'react-native'
import {msg, plural} from '@lingui/macro' import {msg, plural} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {HITSLOP_10, HITSLOP_20} from '#/lib/constants'
import {useHaptics} from '#/lib/haptics' import {useHaptics} from '#/lib/haptics'
import {useRequireAuth} from '#/state/session' import {useRequireAuth} from '#/state/session'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
@@ -64,7 +65,8 @@ let RepostButton = ({
} (${plural(repostCount || 0, {one: '# repost', other: '# reposts'})})`} } (${plural(repostCount || 0, {one: '# repost', other: '# reposts'})})`}
shape="round" shape="round"
variant="ghost" variant="ghost"
color="secondary"> color="secondary"
hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
<Repost style={color} width={big ? 22 : 18} /> <Repost style={color} width={big ? 22 : 18} />
{typeof repostCount !== 'undefined' && repostCount > 0 ? ( {typeof repostCount !== 'undefined' && repostCount > 0 ? (
<Text <Text
+21 -48
View File
@@ -1,11 +1,5 @@
import React from 'react' import React from 'react'
import { import {ActivityIndicator, type FlatList, StyleSheet, View} from 'react-native'
ActivityIndicator,
type FlatList,
Pressable,
StyleSheet,
View,
} from 'react-native'
import {AppBskyActorDefs} from '@atproto/api' import {AppBskyActorDefs} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {FontAwesomeIconStyle} 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 {useSession} from '#/state/session'
import {useSetMinimalShellMode} from '#/state/shell' import {useSetMinimalShellMode} from '#/state/shell'
import {useComposerControls} from '#/state/shell/composer' import {useComposerControls} from '#/state/shell/composer'
import {HITSLOP_10} from 'lib/constants'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {CogIcon, ComposeIcon2, MagnifyingGlassIcon2} from 'lib/icons' import {ComposeIcon2} from 'lib/icons'
import {FeedsTabNavigatorParams, NativeStackScreenProps} from 'lib/routes/types' import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
import {cleanError} from 'lib/strings/errors' import {cleanError} from 'lib/strings/errors'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard' import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
import {ErrorMessage} from 'view/com/util/error/ErrorMessage' import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
import {FAB} from 'view/com/util/fab/FAB' import {FAB} from 'view/com/util/fab/FAB'
import {SearchInput, SearchInputRef} from 'view/com/util/forms/SearchInput' import {SearchInput} from 'view/com/util/forms/SearchInput'
import {Link} from 'view/com/util/Link' import {Link, TextLink} from 'view/com/util/Link'
import {List} from 'view/com/util/List' import {List} from 'view/com/util/List'
import { import {
FeedFeedLoadingPlaceholder, FeedFeedLoadingPlaceholder,
@@ -54,7 +47,7 @@ import {ListMagnifyingGlass_Stroke2_Corner0_Rounded} from '#/components/icons/Li
import {ListSparkle_Stroke2_Corner0_Rounded} from '#/components/icons/ListSparkle' import {ListSparkle_Stroke2_Corner0_Rounded} from '#/components/icons/ListSparkle'
import hairlineWidth = StyleSheet.hairlineWidth import hairlineWidth = StyleSheet.hairlineWidth
type Props = NativeStackScreenProps<FeedsTabNavigatorParams, 'Feeds'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'Feeds'>
type FlatlistSlice = type FlatlistSlice =
| { | {
@@ -159,7 +152,6 @@ export function FeedsScreen(_props: Props) {
} = useSearchPopularFeedsMutation() } = useSearchPopularFeedsMutation()
const {hasSession} = useSession() const {hasSession} = useSession()
const listRef = React.useRef<FlatList>(null) const listRef = React.useRef<FlatList>(null)
const searchInputRef = React.useRef<SearchInputRef>(null)
/** /**
* A search query is present. We may not have search results yet. * 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(() => { const renderHeaderBtn = React.useCallback(() => {
return ( return (
<View style={styles.headerBtnGroup}> <View style={styles.headerBtnGroup}>
<Pressable <TextLink
accessibilityRole="button" testID="editFeedsBtn"
hitSlop={HITSLOP_10} type="lg-medium"
onPress={searchInputRef.current?.focus}>
<MagnifyingGlassIcon2
size={22}
strokeWidth={2}
style={pal.textLight}
/>
</Pressable>
<Link
href="/settings/saved-feeds" href="/settings/saved-feeds"
hitSlop={10} accessibilityLabel={_(msg`Edit My Feeds`)}
accessibilityRole="button" accessibilityHint=""
accessibilityLabel={_(msg`Edit Saved Feeds`)} text={_(msg`Edit`)}
accessibilityHint={_(msg`Opens screen to edit Saved Feeds`)}> style={[pal.link, a.pr_xs]}
<CogIcon size={22} strokeWidth={2} style={pal.textLight} /> />
</Link>
</View> </View>
) )
}, [pal, _]) }, [pal, _])
@@ -480,22 +463,14 @@ export function FeedsScreen(_props: Props) {
<Trans>Feeds</Trans> <Trans>Feeds</Trans>
</Text> </Text>
<View style={styles.headerBtnGroup}> <View style={styles.headerBtnGroup}>
<Pressable <TextLink
accessibilityRole="button" type="lg"
hitSlop={HITSLOP_10}
onPress={searchInputRef.current?.focus}>
<MagnifyingGlassIcon2
size={22}
strokeWidth={2}
style={pal.icon}
/>
</Pressable>
<Link
href="/settings/saved-feeds" href="/settings/saved-feeds"
accessibilityLabel={_(msg`Edit My Feeds`)} accessibilityLabel={_(msg`Edit My Feeds`)}
accessibilityHint=""> accessibilityHint=""
<CogIcon strokeWidth={1.5} style={pal.icon} size={28} /> text={_(msg`Edit`)}
</Link> style={[pal.link]}
/>
</View> </View>
</View> </View>
)} )}
@@ -522,7 +497,6 @@ export function FeedsScreen(_props: Props) {
<FeedsAboutHeader /> <FeedsAboutHeader />
<View style={{paddingHorizontal: 12, paddingBottom: 12}}> <View style={{paddingHorizontal: 12, paddingBottom: 12}}>
<SearchInput <SearchInput
ref={searchInputRef}
query={query} query={query}
onChangeQuery={onChangeQuery} onChangeQuery={onChangeQuery}
onPressCancelSearch={onPressCancelSearch} onPressCancelSearch={onPressCancelSearch}
@@ -577,8 +551,8 @@ export function FeedsScreen(_props: Props) {
pal.view, pal.view,
pal.border, pal.border,
pal.text, pal.text,
pal.icon,
pal.textLight, pal.textLight,
pal.link,
_, _,
query, query,
onChangeQuery, onChangeQuery,
@@ -594,7 +568,6 @@ export function FeedsScreen(_props: Props) {
{isMobile && ( {isMobile && (
<ViewHeader <ViewHeader
title={_(msg`Feeds`)} title={_(msg`Feeds`)}
canGoBack={false}
renderButton={renderHeaderBtn} renderButton={renderHeaderBtn}
showBorder showBorder
/> />
+20 -4
View File
@@ -30,6 +30,7 @@ import {TextLink} from 'view/com/util/Link'
import {ListMethods} from 'view/com/util/List' import {ListMethods} from 'view/com/util/List'
import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
import {CenteredView} from 'view/com/util/Views' import {CenteredView} from 'view/com/util/Views'
import {Loader} from '#/components/Loader'
import {Feed} from '../com/notifications/Feed' import {Feed} from '../com/notifications/Feed'
import {FAB} from '../com/util/fab/FAB' import {FAB} from '../com/util/fab/FAB'
import {MainScrollProvider} from '../com/util/MainScrollProvider' import {MainScrollProvider} from '../com/util/MainScrollProvider'
@@ -43,6 +44,7 @@ export function NotificationsScreen({}: Props) {
const {_} = useLingui() const {_} = useLingui()
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const [isScrolledDown, setIsScrolledDown] = React.useState(false) const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const [isLoadingLatest, setIsLoadingLatest] = React.useState(false)
const scrollElRef = React.useRef<ListMethods>(null) const scrollElRef = React.useRef<ListMethods>(null)
const {screen} = useAnalytics() const {screen} = useAnalytics()
const pal = usePalette('default') const pal = usePalette('default')
@@ -68,9 +70,13 @@ export function NotificationsScreen({}: Props) {
truncateAndInvalidate(queryClient, NOTIFS_RQKEY()) truncateAndInvalidate(queryClient, NOTIFS_RQKEY())
} else { } else {
// check with the server // 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(() => { const onFocusCheckLatest = useNonReactiveCallback(() => {
// on focus, check for latest, but only invalidate if the user // on focus, check for latest, but only invalidate if the user
@@ -139,21 +145,31 @@ export function NotificationsScreen({}: Props) {
} }
onPress={emitSoftReset} onPress={emitSoftReset}
/> />
{isLoadingLatest ? <Loader size="md" /> : <></>}
</View> </View>
) )
} }
return <></> 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 ( return (
<CenteredView <CenteredView
testID="notificationsScreen" testID="notificationsScreen"
style={s.hContentRegion} style={[s.hContentRegion, {paddingTop: 2}]}
sideBorders={true}> sideBorders={true}>
<ViewHeader <ViewHeader
title={_(msg`Notifications`)} title={_(msg`Notifications`)}
canGoBack={false} canGoBack={false}
showBorder={true} showBorder={true}
renderButton={renderHeaderSpinner}
/> />
<MainScrollProvider> <MainScrollProvider>
<Feed <Feed
+1 -1
View File
@@ -468,7 +468,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
}, [onScrollToTop, isScreenFocused]) }, [onScrollToTop, isScreenFocused])
const renderPostsEmpty = useCallback(() => { const renderPostsEmpty = useCallback(() => {
return <EmptyState icon="feed" message={_(msg`This feed is empty!`)} /> return <EmptyState icon="hashtag" message={_(msg`This feed is empty.`)} />
}, [_]) }, [_])
return ( return (
+1 -1
View File
@@ -726,7 +726,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
}, [onScrollToTop, isScreenFocused]) }, [onScrollToTop, isScreenFocused])
const renderPostsEmpty = useCallback(() => { const renderPostsEmpty = useCallback(() => {
return <EmptyState icon="feed" message={_(msg`This feed is empty!`)} /> return <EmptyState icon="hashtag" message={_(msg`This feed is empty.`)} />
}, [_]) }, [_])
return ( 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 {SearchLinkCard, SearchProfileCard} from '#/view/shell/desktop/Search'
import {ProfileCardFeedLoadingPlaceholder} from 'view/com/util/LoadingPlaceholder' import {ProfileCardFeedLoadingPlaceholder} from 'view/com/util/LoadingPlaceholder'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
function Loader() { function Loader() {
const pal = usePalette('default') const pal = usePalette('default')
@@ -712,11 +713,7 @@ export function SearchScreen(
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Menu`)} accessibilityLabel={_(msg`Menu`)}
accessibilityHint={_(msg`Access navigation links and settings`)}> accessibilityHint={_(msg`Access navigation links and settings`)}>
<FontAwesomeIcon <Menu size="lg" fill={pal.colors.textLight} />
icon="bars"
size={18}
color={pal.colors.textLight}
/>
</Pressable> </Pressable>
)} )}
<SearchInputBox <SearchInputBox
@@ -1073,13 +1070,14 @@ function scrollToTopWeb() {
} }
} }
const HEADER_HEIGHT = 50 const HEADER_HEIGHT = 46
const styles = StyleSheet.create({ const styles = StyleSheet.create({
header: { header: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
paddingHorizontal: 12, paddingHorizontal: 12,
paddingLeft: 13,
paddingVertical: 4, paddingVertical: 4,
height: HEADER_HEIGHT, height: HEADER_HEIGHT,
// @ts-ignore web only // @ts-ignore web only
@@ -1092,7 +1090,6 @@ const styles = StyleSheet.create({
height: 30, height: 30,
borderRadius: 30, borderRadius: 30,
marginRight: 6, marginRight: 6,
paddingBottom: 2,
alignItems: 'center', alignItems: 'center',
justifyContent: '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
}
+58 -101
View File
@@ -1,116 +1,73 @@
import React, {useLayoutEffect, useState} from 'react' import React, {useEffect} from 'react'
import {Modal, View} from 'react-native' import {Animated, Easing, StyleSheet, 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 {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {isIOS} from '#/platform/detection' import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
import {Provider as LegacyModalProvider} from '#/state/modals' import {usePalette} from 'lib/hooks/usePalette'
import {useComposerState} from '#/state/shell/composer' import {useComposerState} from 'state/shell/composer'
import {ModalsContainer as LegacyModalsContainer} from '#/view/com/modals/Modal' import {ComposePost} from '../com/composer/Composer'
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'
export const Composer = observer(function ComposerImpl({}: { export const Composer = observer(function ComposerImpl({
winHeight,
}: {
winHeight: number winHeight: number
}) { }) {
const t = useTheme()
const state = useComposerState() const state = useComposerState()
const ref = useComposerCancelRef() const pal = usePalette('default')
const [isModalReady, setIsModalReady] = useState(false) const initInterp = useAnimatedValue(0)
const open = !!state useEffect(() => {
const [prevOpen, setPrevOpen] = useState(open) if (state) {
if (open !== prevOpen) { Animated.timing(initInterp, {
setPrevOpen(open) toValue: 1,
if (!open) { duration: 300,
setIsModalReady(false) 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 ( return (
<Modal <Animated.View
style={[styles.wrapper, pal.view, wrapperAnimStyle]}
aria-modal aria-modal
accessibilityViewIsModal accessibilityViewIsModal>
visible={open} <ComposePost
presentationStyle="formSheet" replyTo={state.replyTo}
animationType="slide" onPost={state.onPost}
onShow={() => setIsModalReady(true)} quote={state.quote}
onRequestClose={() => ref.current?.onPressCancel()}> mention={state.mention}
<View style={[t.atoms.bg, a.flex_1]}> text={state.text}
<Providers open={open}> imageUris={state.imageUris}
<ComposePost />
isModalReady={isModalReady} </Animated.View>
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({ const styles = StyleSheet.create({
children, wrapper: {
open, position: 'absolute',
}: { top: 0,
children: React.ReactNode bottom: 0,
open: boolean width: '100%',
}) { },
// 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
}
-1
View File
@@ -56,7 +56,6 @@ export function Composer({}: {winHeight: number}) {
t.atoms.border_contrast_medium, t.atoms.border_contrast_medium,
]}> ]}>
<ComposePost <ComposePost
isModalReady={true}
replyTo={state.replyTo} replyTo={state.replyTo}
quote={state.quote} quote={state.quote}
onPost={state.onPost} onPost={state.onPost}
+5 -4
View File
@@ -186,10 +186,11 @@ let DrawerContent = ({}: {}): React.ReactNode => {
onPressTab('MyProfile') onPressTab('MyProfile')
}, [onPressTab]) }, [onPressTab])
const onPressMyFeeds = React.useCallback( const onPressMyFeeds = React.useCallback(() => {
() => onPressTab('Feeds'), track('Menu:ItemClicked', {url: 'Feeds'})
[onPressTab], navigation.navigate('Feeds')
) setDrawerOpen(false)
}, [navigation, setDrawerOpen, track])
const onPressLists = React.useCallback(() => { const onPressLists = React.useCallback(() => {
track('Menu:ItemClicked', {url: 'Lists'}) track('Menu:ItemClicked', {url: 'Lists'})

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