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