Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| df5ff1b72e | |||
| 54bf784faf |
@@ -1,5 +1,3 @@
|
||||
import {it, describe, expect} from '@jest/globals'
|
||||
|
||||
import {
|
||||
linkRequiresWarning,
|
||||
isPossiblyAUrl,
|
||||
@@ -8,7 +6,6 @@ import {
|
||||
|
||||
describe('linkRequiresWarning', () => {
|
||||
type Case = [string, string, boolean]
|
||||
|
||||
const cases: Case[] = [
|
||||
['http://example.com', 'http://example.com', false],
|
||||
['http://example.com', 'example.com', false],
|
||||
@@ -67,10 +64,6 @@ describe('linkRequiresWarning', () => {
|
||||
['http://bsky.app/', 'https://google.com', true],
|
||||
['https://bsky.app/', 'https://google.com', true],
|
||||
|
||||
// case insensitive
|
||||
['https://Example.com', 'example.com', false],
|
||||
['https://example.com', 'Example.com', false],
|
||||
|
||||
// bad uri inputs, default to true
|
||||
['', '', true],
|
||||
['example.com', 'example.com', true],
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
import {configure} from '@testing-library/react-native'
|
||||
import 'react-native-gesture-handler/jestSetup'
|
||||
|
||||
// IMPORTANT: this is what's used in the native runtime
|
||||
import 'react-native-url-polyfill/auto'
|
||||
|
||||
configure({asyncUtilTimeout: 20000})
|
||||
|
||||
jest.mock('@react-native-async-storage/async-storage', () =>
|
||||
|
||||
+22
-25
@@ -28,8 +28,6 @@ import {Provider as LightboxStateProvider} from 'state/lightbox'
|
||||
import {Provider as MutedThreadsProvider} from 'state/muted-threads'
|
||||
import {Provider as InvitesStateProvider} from 'state/invites'
|
||||
import {Provider as PrefsStateProvider} from 'state/preferences'
|
||||
import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out'
|
||||
import I18nProvider from './locale/i18nProvider'
|
||||
import {
|
||||
Provider as SessionProvider,
|
||||
useSession,
|
||||
@@ -37,13 +35,18 @@ import {
|
||||
} from 'state/session'
|
||||
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
|
||||
import * as persisted from '#/state/persisted'
|
||||
import {i18n} from '@lingui/core'
|
||||
import {I18nProvider} from '@lingui/react'
|
||||
import {messages} from './locale/locales/en/messages'
|
||||
i18n.load('en', messages)
|
||||
i18n.activate('en')
|
||||
|
||||
enableFreeze(true)
|
||||
SplashScreen.preventAutoHideAsync()
|
||||
|
||||
function InnerApp() {
|
||||
const colorMode = useColorMode()
|
||||
const {isInitialLoad, currentAccount} = useSession()
|
||||
const {isInitialLoad} = useSession()
|
||||
const {resumeSession} = useSessionApi()
|
||||
|
||||
// init
|
||||
@@ -70,25 +73,21 @@ function InnerApp() {
|
||||
*/
|
||||
|
||||
return (
|
||||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<LoggedOutViewProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<ThemeProvider theme={colorMode}>
|
||||
<analytics.Provider>
|
||||
{/* All components should be within this provider */}
|
||||
<RootSiblingParent>
|
||||
<GestureHandlerRootView style={s.h100pct}>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
</GestureHandlerRootView>
|
||||
</RootSiblingParent>
|
||||
</analytics.Provider>
|
||||
</ThemeProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</React.Fragment>
|
||||
<UnreadNotifsProvider>
|
||||
<ThemeProvider theme={colorMode}>
|
||||
<analytics.Provider>
|
||||
<I18nProvider i18n={i18n}>
|
||||
{/* All components should be within this provider */}
|
||||
<RootSiblingParent>
|
||||
<GestureHandlerRootView style={s.h100pct}>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
</GestureHandlerRootView>
|
||||
</RootSiblingParent>
|
||||
</I18nProvider>
|
||||
</analytics.Provider>
|
||||
</ThemeProvider>
|
||||
</UnreadNotifsProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -116,9 +115,7 @@ function App() {
|
||||
<InvitesStateProvider>
|
||||
<ModalStateProvider>
|
||||
<LightboxStateProvider>
|
||||
<I18nProvider>
|
||||
<InnerApp />
|
||||
</I18nProvider>
|
||||
<InnerApp />
|
||||
</LightboxStateProvider>
|
||||
</ModalStateProvider>
|
||||
</InvitesStateProvider>
|
||||
|
||||
+22
-25
@@ -16,14 +16,15 @@ import {Shell} from 'view/shell/index'
|
||||
import {ToastContainer} from 'view/com/util/Toast.web'
|
||||
import {ThemeProvider} from 'lib/ThemeContext'
|
||||
import {queryClient} from 'lib/react-query'
|
||||
import {i18n} from '@lingui/core'
|
||||
import {I18nProvider} from '@lingui/react'
|
||||
import {defaultLocale, dynamicActivate} from './locale/i18n'
|
||||
import {Provider as ShellStateProvider} from 'state/shell'
|
||||
import {Provider as ModalStateProvider} from 'state/modals'
|
||||
import {Provider as LightboxStateProvider} from 'state/lightbox'
|
||||
import {Provider as MutedThreadsProvider} from 'state/muted-threads'
|
||||
import {Provider as InvitesStateProvider} from 'state/invites'
|
||||
import {Provider as PrefsStateProvider} from 'state/preferences'
|
||||
import {Provider as LoggedOutViewProvider} from 'state/shell/logged-out'
|
||||
import I18nProvider from './locale/i18nProvider'
|
||||
import {
|
||||
Provider as SessionProvider,
|
||||
useSession,
|
||||
@@ -35,7 +36,7 @@ import * as persisted from '#/state/persisted'
|
||||
enableFreeze(true)
|
||||
|
||||
function InnerApp() {
|
||||
const {isInitialLoad, currentAccount} = useSession()
|
||||
const {isInitialLoad} = useSession()
|
||||
const {resumeSession} = useSessionApi()
|
||||
const colorMode = useColorMode()
|
||||
|
||||
@@ -43,6 +44,8 @@ function InnerApp() {
|
||||
useEffect(() => {
|
||||
initReminders()
|
||||
analytics.init()
|
||||
dynamicActivate(defaultLocale) // async import of locale data
|
||||
|
||||
const account = persisted.get('session').currentAccount
|
||||
resumeSession(account)
|
||||
}, [resumeSession])
|
||||
@@ -58,25 +61,21 @@ function InnerApp() {
|
||||
*/
|
||||
|
||||
return (
|
||||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<LoggedOutViewProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<ThemeProvider theme={colorMode}>
|
||||
<analytics.Provider>
|
||||
{/* All components should be within this provider */}
|
||||
<RootSiblingParent>
|
||||
<SafeAreaProvider>
|
||||
<Shell />
|
||||
</SafeAreaProvider>
|
||||
</RootSiblingParent>
|
||||
<ToastContainer />
|
||||
</analytics.Provider>
|
||||
</ThemeProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</React.Fragment>
|
||||
<UnreadNotifsProvider>
|
||||
<ThemeProvider theme={colorMode}>
|
||||
<analytics.Provider>
|
||||
<I18nProvider i18n={i18n}>
|
||||
{/* All components should be within this provider */}
|
||||
<RootSiblingParent>
|
||||
<SafeAreaProvider>
|
||||
<Shell />
|
||||
</SafeAreaProvider>
|
||||
</RootSiblingParent>
|
||||
</I18nProvider>
|
||||
<ToastContainer />
|
||||
</analytics.Provider>
|
||||
</ThemeProvider>
|
||||
</UnreadNotifsProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -104,9 +103,7 @@ function App() {
|
||||
<InvitesStateProvider>
|
||||
<ModalStateProvider>
|
||||
<LightboxStateProvider>
|
||||
<I18nProvider>
|
||||
<InnerApp />
|
||||
</I18nProvider>
|
||||
<InnerApp />
|
||||
</LightboxStateProvider>
|
||||
</ModalStateProvider>
|
||||
</InvitesStateProvider>
|
||||
|
||||
+23
-25
@@ -9,6 +9,7 @@ import {
|
||||
DefaultTheme,
|
||||
DarkTheme,
|
||||
} from '@react-navigation/native'
|
||||
import {createNativeStackNavigator} from '@react-navigation/native-stack'
|
||||
import {
|
||||
BottomTabBarProps,
|
||||
createBottomTabNavigator,
|
||||
@@ -68,18 +69,16 @@ import {ModerationBlockedAccounts} from 'view/screens/ModerationBlockedAccounts'
|
||||
import {SavedFeeds} from 'view/screens/SavedFeeds'
|
||||
import {PreferencesHomeFeed} from 'view/screens/PreferencesHomeFeed'
|
||||
import {PreferencesThreads} from 'view/screens/PreferencesThreads'
|
||||
import {createNativeStackNavigatorWithAuth} from './view/shell/createNativeStackNavigatorWithAuth'
|
||||
|
||||
const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
|
||||
|
||||
const HomeTab = createNativeStackNavigatorWithAuth<HomeTabNavigatorParams>()
|
||||
const SearchTab = createNativeStackNavigatorWithAuth<SearchTabNavigatorParams>()
|
||||
const FeedsTab = createNativeStackNavigatorWithAuth<FeedsTabNavigatorParams>()
|
||||
const HomeTab = createNativeStackNavigator<HomeTabNavigatorParams>()
|
||||
const SearchTab = createNativeStackNavigator<SearchTabNavigatorParams>()
|
||||
const FeedsTab = createNativeStackNavigator<FeedsTabNavigatorParams>()
|
||||
const NotificationsTab =
|
||||
createNativeStackNavigatorWithAuth<NotificationsTabNavigatorParams>()
|
||||
const MyProfileTab =
|
||||
createNativeStackNavigatorWithAuth<MyProfileTabNavigatorParams>()
|
||||
const Flat = createNativeStackNavigatorWithAuth<FlatNavigatorParams>()
|
||||
createNativeStackNavigator<NotificationsTabNavigatorParams>()
|
||||
const MyProfileTab = createNativeStackNavigator<MyProfileTabNavigatorParams>()
|
||||
const Flat = createNativeStackNavigator<FlatNavigatorParams>()
|
||||
const Tab = createBottomTabNavigator<BottomTabNavigatorParams>()
|
||||
|
||||
/**
|
||||
@@ -98,37 +97,37 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
||||
<Stack.Screen
|
||||
name="Lists"
|
||||
component={ListsScreen}
|
||||
options={{title: title('Lists'), requireAuth: true}}
|
||||
options={{title: title('Lists')}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Moderation"
|
||||
getComponent={() => ModerationScreen}
|
||||
options={{title: title('Moderation'), requireAuth: true}}
|
||||
options={{title: title('Moderation')}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="ModerationModlists"
|
||||
getComponent={() => ModerationModlistsScreen}
|
||||
options={{title: title('Moderation Lists'), requireAuth: true}}
|
||||
options={{title: title('Moderation Lists')}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="ModerationMutedAccounts"
|
||||
getComponent={() => ModerationMutedAccounts}
|
||||
options={{title: title('Muted Accounts'), requireAuth: true}}
|
||||
options={{title: title('Muted Accounts')}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="ModerationBlockedAccounts"
|
||||
getComponent={() => ModerationBlockedAccounts}
|
||||
options={{title: title('Blocked Accounts'), requireAuth: true}}
|
||||
options={{title: title('Blocked Accounts')}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Settings"
|
||||
getComponent={() => SettingsScreen}
|
||||
options={{title: title('Settings'), requireAuth: true}}
|
||||
options={{title: title('Settings')}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="LanguageSettings"
|
||||
getComponent={() => LanguageSettingsScreen}
|
||||
options={{title: title('Language Settings'), requireAuth: true}}
|
||||
options={{title: title('Language Settings')}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Profile"
|
||||
@@ -155,7 +154,7 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
||||
<Stack.Screen
|
||||
name="ProfileList"
|
||||
getComponent={() => ProfileListScreen}
|
||||
options={{title: title('List'), requireAuth: true}}
|
||||
options={{title: title('List')}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="PostThread"
|
||||
@@ -185,12 +184,12 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
||||
<Stack.Screen
|
||||
name="Debug"
|
||||
getComponent={() => DebugScreen}
|
||||
options={{title: title('Debug'), requireAuth: true}}
|
||||
options={{title: title('Debug')}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Log"
|
||||
getComponent={() => LogScreen}
|
||||
options={{title: title('Log'), requireAuth: true}}
|
||||
options={{title: title('Log')}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Support"
|
||||
@@ -220,22 +219,22 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
||||
<Stack.Screen
|
||||
name="AppPasswords"
|
||||
getComponent={() => AppPasswords}
|
||||
options={{title: title('App Passwords'), requireAuth: true}}
|
||||
options={{title: title('App Passwords')}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="SavedFeeds"
|
||||
getComponent={() => SavedFeeds}
|
||||
options={{title: title('Edit My Feeds'), requireAuth: true}}
|
||||
options={{title: title('Edit My Feeds')}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="PreferencesHomeFeed"
|
||||
getComponent={() => PreferencesHomeFeed}
|
||||
options={{title: title('Home Feed Preferences'), requireAuth: true}}
|
||||
options={{title: title('Home Feed Preferences')}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="PreferencesThreads"
|
||||
getComponent={() => PreferencesThreads}
|
||||
options={{title: title('Threads Preferences'), requireAuth: true}}
|
||||
options={{title: title('Threads Preferences')}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
@@ -340,7 +339,6 @@ function NotificationsTabNavigator() {
|
||||
<NotificationsTab.Screen
|
||||
name="Notifications"
|
||||
getComponent={() => NotificationsScreen}
|
||||
options={{requireAuth: true}}
|
||||
/>
|
||||
{commonScreens(NotificationsTab as typeof HomeTab)}
|
||||
</NotificationsTab.Navigator>
|
||||
@@ -359,8 +357,8 @@ function MyProfileTabNavigator() {
|
||||
contentStyle,
|
||||
}}>
|
||||
<MyProfileTab.Screen
|
||||
// @ts-ignore // TODO: fix this broken type in ProfileScreen
|
||||
name="MyProfile"
|
||||
// @ts-ignore // TODO: fix this broken type in ProfileScreen
|
||||
getComponent={() => ProfileScreen}
|
||||
initialParams={{
|
||||
name: 'me',
|
||||
@@ -407,7 +405,7 @@ const FlatNavigator = () => {
|
||||
<Flat.Screen
|
||||
name="Notifications"
|
||||
getComponent={() => NotificationsScreen}
|
||||
options={{title: title('Notifications'), requireAuth: true}}
|
||||
options={{title: title('Notifications')}}
|
||||
/>
|
||||
{commonScreens(Flat as typeof HomeTab, numUnread)}
|
||||
</Flat.Navigator>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export {unstable_batchedUpdates as batchedUpdates} from 'react-native'
|
||||
@@ -1,2 +0,0 @@
|
||||
// @ts-ignore
|
||||
export {unstable_batchedUpdates as batchedUpdates} from 'react-dom'
|
||||
@@ -3,7 +3,6 @@ import {useCallback, useEffect} from 'react'
|
||||
import {AppState} from 'react-native'
|
||||
import {logger} from '#/logger'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {t} from '@lingui/macro'
|
||||
|
||||
export function useOTAUpdate() {
|
||||
const {openModal} = useModalControls()
|
||||
@@ -12,8 +11,9 @@ export function useOTAUpdate() {
|
||||
const showUpdatePopup = useCallback(() => {
|
||||
openModal({
|
||||
name: 'confirm',
|
||||
title: t`Update Available`,
|
||||
message: t`A new version of the app is available. Please update to continue using the app.`,
|
||||
title: 'Update Available',
|
||||
message:
|
||||
'A new version of the app is available. Please update to continue using the app.',
|
||||
onPressConfirm: async () => {
|
||||
Updates.reloadAsync().catch(err => {
|
||||
throw err
|
||||
|
||||
@@ -168,15 +168,8 @@ export function getYoutubeVideoId(link: string): string | undefined {
|
||||
return videoId
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the label in the post text matches the host of the link facet.
|
||||
*
|
||||
* Hosts are case-insensitive, so should be lowercase for comparison.
|
||||
* @see https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2
|
||||
*/
|
||||
export function linkRequiresWarning(uri: string, label: string) {
|
||||
const labelDomain = labelToDomain(label)
|
||||
|
||||
let urip
|
||||
try {
|
||||
urip = new URL(uri)
|
||||
@@ -184,9 +177,7 @@ export function linkRequiresWarning(uri: string, label: string) {
|
||||
return true
|
||||
}
|
||||
|
||||
const host = urip.hostname.toLowerCase()
|
||||
|
||||
if (host === 'bsky.app') {
|
||||
if (urip.hostname === 'bsky.app') {
|
||||
// if this is a link to internal content,
|
||||
// warn if it represents itself as a URL to another app
|
||||
if (
|
||||
@@ -203,26 +194,20 @@ export function linkRequiresWarning(uri: string, label: string) {
|
||||
if (!labelDomain) {
|
||||
return true
|
||||
}
|
||||
return labelDomain !== host
|
||||
return labelDomain !== urip.hostname
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a lowercase domain hostname if the label is a valid URL.
|
||||
*
|
||||
* Hosts are case-insensitive, so should be lowercase for comparison.
|
||||
* @see https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2
|
||||
*/
|
||||
export function labelToDomain(label: string): string | undefined {
|
||||
function labelToDomain(label: string): string | undefined {
|
||||
// any spaces just immediately consider the label a non-url
|
||||
if (/\s/.test(label)) {
|
||||
return undefined
|
||||
}
|
||||
try {
|
||||
return new URL(label).hostname.toLowerCase()
|
||||
return new URL(label).hostname
|
||||
} catch {}
|
||||
try {
|
||||
return new URL('https://' + label).hostname.toLowerCase()
|
||||
return new URL('https://' + label).hostname
|
||||
} catch {}
|
||||
return undefined
|
||||
}
|
||||
|
||||
+3
-21
@@ -1,8 +1,4 @@
|
||||
import {useLanguagePrefs} from '#/state/preferences'
|
||||
import {i18n} from '@lingui/core'
|
||||
import {useEffect} from 'react'
|
||||
import {messages as messagesEn} from './locales/en/messages'
|
||||
import {messages as messagesHi} from './locales/hi/messages'
|
||||
|
||||
export const locales = {
|
||||
en: 'English',
|
||||
@@ -18,21 +14,7 @@ export const defaultLocale = 'en'
|
||||
* @param locale any locale string
|
||||
*/
|
||||
export async function dynamicActivate(locale: string) {
|
||||
if (locale === 'en') {
|
||||
i18n.loadAndActivate({locale, messages: messagesEn})
|
||||
return
|
||||
} else if (locale === 'hi') {
|
||||
i18n.loadAndActivate({locale, messages: messagesHi})
|
||||
return
|
||||
} else {
|
||||
i18n.loadAndActivate({locale, messages: messagesEn})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
export async function useLocaleLanguage() {
|
||||
const {appLanguage} = useLanguagePrefs()
|
||||
useEffect(() => {
|
||||
dynamicActivate(appLanguage)
|
||||
}, [appLanguage])
|
||||
const {messages} = await import(`./locales/${locale}/messages`)
|
||||
i18n.load(locale, messages)
|
||||
i18n.activate(locale)
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import {useLanguagePrefs} from '#/state/preferences'
|
||||
import {i18n} from '@lingui/core'
|
||||
import {useEffect} from 'react'
|
||||
|
||||
export const locales = {
|
||||
en: 'English',
|
||||
cs: 'Česky',
|
||||
fr: 'Français',
|
||||
hi: 'हिंदी',
|
||||
es: 'Español',
|
||||
}
|
||||
export const defaultLocale = 'en'
|
||||
|
||||
/**
|
||||
* We do a dynamic import of just the catalog that we need
|
||||
* @param locale any locale string
|
||||
*/
|
||||
export async function dynamicActivate(locale: string) {
|
||||
const {messages} = await import(`./locales/${locale}/messages`)
|
||||
i18n.load(locale, messages)
|
||||
i18n.activate(locale)
|
||||
}
|
||||
|
||||
export async function useLocaleLanguage() {
|
||||
const {appLanguage} = useLanguagePrefs()
|
||||
useEffect(() => {
|
||||
dynamicActivate(appLanguage)
|
||||
}, [appLanguage])
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import React from 'react'
|
||||
import {I18nProvider as DefaultI18nProvider} from '@lingui/react'
|
||||
import {i18n} from '@lingui/core'
|
||||
import {useLocaleLanguage} from './i18n'
|
||||
|
||||
export default function I18nProvider({children}: {children: React.ReactNode}) {
|
||||
useLocaleLanguage()
|
||||
return <DefaultI18nProvider i18n={i18n}>{children}</DefaultI18nProvider>
|
||||
}
|
||||
@@ -4,16 +4,6 @@ interface Language {
|
||||
name: string
|
||||
}
|
||||
|
||||
interface AppLanguage {
|
||||
code2: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export const APP_LANGUAGES: AppLanguage[] = [
|
||||
{code2: 'en', name: 'English'},
|
||||
{code2: 'hi', name: 'हिंदी'},
|
||||
]
|
||||
|
||||
export const LANGUAGES: Language[] = [
|
||||
{code3: 'aar', code2: 'aa', name: 'Afar'},
|
||||
{code3: 'abk', code2: 'ab', name: 'Abkhazian'},
|
||||
|
||||
File diff suppressed because one or more lines are too long
+278
-837
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+278
-837
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+278
-837
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+278
-837
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+626
-1177
File diff suppressed because it is too large
Load Diff
Vendored
+29
-39
@@ -1,8 +1,7 @@
|
||||
import {useEffect, useState, useMemo, useCallback} from 'react'
|
||||
import {useEffect, useState, useCallback, useRef} from 'react'
|
||||
import EventEmitter from 'eventemitter3'
|
||||
import {AppBskyFeedDefs} from '@atproto/api'
|
||||
import {batchedUpdates} from '#/lib/batchedUpdates'
|
||||
import {Shadow, castAsShadow} from './types'
|
||||
import {Shadow} from './types'
|
||||
export type {Shadow} from './types'
|
||||
|
||||
const emitter = new EventEmitter()
|
||||
@@ -22,36 +21,15 @@ interface CacheEntry {
|
||||
value: PostShadow
|
||||
}
|
||||
|
||||
const firstSeenMap = new WeakMap<AppBskyFeedDefs.PostView, number>()
|
||||
function getFirstSeenTS(post: AppBskyFeedDefs.PostView): number {
|
||||
let timeStamp = firstSeenMap.get(post)
|
||||
if (timeStamp !== undefined) {
|
||||
return timeStamp
|
||||
}
|
||||
timeStamp = Date.now()
|
||||
firstSeenMap.set(post, timeStamp)
|
||||
return timeStamp
|
||||
}
|
||||
|
||||
export function usePostShadow(
|
||||
post: AppBskyFeedDefs.PostView,
|
||||
ifAfterTS: number,
|
||||
): Shadow<AppBskyFeedDefs.PostView> | typeof POST_TOMBSTONE {
|
||||
const postSeenTS = getFirstSeenTS(post)
|
||||
const [state, setState] = useState<CacheEntry>(() => ({
|
||||
ts: postSeenTS,
|
||||
const [state, setState] = useState<CacheEntry>({
|
||||
ts: Date.now(),
|
||||
value: fromPost(post),
|
||||
}))
|
||||
|
||||
const [prevPost, setPrevPost] = useState(post)
|
||||
if (post !== prevPost) {
|
||||
// if we got a new prop, assume it's fresher
|
||||
// than whatever shadow state we accumulated
|
||||
setPrevPost(post)
|
||||
setState({
|
||||
ts: postSeenTS,
|
||||
value: fromPost(post),
|
||||
})
|
||||
}
|
||||
})
|
||||
const firstRun = useRef(true)
|
||||
|
||||
const onUpdate = useCallback(
|
||||
(value: Partial<PostShadow>) => {
|
||||
@@ -68,17 +46,28 @@ export function usePostShadow(
|
||||
}
|
||||
}, [post.uri, onUpdate])
|
||||
|
||||
return useMemo(() => {
|
||||
return state.ts > postSeenTS
|
||||
? mergeShadow(post, state.value)
|
||||
: castAsShadow(post)
|
||||
}, [post, state, postSeenTS])
|
||||
// react to post updates
|
||||
useEffect(() => {
|
||||
// dont fire on first run to avoid needless re-renders
|
||||
if (!firstRun.current) {
|
||||
setState({ts: Date.now(), value: fromPost(post)})
|
||||
}
|
||||
firstRun.current = false
|
||||
}, [post])
|
||||
|
||||
return state.ts > ifAfterTS
|
||||
? mergeShadow(post, state.value)
|
||||
: {...post, isShadowed: true}
|
||||
}
|
||||
|
||||
export function updatePostShadow(uri: string, value: Partial<PostShadow>) {
|
||||
batchedUpdates(() => {
|
||||
emitter.emit(uri, value)
|
||||
})
|
||||
emitter.emit(uri, value)
|
||||
}
|
||||
|
||||
export function isPostShadowed(
|
||||
v: AppBskyFeedDefs.PostView | Shadow<AppBskyFeedDefs.PostView>,
|
||||
): v is Shadow<AppBskyFeedDefs.PostView> {
|
||||
return 'isShadowed' in v && !!v.isShadowed
|
||||
}
|
||||
|
||||
function fromPost(post: AppBskyFeedDefs.PostView): PostShadow {
|
||||
@@ -98,7 +87,7 @@ function mergeShadow(
|
||||
if (shadow.isDeleted) {
|
||||
return POST_TOMBSTONE
|
||||
}
|
||||
return castAsShadow({
|
||||
return {
|
||||
...post,
|
||||
likeCount: shadow.likeCount,
|
||||
repostCount: shadow.repostCount,
|
||||
@@ -107,5 +96,6 @@ function mergeShadow(
|
||||
like: shadow.likeUri,
|
||||
repost: shadow.repostUri,
|
||||
},
|
||||
})
|
||||
isShadowed: true,
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+32
-40
@@ -1,8 +1,7 @@
|
||||
import {useEffect, useState, useMemo, useCallback} from 'react'
|
||||
import {useEffect, useState, useCallback, useRef} from 'react'
|
||||
import EventEmitter from 'eventemitter3'
|
||||
import {AppBskyActorDefs} from '@atproto/api'
|
||||
import {batchedUpdates} from '#/lib/batchedUpdates'
|
||||
import {Shadow, castAsShadow} from './types'
|
||||
import {Shadow} from './types'
|
||||
export type {Shadow} from './types'
|
||||
|
||||
const emitter = new EventEmitter()
|
||||
@@ -23,34 +22,15 @@ type ProfileView =
|
||||
| AppBskyActorDefs.ProfileViewBasic
|
||||
| AppBskyActorDefs.ProfileViewDetailed
|
||||
|
||||
const firstSeenMap = new WeakMap<ProfileView, number>()
|
||||
function getFirstSeenTS(profile: ProfileView): number {
|
||||
let timeStamp = firstSeenMap.get(profile)
|
||||
if (timeStamp !== undefined) {
|
||||
return timeStamp
|
||||
}
|
||||
timeStamp = Date.now()
|
||||
firstSeenMap.set(profile, timeStamp)
|
||||
return timeStamp
|
||||
}
|
||||
|
||||
export function useProfileShadow(profile: ProfileView): Shadow<ProfileView> {
|
||||
const profileSeenTS = getFirstSeenTS(profile)
|
||||
const [state, setState] = useState<CacheEntry>(() => ({
|
||||
ts: profileSeenTS,
|
||||
export function useProfileShadow(
|
||||
profile: ProfileView,
|
||||
ifAfterTS: number,
|
||||
): Shadow<ProfileView> {
|
||||
const [state, setState] = useState<CacheEntry>({
|
||||
ts: Date.now(),
|
||||
value: fromProfile(profile),
|
||||
}))
|
||||
|
||||
const [prevProfile, setPrevProfile] = useState(profile)
|
||||
if (profile !== prevProfile) {
|
||||
// if we got a new prop, assume it's fresher
|
||||
// than whatever shadow state we accumulated
|
||||
setPrevProfile(profile)
|
||||
setState({
|
||||
ts: profileSeenTS,
|
||||
value: fromProfile(profile),
|
||||
})
|
||||
}
|
||||
})
|
||||
const firstRun = useRef(true)
|
||||
|
||||
const onUpdate = useCallback(
|
||||
(value: Partial<ProfileShadow>) => {
|
||||
@@ -67,20 +47,31 @@ export function useProfileShadow(profile: ProfileView): Shadow<ProfileView> {
|
||||
}
|
||||
}, [profile.did, onUpdate])
|
||||
|
||||
return useMemo(() => {
|
||||
return state.ts > profileSeenTS
|
||||
? mergeShadow(profile, state.value)
|
||||
: castAsShadow(profile)
|
||||
}, [profile, state, profileSeenTS])
|
||||
// react to profile updates
|
||||
useEffect(() => {
|
||||
// dont fire on first run to avoid needless re-renders
|
||||
if (!firstRun.current) {
|
||||
setState({ts: Date.now(), value: fromProfile(profile)})
|
||||
}
|
||||
firstRun.current = false
|
||||
}, [profile])
|
||||
|
||||
return state.ts > ifAfterTS
|
||||
? mergeShadow(profile, state.value)
|
||||
: {...profile, isShadowed: true}
|
||||
}
|
||||
|
||||
export function updateProfileShadow(
|
||||
uri: string,
|
||||
value: Partial<ProfileShadow>,
|
||||
) {
|
||||
batchedUpdates(() => {
|
||||
emitter.emit(uri, value)
|
||||
})
|
||||
emitter.emit(uri, value)
|
||||
}
|
||||
|
||||
export function isProfileShadowed<T extends ProfileView>(
|
||||
v: T | Shadow<T>,
|
||||
): v is Shadow<T> {
|
||||
return 'isShadowed' in v && !!v.isShadowed
|
||||
}
|
||||
|
||||
function fromProfile(profile: ProfileView): ProfileShadow {
|
||||
@@ -95,7 +86,7 @@ function mergeShadow(
|
||||
profile: ProfileView,
|
||||
shadow: ProfileShadow,
|
||||
): Shadow<ProfileView> {
|
||||
return castAsShadow({
|
||||
return {
|
||||
...profile,
|
||||
viewer: {
|
||||
...(profile.viewer || {}),
|
||||
@@ -103,5 +94,6 @@ function mergeShadow(
|
||||
muted: shadow.muted,
|
||||
blocking: shadow.blockingUri,
|
||||
},
|
||||
})
|
||||
isShadowed: true,
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+1
-7
@@ -1,7 +1 @@
|
||||
// This isn't a real property, but it prevents T being compatible with Shadow<T>.
|
||||
declare const shadowTag: unique symbol
|
||||
export type Shadow<T> = T & {[shadowTag]: true}
|
||||
|
||||
export function castAsShadow<T>(value: T): Shadow<T> {
|
||||
return value as any as Shadow<T>
|
||||
}
|
||||
export type Shadow<T> = T & {isShadowed: true}
|
||||
|
||||
@@ -94,8 +94,6 @@ export function transform(legacy: Partial<LegacySchema>): Schema {
|
||||
postLanguageHistory:
|
||||
legacy.preferences?.postLanguageHistory ||
|
||||
defaults.languagePrefs.postLanguageHistory,
|
||||
appLanguage:
|
||||
legacy.preferences?.postLanguage || defaults.languagePrefs.appLanguage,
|
||||
},
|
||||
requireAltTextEnabled:
|
||||
legacy.preferences?.requireAltTextEnabled ||
|
||||
@@ -130,8 +128,6 @@ export async function migrate() {
|
||||
const newData = transform(legacyData)
|
||||
await write(newData)
|
||||
logger.debug('persisted state: migrated legacy storage')
|
||||
} else {
|
||||
logger.debug('persisted state: no migration needed')
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('persisted state: error migrating legacy storage', {
|
||||
@@ -139,13 +135,3 @@ export async function migrate() {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearLegacyStorage() {
|
||||
try {
|
||||
await AsyncStorage.removeItem(DEPRECATED_ROOT_STATE_STORAGE_KEY)
|
||||
} catch (e: any) {
|
||||
logger.error(`persisted legacy store: failed to clear`, {
|
||||
error: e.toString(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ export const schema = z.object({
|
||||
contentLanguages: z.array(z.string()), // should move to server
|
||||
postLanguage: z.string(), // should move to server
|
||||
postLanguageHistory: z.array(z.string()),
|
||||
appLanguage: z.string(),
|
||||
}),
|
||||
requireAltTextEnabled: z.boolean(), // should move to server
|
||||
mutedThreads: z.array(z.string()), // should move to server
|
||||
@@ -59,7 +58,6 @@ export const defaults: Schema = {
|
||||
postLanguageHistory: (deviceLocales || [])
|
||||
.concat(['en', 'ja', 'pt', 'de'])
|
||||
.slice(0, 6),
|
||||
appLanguage: deviceLocales[0] || 'en',
|
||||
},
|
||||
requireAltTextEnabled: false,
|
||||
mutedThreads: [],
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
|
||||
import {Schema, schema} from '#/state/persisted/schema'
|
||||
import {logger} from '#/logger'
|
||||
|
||||
const BSKY_STORAGE = 'BSKY_STORAGE'
|
||||
|
||||
@@ -17,11 +16,3 @@ export async function read(): Promise<Schema | undefined> {
|
||||
return objData
|
||||
}
|
||||
}
|
||||
|
||||
export async function clear() {
|
||||
try {
|
||||
await AsyncStorage.removeItem(BSKY_STORAGE)
|
||||
} catch (e: any) {
|
||||
logger.error(`persisted store: failed to clear`, {error: e.toString()})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ type ApiContext = {
|
||||
toggleContentLanguage: (code2: string) => void
|
||||
togglePostLanguage: (code2: string) => void
|
||||
savePostLanguageToHistory: () => void
|
||||
setAppLanguage: (code2: string) => void
|
||||
}
|
||||
|
||||
const stateContext = React.createContext<StateContext>(
|
||||
@@ -23,7 +22,6 @@ const apiContext = React.createContext<ApiContext>({
|
||||
toggleContentLanguage: (_: string) => {},
|
||||
togglePostLanguage: (_: string) => {},
|
||||
savePostLanguageToHistory: () => {},
|
||||
setAppLanguage: (_: string) => {},
|
||||
})
|
||||
|
||||
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
@@ -106,9 +104,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
.slice(0, 6),
|
||||
}))
|
||||
},
|
||||
setAppLanguage(code2: string) {
|
||||
setStateWrapped(s => ({...s, appLanguage: code2}))
|
||||
},
|
||||
}),
|
||||
[state, setStateWrapped],
|
||||
)
|
||||
|
||||
@@ -160,35 +160,6 @@ export function useFeedSourceInfoQuery({uri}: {uri: string}) {
|
||||
})
|
||||
}
|
||||
|
||||
export const isFeedPublicQueryKey = ({uri}: {uri: string}) => [
|
||||
'isFeedPublic',
|
||||
uri,
|
||||
]
|
||||
|
||||
export function useIsFeedPublicQuery({uri}: {uri: string}) {
|
||||
return useQuery({
|
||||
queryKey: isFeedPublicQueryKey({uri}),
|
||||
queryFn: async ({queryKey}) => {
|
||||
const [, uri] = queryKey
|
||||
try {
|
||||
const res = await getAgent().app.bsky.feed.getFeed({
|
||||
feed: uri,
|
||||
limit: 1,
|
||||
})
|
||||
return Boolean(res.data.feed)
|
||||
} catch (e: any) {
|
||||
const msg = e.toString() as string
|
||||
|
||||
if (msg.includes('missing jwt')) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useGetPopularFeedsQueryKey = ['getPopularFeeds']
|
||||
|
||||
export function useGetPopularFeedsQuery() {
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
UsePreferencesQueryResponse,
|
||||
ThreadViewPreferences,
|
||||
} from '#/state/queries/preferences/types'
|
||||
import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/moderation'
|
||||
|
||||
export const DEFAULT_HOME_FEED_PREFS: UsePreferencesQueryResponse['feedViewPrefs'] =
|
||||
{
|
||||
@@ -26,26 +25,3 @@ export const DEFAULT_PROD_FEEDS = {
|
||||
pinned: [DEFAULT_PROD_FEED_PREFIX('whats-hot')],
|
||||
saved: [DEFAULT_PROD_FEED_PREFIX('whats-hot')],
|
||||
}
|
||||
|
||||
export const DEFAULT_LOGGED_OUT_PREFERENCES: UsePreferencesQueryResponse = {
|
||||
birthDate: new Date('2022-11-17'), // TODO(pwi)
|
||||
adultContentEnabled: false,
|
||||
feeds: {
|
||||
saved: [],
|
||||
pinned: [],
|
||||
unpinned: [],
|
||||
},
|
||||
// labels are undefined until set by user
|
||||
contentLabels: {
|
||||
nsfw: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES.nsfw,
|
||||
nudity: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES.nudity,
|
||||
suggestive: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES.suggestive,
|
||||
gore: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES.gore,
|
||||
hate: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES.hate,
|
||||
spam: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES.spam,
|
||||
impersonation: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES.impersonation,
|
||||
},
|
||||
feedViewPrefs: DEFAULT_HOME_FEED_PREFS,
|
||||
threadViewPrefs: DEFAULT_THREAD_VIEW_PREFS,
|
||||
userAge: 13, // TODO(pwi)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import {useMemo} from 'react'
|
||||
import {useEffect, useState} from 'react'
|
||||
import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query'
|
||||
import {LabelPreference, BskyFeedViewPreference} from '@atproto/api'
|
||||
import {
|
||||
LabelPreference,
|
||||
BskyFeedViewPreference,
|
||||
ModerationOpts,
|
||||
} from '@atproto/api'
|
||||
import isEqual from 'lodash.isequal'
|
||||
|
||||
import {track} from '#/lib/analytics/analytics'
|
||||
import {getAge} from '#/lib/strings/time'
|
||||
@@ -15,7 +20,6 @@ import {temp__migrateLabelPref} from '#/state/queries/preferences/util'
|
||||
import {
|
||||
DEFAULT_HOME_FEED_PREFS,
|
||||
DEFAULT_THREAD_VIEW_PREFS,
|
||||
DEFAULT_LOGGED_OUT_PREFERENCES,
|
||||
} from '#/state/queries/preferences/const'
|
||||
import {getModerationOpts} from '#/state/queries/preferences/moderation'
|
||||
import {STALE} from '#/state/queries'
|
||||
@@ -24,83 +28,84 @@ export * from '#/state/queries/preferences/types'
|
||||
export * from '#/state/queries/preferences/moderation'
|
||||
export * from '#/state/queries/preferences/const'
|
||||
|
||||
export const preferencesQueryKey = ['getPreferences']
|
||||
export const usePreferencesQueryKey = ['getPreferences']
|
||||
|
||||
export function usePreferencesQuery() {
|
||||
const {hasSession} = useSession()
|
||||
return useQuery({
|
||||
enabled: hasSession,
|
||||
staleTime: STALE.MINUTES.ONE,
|
||||
queryKey: preferencesQueryKey,
|
||||
queryKey: usePreferencesQueryKey,
|
||||
queryFn: async () => {
|
||||
const agent = getAgent()
|
||||
|
||||
if (agent.session?.did === undefined) {
|
||||
return DEFAULT_LOGGED_OUT_PREFERENCES
|
||||
} else {
|
||||
const res = await agent.getPreferences()
|
||||
const preferences: UsePreferencesQueryResponse = {
|
||||
...res,
|
||||
feeds: {
|
||||
saved: res.feeds?.saved || [],
|
||||
pinned: res.feeds?.pinned || [],
|
||||
unpinned:
|
||||
res.feeds.saved?.filter(f => {
|
||||
return !res.feeds.pinned?.includes(f)
|
||||
}) || [],
|
||||
},
|
||||
// labels are undefined until set by user
|
||||
contentLabels: {
|
||||
nsfw: temp__migrateLabelPref(
|
||||
res.contentLabels?.nsfw || DEFAULT_LABEL_PREFERENCES.nsfw,
|
||||
),
|
||||
nudity: temp__migrateLabelPref(
|
||||
res.contentLabels?.nudity || DEFAULT_LABEL_PREFERENCES.nudity,
|
||||
),
|
||||
suggestive: temp__migrateLabelPref(
|
||||
res.contentLabels?.suggestive ||
|
||||
DEFAULT_LABEL_PREFERENCES.suggestive,
|
||||
),
|
||||
gore: temp__migrateLabelPref(
|
||||
res.contentLabels?.gore || DEFAULT_LABEL_PREFERENCES.gore,
|
||||
),
|
||||
hate: temp__migrateLabelPref(
|
||||
res.contentLabels?.hate || DEFAULT_LABEL_PREFERENCES.hate,
|
||||
),
|
||||
spam: temp__migrateLabelPref(
|
||||
res.contentLabels?.spam || DEFAULT_LABEL_PREFERENCES.spam,
|
||||
),
|
||||
impersonation: temp__migrateLabelPref(
|
||||
res.contentLabels?.impersonation ||
|
||||
DEFAULT_LABEL_PREFERENCES.impersonation,
|
||||
),
|
||||
},
|
||||
feedViewPrefs: {
|
||||
...DEFAULT_HOME_FEED_PREFS,
|
||||
...(res.feedViewPrefs.home || {}),
|
||||
},
|
||||
threadViewPrefs: {
|
||||
...DEFAULT_THREAD_VIEW_PREFS,
|
||||
...(res.threadViewPrefs ?? {}),
|
||||
},
|
||||
userAge: res.birthDate ? getAge(res.birthDate) : undefined,
|
||||
}
|
||||
return preferences
|
||||
const res = await getAgent().getPreferences()
|
||||
const preferences: UsePreferencesQueryResponse = {
|
||||
...res,
|
||||
feeds: {
|
||||
saved: res.feeds?.saved || [],
|
||||
pinned: res.feeds?.pinned || [],
|
||||
unpinned:
|
||||
res.feeds.saved?.filter(f => {
|
||||
return !res.feeds.pinned?.includes(f)
|
||||
}) || [],
|
||||
},
|
||||
// labels are undefined until set by user
|
||||
contentLabels: {
|
||||
nsfw: temp__migrateLabelPref(
|
||||
res.contentLabels?.nsfw || DEFAULT_LABEL_PREFERENCES.nsfw,
|
||||
),
|
||||
nudity: temp__migrateLabelPref(
|
||||
res.contentLabels?.nudity || DEFAULT_LABEL_PREFERENCES.nudity,
|
||||
),
|
||||
suggestive: temp__migrateLabelPref(
|
||||
res.contentLabels?.suggestive ||
|
||||
DEFAULT_LABEL_PREFERENCES.suggestive,
|
||||
),
|
||||
gore: temp__migrateLabelPref(
|
||||
res.contentLabels?.gore || DEFAULT_LABEL_PREFERENCES.gore,
|
||||
),
|
||||
hate: temp__migrateLabelPref(
|
||||
res.contentLabels?.hate || DEFAULT_LABEL_PREFERENCES.hate,
|
||||
),
|
||||
spam: temp__migrateLabelPref(
|
||||
res.contentLabels?.spam || DEFAULT_LABEL_PREFERENCES.spam,
|
||||
),
|
||||
impersonation: temp__migrateLabelPref(
|
||||
res.contentLabels?.impersonation ||
|
||||
DEFAULT_LABEL_PREFERENCES.impersonation,
|
||||
),
|
||||
},
|
||||
feedViewPrefs: {
|
||||
...DEFAULT_HOME_FEED_PREFS,
|
||||
...(res.feedViewPrefs.home || {}),
|
||||
},
|
||||
threadViewPrefs: {
|
||||
...DEFAULT_THREAD_VIEW_PREFS,
|
||||
...(res.threadViewPrefs ?? {}),
|
||||
},
|
||||
userAge: res.birthDate ? getAge(res.birthDate) : undefined,
|
||||
}
|
||||
return preferences
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useModerationOpts() {
|
||||
const {currentAccount} = useSession()
|
||||
const [opts, setOpts] = useState<ModerationOpts | undefined>()
|
||||
const prefs = usePreferencesQuery()
|
||||
const opts = useMemo(() => {
|
||||
useEffect(() => {
|
||||
if (!prefs.data) {
|
||||
return
|
||||
}
|
||||
return getModerationOpts({
|
||||
// only update this hook when the moderation options change
|
||||
const newOpts = getModerationOpts({
|
||||
userDid: currentAccount?.did || '',
|
||||
preferences: prefs.data,
|
||||
})
|
||||
}, [currentAccount?.did, prefs.data])
|
||||
if (!isEqual(opts, newOpts)) {
|
||||
setOpts(newOpts)
|
||||
}
|
||||
}, [prefs.data, currentAccount, opts, setOpts])
|
||||
return opts
|
||||
}
|
||||
|
||||
@@ -112,7 +117,7 @@ export function useClearPreferencesMutation() {
|
||||
await getAgent().app.bsky.actor.putPreferences({preferences: []})
|
||||
// triggers a refetch
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: preferencesQueryKey,
|
||||
queryKey: usePreferencesQueryKey,
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -130,7 +135,7 @@ export function usePreferencesSetContentLabelMutation() {
|
||||
await getAgent().setContentLabelPref(labelGroup, visibility)
|
||||
// triggers a refetch
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: preferencesQueryKey,
|
||||
queryKey: usePreferencesQueryKey,
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -144,7 +149,7 @@ export function usePreferencesSetAdultContentMutation() {
|
||||
await getAgent().setAdultContentEnabled(enabled)
|
||||
// triggers a refetch
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: preferencesQueryKey,
|
||||
queryKey: usePreferencesQueryKey,
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -158,7 +163,7 @@ export function usePreferencesSetBirthDateMutation() {
|
||||
await getAgent().setPersonalDetails({birthDate})
|
||||
// triggers a refetch
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: preferencesQueryKey,
|
||||
queryKey: usePreferencesQueryKey,
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -172,7 +177,7 @@ export function useSetFeedViewPreferencesMutation() {
|
||||
await getAgent().setFeedViewPrefs('home', prefs)
|
||||
// triggers a refetch
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: preferencesQueryKey,
|
||||
queryKey: usePreferencesQueryKey,
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -186,7 +191,7 @@ export function useSetThreadViewPreferencesMutation() {
|
||||
await getAgent().setThreadViewPrefs(prefs)
|
||||
// triggers a refetch
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: preferencesQueryKey,
|
||||
queryKey: usePreferencesQueryKey,
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -204,7 +209,7 @@ export function useSetSaveFeedsMutation() {
|
||||
await getAgent().setSavedFeeds(saved, pinned)
|
||||
// triggers a refetch
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: preferencesQueryKey,
|
||||
queryKey: usePreferencesQueryKey,
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -219,7 +224,7 @@ export function useSaveFeedMutation() {
|
||||
track('CustomFeed:Save')
|
||||
// triggers a refetch
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: preferencesQueryKey,
|
||||
queryKey: usePreferencesQueryKey,
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -234,7 +239,7 @@ export function useRemoveFeedMutation() {
|
||||
track('CustomFeed:Unsave')
|
||||
// triggers a refetch
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: preferencesQueryKey,
|
||||
queryKey: usePreferencesQueryKey,
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -249,7 +254,7 @@ export function usePinFeedMutation() {
|
||||
track('CustomFeed:Pin', {uri})
|
||||
// triggers a refetch
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: preferencesQueryKey,
|
||||
queryKey: usePreferencesQueryKey,
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -264,7 +269,7 @@ export function useUnpinFeedMutation() {
|
||||
track('CustomFeed:Unpin', {uri})
|
||||
// triggers a refetch
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: preferencesQueryKey,
|
||||
queryKey: usePreferencesQueryKey,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -34,24 +34,6 @@ export const DEFAULT_LABEL_PREFERENCES: Record<
|
||||
impersonation: 'hide',
|
||||
}
|
||||
|
||||
/**
|
||||
* More strict than our default settings for logged in users.
|
||||
*
|
||||
* TODO(pwi)
|
||||
*/
|
||||
export const DEFAULT_LOGGED_OUT_LABEL_PREFERENCES: Record<
|
||||
ConfigurableLabelGroup,
|
||||
LabelPreference
|
||||
> = {
|
||||
nsfw: 'hide',
|
||||
nudity: 'hide',
|
||||
suggestive: 'hide',
|
||||
gore: 'hide',
|
||||
hate: 'hide',
|
||||
spam: 'hide',
|
||||
impersonation: 'hide',
|
||||
}
|
||||
|
||||
export const ILLEGAL_LABEL_GROUP: LabelGroupConfig = {
|
||||
id: 'illegal',
|
||||
title: 'Illegal Content',
|
||||
|
||||
@@ -43,10 +43,7 @@ export type UsePreferencesQueryResponse = Omit<
|
||||
}
|
||||
}
|
||||
|
||||
export type ThreadViewPreferences = Pick<
|
||||
BskyThreadViewPreference,
|
||||
'prioritizeFollowedUsers'
|
||||
> & {
|
||||
export type ThreadViewPreferences = Omit<BskyThreadViewPreference, 'sort'> & {
|
||||
sort: 'oldest' | 'newest' | 'most-likes' | 'random' | string
|
||||
lab_treeViewEnabled?: boolean
|
||||
}
|
||||
|
||||
+17
-49
@@ -8,8 +8,6 @@ import * as persisted from '#/state/persisted'
|
||||
import {PUBLIC_BSKY_AGENT} from '#/state/queries'
|
||||
import {IS_PROD} from '#/lib/constants'
|
||||
import {emitSessionLoaded, emitSessionDropped} from '../events'
|
||||
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
||||
import {useCloseAllActiveElements} from '#/state/util'
|
||||
|
||||
let __globalAgent: BskyAgent = PUBLIC_BSKY_AGENT
|
||||
|
||||
@@ -42,20 +40,7 @@ export type ApiContext = {
|
||||
identifier: string
|
||||
password: string
|
||||
}) => Promise<void>
|
||||
/**
|
||||
* A full logout. Clears the `currentAccount` from session, AND removes
|
||||
* access tokens from all accounts, so that returning as any user will
|
||||
* require a full login.
|
||||
*/
|
||||
logout: () => Promise<void>
|
||||
/**
|
||||
* A partial logout. Clears the `currentAccount` from session, but DOES NOT
|
||||
* clear access tokens from accounts, allowing the user to return to their
|
||||
* other accounts without logging in.
|
||||
*
|
||||
* Used when adding a new account, deleting an account.
|
||||
*/
|
||||
clearCurrentAccount: () => void
|
||||
initSession: (account: SessionAccount) => Promise<void>
|
||||
resumeSession: (account?: SessionAccount) => Promise<void>
|
||||
removeAccount: (account: SessionAccount) => void
|
||||
@@ -65,6 +50,7 @@ export type ApiContext = {
|
||||
Pick<SessionAccount, 'handle' | 'email' | 'emailConfirmed'>
|
||||
>,
|
||||
) => void
|
||||
clearCurrentAccount: () => void
|
||||
}
|
||||
|
||||
const StateContext = React.createContext<StateContext>({
|
||||
@@ -268,26 +254,13 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
[upsertAccount, queryClient],
|
||||
)
|
||||
|
||||
const clearCurrentAccount = React.useCallback(() => {
|
||||
logger.debug(
|
||||
`session: clear current account`,
|
||||
{},
|
||||
logger.DebugContext.session,
|
||||
)
|
||||
__globalAgent = PUBLIC_BSKY_AGENT
|
||||
queryClient.clear()
|
||||
setStateAndPersist(s => ({
|
||||
...s,
|
||||
currentAccount: undefined,
|
||||
}))
|
||||
}, [setStateAndPersist, queryClient])
|
||||
|
||||
const logout = React.useCallback<ApiContext['logout']>(async () => {
|
||||
clearCurrentAccount()
|
||||
logger.debug(`session: logout`, {}, logger.DebugContext.session)
|
||||
setStateAndPersist(s => {
|
||||
return {
|
||||
...s,
|
||||
agent: PUBLIC_BSKY_AGENT,
|
||||
currentAccount: undefined,
|
||||
accounts: s.accounts.map(a => ({
|
||||
...a,
|
||||
refreshJwt: undefined,
|
||||
@@ -295,7 +268,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
})),
|
||||
}
|
||||
})
|
||||
}, [clearCurrentAccount, setStateAndPersist])
|
||||
}, [setStateAndPersist])
|
||||
|
||||
const initSession = React.useCallback<ApiContext['initSession']>(
|
||||
async account => {
|
||||
@@ -429,6 +402,19 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
[setState, initSession],
|
||||
)
|
||||
|
||||
/**
|
||||
* Clears the `currentAccount` from session. Typically used to drop the user
|
||||
* back to the sign-in page.
|
||||
*/
|
||||
const clearCurrentAccount = React.useCallback(() => {
|
||||
__globalAgent = PUBLIC_BSKY_AGENT
|
||||
queryClient.clear()
|
||||
setStateAndPersist(s => ({
|
||||
...s,
|
||||
currentAccount: undefined,
|
||||
}))
|
||||
}, [setStateAndPersist, queryClient])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isDirty.current) {
|
||||
isDirty.current = false
|
||||
@@ -529,21 +515,3 @@ export function useSession() {
|
||||
export function useSessionApi() {
|
||||
return React.useContext(ApiContext)
|
||||
}
|
||||
|
||||
export function useRequireAuth() {
|
||||
const {hasSession} = useSession()
|
||||
const {setShowLoggedOut} = useLoggedOutViewControls()
|
||||
const closeAll = useCloseAllActiveElements()
|
||||
|
||||
return React.useCallback(
|
||||
(fn: () => void) => {
|
||||
if (hasSession) {
|
||||
fn()
|
||||
} else {
|
||||
closeAll()
|
||||
setShowLoggedOut(true)
|
||||
}
|
||||
},
|
||||
[hasSession, setShowLoggedOut, closeAll],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
type StateContext = {
|
||||
showLoggedOut: boolean
|
||||
}
|
||||
|
||||
const StateContext = React.createContext<StateContext>({
|
||||
showLoggedOut: false,
|
||||
})
|
||||
const ControlsContext = React.createContext<{
|
||||
setShowLoggedOut: (show: boolean) => void
|
||||
}>({
|
||||
setShowLoggedOut: () => {},
|
||||
})
|
||||
|
||||
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
const [showLoggedOut, setShowLoggedOut] = React.useState(false)
|
||||
|
||||
const state = React.useMemo(() => ({showLoggedOut}), [showLoggedOut])
|
||||
const controls = React.useMemo(() => ({setShowLoggedOut}), [setShowLoggedOut])
|
||||
|
||||
return (
|
||||
<StateContext.Provider value={state}>
|
||||
<ControlsContext.Provider value={controls}>
|
||||
{children}
|
||||
</ControlsContext.Provider>
|
||||
</StateContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useLoggedOutView() {
|
||||
return React.useContext(StateContext)
|
||||
}
|
||||
|
||||
export function useLoggedOutViewControls() {
|
||||
return React.useContext(ControlsContext)
|
||||
}
|
||||
@@ -1,10 +1,5 @@
|
||||
import React from 'react'
|
||||
import {View, Pressable} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg} from '@lingui/macro'
|
||||
|
||||
import {isIOS} from 'platform/detection'
|
||||
import {SafeAreaView} from 'react-native'
|
||||
import {Login} from 'view/com/auth/login/Login'
|
||||
import {CreateAccount} from 'view/com/auth/create/CreateAccount'
|
||||
import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
|
||||
@@ -13,7 +8,6 @@ import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {SplashScreen} from './SplashScreen'
|
||||
import {useSetMinimalShellMode} from '#/state/shell/minimal-mode'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
|
||||
enum ScreenState {
|
||||
S_LoginOrCreateAccount,
|
||||
@@ -21,66 +15,31 @@ enum ScreenState {
|
||||
S_CreateAccount,
|
||||
}
|
||||
|
||||
export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
|
||||
const {_} = useLingui()
|
||||
export function LoggedOut() {
|
||||
const pal = usePalette('default')
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {screen} = useAnalytics()
|
||||
const [screenState, setScreenState] = React.useState<ScreenState>(
|
||||
ScreenState.S_LoginOrCreateAccount,
|
||||
)
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
|
||||
React.useEffect(() => {
|
||||
screen('Login')
|
||||
setMinimalShellMode(true)
|
||||
}, [screen, setMinimalShellMode])
|
||||
|
||||
return (
|
||||
<View
|
||||
testID="noSessionView"
|
||||
style={[
|
||||
s.hContentRegion,
|
||||
pal.view,
|
||||
{
|
||||
// only needed if dismiss button is present
|
||||
paddingTop: onDismiss && isMobile ? 40 : 0,
|
||||
},
|
||||
]}>
|
||||
<ErrorBoundary>
|
||||
{onDismiss && (
|
||||
<Pressable
|
||||
accessibilityHint={_(msg`Go back`)}
|
||||
accessibilityLabel={_(msg`Go back`)}
|
||||
accessibilityRole="button"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: isIOS ? 0 : 20,
|
||||
right: 20,
|
||||
padding: 10,
|
||||
zIndex: 100,
|
||||
backgroundColor: pal.text.color,
|
||||
borderRadius: 100,
|
||||
}}
|
||||
onPress={onDismiss}>
|
||||
<FontAwesomeIcon
|
||||
icon="x"
|
||||
size={12}
|
||||
style={{
|
||||
color: String(pal.textInverted.color),
|
||||
}}
|
||||
/>
|
||||
</Pressable>
|
||||
)}
|
||||
if (screenState === ScreenState.S_LoginOrCreateAccount) {
|
||||
return (
|
||||
<SplashScreen
|
||||
onPressSignin={() => setScreenState(ScreenState.S_Login)}
|
||||
onPressCreateAccount={() => setScreenState(ScreenState.S_CreateAccount)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
{screenState === ScreenState.S_LoginOrCreateAccount ? (
|
||||
<SplashScreen
|
||||
onPressSignin={() => setScreenState(ScreenState.S_Login)}
|
||||
onPressCreateAccount={() =>
|
||||
setScreenState(ScreenState.S_CreateAccount)
|
||||
}
|
||||
/>
|
||||
) : undefined}
|
||||
return (
|
||||
<SafeAreaView testID="noSessionView" style={[s.hContentRegion, pal.view]}>
|
||||
<ErrorBoundary>
|
||||
{screenState === ScreenState.S_Login ? (
|
||||
<Login
|
||||
onPressBack={() =>
|
||||
@@ -96,6 +55,6 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
|
||||
/>
|
||||
) : undefined}
|
||||
</ErrorBoundary>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import {SafeAreaView, Platform} from 'react-native'
|
||||
import {SafeAreaView} from 'react-native'
|
||||
import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
|
||||
import {s} from 'lib/styles'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
@@ -23,18 +23,7 @@ export function Onboarding() {
|
||||
const skip = () => onboardingDispatch({type: 'skip'})
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
testID="onboardingView"
|
||||
style={[
|
||||
s.hContentRegion,
|
||||
pal.view,
|
||||
// @ts-ignore web only -esb
|
||||
Platform.select({
|
||||
web: {
|
||||
height: '100vh',
|
||||
},
|
||||
}),
|
||||
]}>
|
||||
<SafeAreaView testID="onboardingView" style={[s.hContentRegion, pal.view]}>
|
||||
<ErrorBoundary>
|
||||
{onboardingState.step === 'Welcome' && (
|
||||
<Welcome skip={skip} next={next} />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
import {SafeAreaView, StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
|
||||
import {s, colors} from 'lib/styles'
|
||||
@@ -20,40 +20,42 @@ export const SplashScreen = ({
|
||||
|
||||
return (
|
||||
<CenteredView style={[styles.container, pal.view]}>
|
||||
<ErrorBoundary>
|
||||
<View style={styles.hero}>
|
||||
<Text style={[styles.title, pal.link]}>
|
||||
<Trans>Bluesky</Trans>
|
||||
</Text>
|
||||
<Text style={[styles.subtitle, pal.textLight]}>
|
||||
<Trans>See what's next</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
<View testID="signinOrCreateAccount" style={styles.btns}>
|
||||
<TouchableOpacity
|
||||
testID="createAccountButton"
|
||||
style={[styles.btn, {backgroundColor: colors.blue3}]}
|
||||
onPress={onPressCreateAccount}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Create new account`)}
|
||||
accessibilityHint="Opens flow to create a new Bluesky account">
|
||||
<Text style={[s.white, styles.btnLabel]}>
|
||||
<Trans>Create a new account</Trans>
|
||||
<SafeAreaView testID="noSessionView" style={styles.container}>
|
||||
<ErrorBoundary>
|
||||
<View style={styles.hero}>
|
||||
<Text style={[styles.title, pal.link]}>
|
||||
<Trans>Bluesky</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
testID="signInButton"
|
||||
style={[styles.btn, pal.btn]}
|
||||
onPress={onPressSignin}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Sign in`)}
|
||||
accessibilityHint="Opens flow to sign into your existing Bluesky account">
|
||||
<Text style={[pal.text, styles.btnLabel]}>
|
||||
<Trans>Sign In</Trans>
|
||||
<Text style={[styles.subtitle, pal.textLight]}>
|
||||
<Trans>See what's next</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</ErrorBoundary>
|
||||
</View>
|
||||
<View testID="signinOrCreateAccount" style={styles.btns}>
|
||||
<TouchableOpacity
|
||||
testID="createAccountButton"
|
||||
style={[styles.btn, {backgroundColor: colors.blue3}]}
|
||||
onPress={onPressCreateAccount}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Create new account`)}
|
||||
accessibilityHint="Opens flow to create a new Bluesky account">
|
||||
<Text style={[s.white, styles.btnLabel]}>
|
||||
<Trans>Create a new account</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
testID="signInButton"
|
||||
style={[styles.btn, pal.btn]}
|
||||
onPress={onPressSignin}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Sign in`)}
|
||||
accessibilityHint="Opens flow to sign into your existing Bluesky account">
|
||||
<Text style={[pal.text, styles.btnLabel]}>
|
||||
<Trans>Sign In</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</ErrorBoundary>
|
||||
</SafeAreaView>
|
||||
</CenteredView>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react'
|
||||
import {StyleSheet, TouchableOpacity, View, Pressable} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {TextLink} from '../util/Link'
|
||||
import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
|
||||
@@ -12,11 +11,9 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {Trans} from '@lingui/macro'
|
||||
|
||||
export const SplashScreen = ({
|
||||
onDismiss,
|
||||
onPressSignin,
|
||||
onPressCreateAccount,
|
||||
}: {
|
||||
onDismiss?: () => void
|
||||
onPressSignin: () => void
|
||||
onPressCreateAccount: () => void
|
||||
}) => {
|
||||
@@ -26,70 +23,47 @@ export const SplashScreen = ({
|
||||
const isMobileWeb = isWeb && isTabletOrMobile
|
||||
|
||||
return (
|
||||
<>
|
||||
{onDismiss && (
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 20,
|
||||
right: 20,
|
||||
padding: 20,
|
||||
zIndex: 100,
|
||||
}}
|
||||
onPress={onDismiss}>
|
||||
<FontAwesomeIcon
|
||||
icon="x"
|
||||
size={24}
|
||||
style={{
|
||||
color: String(pal.text.color),
|
||||
}}
|
||||
/>
|
||||
</Pressable>
|
||||
)}
|
||||
|
||||
<CenteredView style={[styles.container, pal.view]}>
|
||||
<View
|
||||
testID="noSessionView"
|
||||
style={[
|
||||
styles.containerInner,
|
||||
isMobileWeb && styles.containerInnerMobile,
|
||||
pal.border,
|
||||
]}>
|
||||
<ErrorBoundary>
|
||||
<Text style={isMobileWeb ? styles.titleMobile : styles.title}>
|
||||
Bluesky
|
||||
</Text>
|
||||
<Text style={isMobileWeb ? styles.subtitleMobile : styles.subtitle}>
|
||||
See what's next
|
||||
</Text>
|
||||
<View testID="signinOrCreateAccount" style={styles.btns}>
|
||||
<TouchableOpacity
|
||||
testID="createAccountButton"
|
||||
style={[styles.btn, {backgroundColor: colors.blue3}]}
|
||||
onPress={onPressCreateAccount}
|
||||
// TODO: web accessibility
|
||||
accessibilityRole="button">
|
||||
<Text style={[s.white, styles.btnLabel]}>
|
||||
Create a new account
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
testID="signInButton"
|
||||
style={[styles.btn, pal.btn]}
|
||||
onPress={onPressSignin}
|
||||
// TODO: web accessibility
|
||||
accessibilityRole="button">
|
||||
<Text style={[pal.text, styles.btnLabel]}>
|
||||
<Trans>Sign In</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</ErrorBoundary>
|
||||
</View>
|
||||
<Footer styles={styles} />
|
||||
</CenteredView>
|
||||
</>
|
||||
<CenteredView style={[styles.container, pal.view]}>
|
||||
<View
|
||||
testID="noSessionView"
|
||||
style={[
|
||||
styles.containerInner,
|
||||
isMobileWeb && styles.containerInnerMobile,
|
||||
pal.border,
|
||||
]}>
|
||||
<ErrorBoundary>
|
||||
<Text style={isMobileWeb ? styles.titleMobile : styles.title}>
|
||||
Bluesky
|
||||
</Text>
|
||||
<Text style={isMobileWeb ? styles.subtitleMobile : styles.subtitle}>
|
||||
See what's next
|
||||
</Text>
|
||||
<View testID="signinOrCreateAccount" style={styles.btns}>
|
||||
<TouchableOpacity
|
||||
testID="createAccountButton"
|
||||
style={[styles.btn, {backgroundColor: colors.blue3}]}
|
||||
onPress={onPressCreateAccount}
|
||||
// TODO: web accessibility
|
||||
accessibilityRole="button">
|
||||
<Text style={[s.white, styles.btnLabel]}>
|
||||
Create a new account
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
testID="signInButton"
|
||||
style={[styles.btn, pal.btn]}
|
||||
onPress={onPressSignin}
|
||||
// TODO: web accessibility
|
||||
accessibilityRole="button">
|
||||
<Text style={[pal.text, styles.btnLabel]}>
|
||||
<Trans>Sign In</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</ErrorBoundary>
|
||||
</View>
|
||||
<Footer styles={styles} />
|
||||
</CenteredView>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -136,12 +136,7 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
|
||||
</LoggedOutLayout>
|
||||
) : undefined}
|
||||
{currentForm === Forms.PasswordUpdated ? (
|
||||
<LoggedOutLayout
|
||||
leadin=""
|
||||
title={_(msg`Password updated`)}
|
||||
description={_(msg`You can now sign in with your new password.`)}>
|
||||
<PasswordUpdatedForm onPressNext={gotoForm(Forms.Login)} />
|
||||
</LoggedOutLayout>
|
||||
<PasswordUpdatedForm onPressNext={gotoForm(Forms.Login)} />
|
||||
) : undefined}
|
||||
</KeyboardAvoidingView>
|
||||
)
|
||||
|
||||
@@ -10,8 +10,6 @@ import {RecommendedFeedsItem} from './RecommendedFeedsItem'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useSuggestedFeedsQuery} from '#/state/queries/suggested-feeds'
|
||||
|
||||
type Props = {
|
||||
@@ -19,45 +17,40 @@ type Props = {
|
||||
}
|
||||
export function RecommendedFeeds({next}: Props) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {isTabletOrMobile} = useWebMediaQueries()
|
||||
const {isLoading, data} = useSuggestedFeedsQuery()
|
||||
|
||||
const hasFeeds = data && data.pages[0].feeds.length
|
||||
const hasFeeds = data && data?.pages?.[0]?.feeds?.length
|
||||
|
||||
const title = (
|
||||
<>
|
||||
<Trans>
|
||||
<Text
|
||||
style={[
|
||||
pal.textLight,
|
||||
tdStyles.title1,
|
||||
isTabletOrMobile && tdStyles.title1Small,
|
||||
]}>
|
||||
Choose your
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
pal.link,
|
||||
tdStyles.title2,
|
||||
isTabletOrMobile && tdStyles.title2Small,
|
||||
]}>
|
||||
Recommended
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
pal.link,
|
||||
tdStyles.title2,
|
||||
isTabletOrMobile && tdStyles.title2Small,
|
||||
]}>
|
||||
Feeds
|
||||
</Text>
|
||||
</Trans>
|
||||
<Text
|
||||
style={[
|
||||
pal.textLight,
|
||||
tdStyles.title1,
|
||||
isTabletOrMobile && tdStyles.title1Small,
|
||||
]}>
|
||||
Choose your
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
pal.link,
|
||||
tdStyles.title2,
|
||||
isTabletOrMobile && tdStyles.title2Small,
|
||||
]}>
|
||||
Recommended
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
pal.link,
|
||||
tdStyles.title2,
|
||||
isTabletOrMobile && tdStyles.title2Small,
|
||||
]}>
|
||||
Feeds
|
||||
</Text>
|
||||
<Text type="2xl-medium" style={[pal.textLight, tdStyles.description]}>
|
||||
<Trans>
|
||||
Feeds are created by users to curate content. Choose some feeds that
|
||||
you find interesting.
|
||||
</Trans>
|
||||
Feeds are created by users to curate content. Choose some feeds that you
|
||||
find interesting.
|
||||
</Text>
|
||||
<View
|
||||
style={{
|
||||
@@ -76,7 +69,7 @@ export function RecommendedFeeds({next}: Props) {
|
||||
<Text
|
||||
type="2xl-medium"
|
||||
style={{color: '#fff', position: 'relative', top: -1}}>
|
||||
<Trans>Next</Trans>
|
||||
Next
|
||||
</Text>
|
||||
<FontAwesomeIcon icon="angle-right" color="#fff" size={14} />
|
||||
</View>
|
||||
@@ -106,22 +99,20 @@ export function RecommendedFeeds({next}: Props) {
|
||||
<ActivityIndicator size="large" />
|
||||
</View>
|
||||
) : (
|
||||
<ErrorMessage message={_(msg`Failed to load recommended feeds`)} />
|
||||
<ErrorMessage message="Failed to load recommended feeds" />
|
||||
)}
|
||||
</TitleColumnLayout>
|
||||
</TabletOrDesktop>
|
||||
<Mobile>
|
||||
<View style={[mStyles.container]} testID="recommendedFeedsOnboarding">
|
||||
<ViewHeader
|
||||
title={_(msg`Recommended Feeds`)}
|
||||
title="Recommended Feeds"
|
||||
showBackButton={false}
|
||||
showOnDesktop
|
||||
/>
|
||||
<Text type="lg-medium" style={[pal.text, mStyles.header]}>
|
||||
<Trans>
|
||||
Check out some recommended feeds. Tap + to add them to your list
|
||||
of pinned feeds.
|
||||
</Trans>
|
||||
Check out some recommended feeds. Tap + to add them to your list of
|
||||
pinned feeds.
|
||||
</Text>
|
||||
|
||||
{hasFeeds ? (
|
||||
@@ -137,15 +128,13 @@ export function RecommendedFeeds({next}: Props) {
|
||||
</View>
|
||||
) : (
|
||||
<View style={{flex: 1}}>
|
||||
<ErrorMessage
|
||||
message={_(msg`Failed to load recommended feeds`)}
|
||||
/>
|
||||
<ErrorMessage message="Failed to load recommended feeds" />
|
||||
</View>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onPress={next}
|
||||
label={_(msg`Continue`)}
|
||||
label="Continue"
|
||||
testID="continueBtn"
|
||||
style={mStyles.button}
|
||||
labelStyle={mStyles.buttonText}
|
||||
|
||||
@@ -14,17 +14,14 @@ import {useSuggestedFollowsQuery} from '#/state/queries/suggested-follows'
|
||||
import {useGetSuggestedFollowersByActor} from '#/state/queries/suggested-follows'
|
||||
import {useModerationOpts} from '#/state/queries/preferences'
|
||||
import {logger} from '#/logger'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
type Props = {
|
||||
next: () => void
|
||||
}
|
||||
export function RecommendedFollows({next}: Props) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {isTabletOrMobile} = useWebMediaQueries()
|
||||
const {data: suggestedFollows} = useSuggestedFollowsQuery()
|
||||
const {data: suggestedFollows, dataUpdatedAt} = useSuggestedFollowsQuery()
|
||||
const getSuggestedFollowsByActor = useGetSuggestedFollowersByActor()
|
||||
const [additionalSuggestions, setAdditionalSuggestions] = React.useState<{
|
||||
[did: string]: AppBskyActorDefs.ProfileView[]
|
||||
@@ -34,37 +31,33 @@ export function RecommendedFollows({next}: Props) {
|
||||
|
||||
const title = (
|
||||
<>
|
||||
<Trans>
|
||||
<Text
|
||||
style={[
|
||||
pal.textLight,
|
||||
tdStyles.title1,
|
||||
isTabletOrMobile && tdStyles.title1Small,
|
||||
]}>
|
||||
Follow some
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
pal.link,
|
||||
tdStyles.title2,
|
||||
isTabletOrMobile && tdStyles.title2Small,
|
||||
]}>
|
||||
Recommended
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
pal.link,
|
||||
tdStyles.title2,
|
||||
isTabletOrMobile && tdStyles.title2Small,
|
||||
]}>
|
||||
Users
|
||||
</Text>
|
||||
</Trans>
|
||||
<Text
|
||||
style={[
|
||||
pal.textLight,
|
||||
tdStyles.title1,
|
||||
isTabletOrMobile && tdStyles.title1Small,
|
||||
]}>
|
||||
Follow some
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
pal.link,
|
||||
tdStyles.title2,
|
||||
isTabletOrMobile && tdStyles.title2Small,
|
||||
]}>
|
||||
Recommended
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
pal.link,
|
||||
tdStyles.title2,
|
||||
isTabletOrMobile && tdStyles.title2Small,
|
||||
]}>
|
||||
Users
|
||||
</Text>
|
||||
<Text type="2xl-medium" style={[pal.textLight, tdStyles.description]}>
|
||||
<Trans>
|
||||
Follow some users to get started. We can recommend you more users
|
||||
based on who you find interesting.
|
||||
</Trans>
|
||||
Follow some users to get started. We can recommend you more users based
|
||||
on who you find interesting.
|
||||
</Text>
|
||||
<View
|
||||
style={{
|
||||
@@ -83,7 +76,7 @@ export function RecommendedFollows({next}: Props) {
|
||||
<Text
|
||||
type="2xl-medium"
|
||||
style={{color: '#fff', position: 'relative', top: -1}}>
|
||||
<Trans>Done</Trans>
|
||||
Done
|
||||
</Text>
|
||||
<FontAwesomeIcon icon="angle-right" color="#fff" size={14} />
|
||||
</View>
|
||||
@@ -162,6 +155,7 @@ export function RecommendedFollows({next}: Props) {
|
||||
renderItem={({item}) => (
|
||||
<RecommendedFollowsItem
|
||||
profile={item}
|
||||
dataUpdatedAt={dataUpdatedAt}
|
||||
onFollowStateChange={onFollowStateChange}
|
||||
moderation={moderateProfile(item, moderationOpts)}
|
||||
/>
|
||||
@@ -177,15 +171,13 @@ export function RecommendedFollows({next}: Props) {
|
||||
<View style={[mStyles.container]} testID="recommendedFollowsOnboarding">
|
||||
<View>
|
||||
<ViewHeader
|
||||
title={_(msg`Recommended Users`)}
|
||||
title="Recommended Follows"
|
||||
showBackButton={false}
|
||||
showOnDesktop
|
||||
/>
|
||||
<Text type="lg-medium" style={[pal.text, mStyles.header]}>
|
||||
<Trans>
|
||||
Check out some recommended users. Follow them to see similar
|
||||
users.
|
||||
</Trans>
|
||||
Check out some recommended users. Follow them to see similar
|
||||
users.
|
||||
</Text>
|
||||
</View>
|
||||
{!suggestedFollows || !moderationOpts ? (
|
||||
@@ -196,6 +188,7 @@ export function RecommendedFollows({next}: Props) {
|
||||
renderItem={({item}) => (
|
||||
<RecommendedFollowsItem
|
||||
profile={item}
|
||||
dataUpdatedAt={dataUpdatedAt}
|
||||
onFollowStateChange={onFollowStateChange}
|
||||
moderation={moderateProfile(item, moderationOpts)}
|
||||
/>
|
||||
@@ -206,7 +199,7 @@ export function RecommendedFollows({next}: Props) {
|
||||
)}
|
||||
<Button
|
||||
onPress={next}
|
||||
label={_(msg`Continue`)}
|
||||
label="Continue"
|
||||
testID="continueBtn"
|
||||
style={mStyles.button}
|
||||
labelStyle={mStyles.buttonText}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {logger} from '#/logger'
|
||||
|
||||
type Props = {
|
||||
profile: AppBskyActorDefs.ProfileViewBasic
|
||||
dataUpdatedAt: number
|
||||
moderation: ProfileModeration
|
||||
onFollowStateChange: (props: {
|
||||
did: string
|
||||
@@ -27,12 +28,13 @@ type Props = {
|
||||
|
||||
export function RecommendedFollowsItem({
|
||||
profile,
|
||||
dataUpdatedAt,
|
||||
moderation,
|
||||
onFollowStateChange,
|
||||
}: React.PropsWithChildren<Props>) {
|
||||
const pal = usePalette('default')
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const shadowedProfile = useProfileShadow(profile)
|
||||
const shadowedProfile = useProfileShadow(profile, dataUpdatedAt)
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
|
||||
@@ -43,10 +43,10 @@ export function WelcomeMobile({next, skip}: Props) {
|
||||
/>
|
||||
<View>
|
||||
<Text style={[pal.text, styles.title]}>
|
||||
<Trans>
|
||||
Welcome to{' '}
|
||||
<Text style={[pal.text, pal.link, styles.title]}>Bluesky</Text>
|
||||
</Trans>
|
||||
Welcome to{' '}
|
||||
<Text style={[pal.text, pal.link, styles.title]}>
|
||||
<Trans>Bluesky</Trans>
|
||||
</Text>
|
||||
</Text>
|
||||
<View style={styles.spacer} />
|
||||
<View style={[styles.row]}>
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Linking,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
} from 'react-native'
|
||||
import {CenteredView} from '../util/Views'
|
||||
import {LoggedOut} from './LoggedOut'
|
||||
import {Onboarding} from './Onboarding'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {STATUS_PAGE_URL} from 'lib/constants'
|
||||
import {useOnboardingState} from '#/state/shell'
|
||||
import {useSession} from '#/state/session'
|
||||
|
||||
export const withAuthRequired = <P extends object>(
|
||||
Component: React.ComponentType<P>,
|
||||
): React.FC<P> =>
|
||||
function AuthRequired(props: P) {
|
||||
const {isInitialLoad, hasSession} = useSession()
|
||||
const onboardingState = useOnboardingState()
|
||||
if (isInitialLoad) {
|
||||
return <Loading />
|
||||
}
|
||||
if (!hasSession) {
|
||||
return <LoggedOut />
|
||||
}
|
||||
if (onboardingState.isActive) {
|
||||
return <Onboarding />
|
||||
}
|
||||
return <Component {...props} />
|
||||
}
|
||||
|
||||
function Loading() {
|
||||
const pal = usePalette('default')
|
||||
|
||||
const [isTakingTooLong, setIsTakingTooLong] = React.useState(false)
|
||||
React.useEffect(() => {
|
||||
const t = setTimeout(() => setIsTakingTooLong(true), 15e3) // 15 seconds
|
||||
return () => clearTimeout(t)
|
||||
}, [setIsTakingTooLong])
|
||||
|
||||
return (
|
||||
<CenteredView style={[styles.loading, pal.view]}>
|
||||
<ActivityIndicator size="large" />
|
||||
<Text type="2xl" style={[styles.loadingText, pal.textLight]}>
|
||||
{isTakingTooLong
|
||||
? "This is taking too long. There may be a problem with your internet or with the service, but we're going to try a couple more times..."
|
||||
: 'Connecting...'}
|
||||
</Text>
|
||||
{isTakingTooLong ? (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
Linking.openURL(STATUS_PAGE_URL)
|
||||
}}
|
||||
accessibilityRole="button">
|
||||
<Text type="2xl" style={[styles.loadingText, pal.link]}>
|
||||
Check Bluesky status page
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
</CenteredView>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
loading: {
|
||||
height: '100%',
|
||||
alignContent: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingBottom: 100,
|
||||
},
|
||||
loadingText: {
|
||||
paddingVertical: 20,
|
||||
paddingHorizontal: 20,
|
||||
textAlign: 'center',
|
||||
},
|
||||
})
|
||||
@@ -129,19 +129,19 @@ export const ComposePost = observer(function ComposePost({
|
||||
}
|
||||
openModal({
|
||||
name: 'confirm',
|
||||
title: _(msg`Discard draft`),
|
||||
title: 'Discard draft',
|
||||
onPressConfirm: onClose,
|
||||
onPressCancel: () => {
|
||||
closeModal()
|
||||
},
|
||||
message: _(msg`Are you sure you'd like to discard this draft?`),
|
||||
confirmBtnText: _(msg`Discard`),
|
||||
message: "Are you sure you'd like to discard this draft?",
|
||||
confirmBtnText: 'Discard',
|
||||
confirmBtnStyle: {backgroundColor: colors.red4},
|
||||
})
|
||||
} else {
|
||||
onClose()
|
||||
}
|
||||
}, [openModal, closeModal, activeModals, onClose, graphemeLength, gallery, _])
|
||||
}, [openModal, closeModal, activeModals, onClose, graphemeLength, gallery])
|
||||
// android back button
|
||||
useEffect(() => {
|
||||
if (!isAndroid) {
|
||||
|
||||
@@ -41,7 +41,7 @@ export function FeedPage({
|
||||
renderEmptyState: () => JSX.Element
|
||||
renderEndOfFeed?: () => JSX.Element
|
||||
}) {
|
||||
const {isSandbox, hasSession} = useSession()
|
||||
const {isSandbox} = useSession()
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {isDesktop} = useWebMediaQueries()
|
||||
@@ -123,35 +123,24 @@ export function FeedPage({
|
||||
}
|
||||
onPress={emitSoftReset}
|
||||
/>
|
||||
{hasSession && (
|
||||
<TextLink
|
||||
type="title-lg"
|
||||
href="/settings/home-feed"
|
||||
style={{fontWeight: 'bold'}}
|
||||
accessibilityLabel={_(msg`Feed Preferences`)}
|
||||
accessibilityHint=""
|
||||
text={
|
||||
<FontAwesomeIcon
|
||||
icon="sliders"
|
||||
style={pal.textLight as FontAwesomeIconStyle}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<TextLink
|
||||
type="title-lg"
|
||||
href="/settings/home-feed"
|
||||
style={{fontWeight: 'bold'}}
|
||||
accessibilityLabel={_(msg`Feed Preferences`)}
|
||||
accessibilityHint=""
|
||||
text={
|
||||
<FontAwesomeIcon
|
||||
icon="sliders"
|
||||
style={pal.textLight as FontAwesomeIconStyle}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
return <></>
|
||||
}, [
|
||||
isDesktop,
|
||||
pal.view,
|
||||
pal.text,
|
||||
pal.textLight,
|
||||
hasNew,
|
||||
_,
|
||||
isSandbox,
|
||||
hasSession,
|
||||
])
|
||||
}, [isDesktop, pal.view, pal.text, pal.textLight, hasNew, _, isSandbox])
|
||||
|
||||
return (
|
||||
<View testID={testID} style={s.h100pct}>
|
||||
@@ -177,17 +166,14 @@ export function FeedPage({
|
||||
showIndicator={hasNew}
|
||||
/>
|
||||
)}
|
||||
|
||||
{hasSession && (
|
||||
<FAB
|
||||
testID="composeFAB"
|
||||
onPress={onPressCompose}
|
||||
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`New post`)}
|
||||
accessibilityHint=""
|
||||
/>
|
||||
)}
|
||||
<FAB
|
||||
testID="composeFAB"
|
||||
onPress={onPressCompose}
|
||||
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`New post`)}
|
||||
accessibilityHint=""
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,8 +14,6 @@ import * as Toast from 'view/com/util/Toast'
|
||||
import {sanitizeHandle} from 'lib/strings/handles'
|
||||
import {logger} from '#/logger'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {
|
||||
UsePreferencesQueryResponse,
|
||||
usePreferencesQuery,
|
||||
@@ -70,7 +68,6 @@ export function FeedSourceCardLoaded({
|
||||
showLikes?: boolean
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {openModal} = useModalControls()
|
||||
|
||||
@@ -88,8 +85,8 @@ export function FeedSourceCardLoaded({
|
||||
if (isSaved) {
|
||||
openModal({
|
||||
name: 'confirm',
|
||||
title: _(msg`Remove from my feeds`),
|
||||
message: _(msg`Remove ${feed.displayName} from my feeds?`),
|
||||
title: 'Remove from my feeds',
|
||||
message: `Remove ${feed?.displayName} from my feeds?`,
|
||||
onPressConfirm: async () => {
|
||||
try {
|
||||
await removeFeed({uri: feed.uri})
|
||||
@@ -110,7 +107,7 @@ export function FeedSourceCardLoaded({
|
||||
logger.error('Failed to save feed', {error: e})
|
||||
}
|
||||
}
|
||||
}, [isSaved, openModal, feed, removeFeed, saveFeed, _])
|
||||
}, [isSaved, openModal, feed, removeFeed, saveFeed])
|
||||
|
||||
if (!feed || !preferences) return null
|
||||
|
||||
|
||||
@@ -8,14 +8,13 @@ import {
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
import {FlatList} from '../util/Views'
|
||||
import {FeedSourceCardLoaded} from './FeedSourceCard'
|
||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useProfileFeedgensQuery, RQKEY} from '#/state/queries/profile-feedgens'
|
||||
import {useProfileFeedgensQuery} from '#/state/queries/profile-feedgens'
|
||||
import {OnScrollHandler} from '#/lib/hooks/useOnMainScroll'
|
||||
import {logger} from '#/logger'
|
||||
import {Trans} from '@lingui/macro'
|
||||
@@ -30,37 +29,25 @@ const EMPTY = {_reactKey: '__empty__'}
|
||||
const ERROR_ITEM = {_reactKey: '__error__'}
|
||||
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
|
||||
|
||||
interface SectionRef {
|
||||
scrollToTop: () => void
|
||||
}
|
||||
|
||||
interface ProfileFeedgensProps {
|
||||
export function ProfileFeedgens({
|
||||
did,
|
||||
scrollElRef,
|
||||
onScroll,
|
||||
scrollEventThrottle,
|
||||
headerOffset,
|
||||
enabled,
|
||||
style,
|
||||
testID,
|
||||
}: {
|
||||
did: string
|
||||
scrollElRef: MutableRefObject<FlatList<any> | null>
|
||||
scrollElRef?: MutableRefObject<FlatList<any> | null>
|
||||
onScroll?: OnScrollHandler
|
||||
scrollEventThrottle?: number
|
||||
headerOffset: number
|
||||
enabled?: boolean
|
||||
style?: StyleProp<ViewStyle>
|
||||
testID?: string
|
||||
}
|
||||
|
||||
export const ProfileFeedgens = React.forwardRef<
|
||||
SectionRef,
|
||||
ProfileFeedgensProps
|
||||
>(function ProfileFeedgensImpl(
|
||||
{
|
||||
did,
|
||||
scrollElRef,
|
||||
onScroll,
|
||||
scrollEventThrottle,
|
||||
headerOffset,
|
||||
enabled,
|
||||
style,
|
||||
testID,
|
||||
},
|
||||
ref,
|
||||
) {
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const theme = useTheme()
|
||||
const [isPTRing, setIsPTRing] = React.useState(false)
|
||||
@@ -101,17 +88,6 @@ export const ProfileFeedgens = React.forwardRef<
|
||||
// events
|
||||
// =
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const onScrollToTop = React.useCallback(() => {
|
||||
scrollElRef.current?.scrollToOffset({offset: -headerOffset})
|
||||
queryClient.invalidateQueries({queryKey: RQKEY(did)})
|
||||
}, [scrollElRef, queryClient, headerOffset, did])
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
scrollToTop: onScrollToTop,
|
||||
}))
|
||||
|
||||
const onRefresh = React.useCallback(async () => {
|
||||
setIsPTRing(true)
|
||||
try {
|
||||
@@ -216,7 +192,7 @@ export const ProfileFeedgens = React.forwardRef<
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
item: {
|
||||
|
||||
@@ -64,6 +64,7 @@ export function ListMembers({
|
||||
|
||||
const {
|
||||
data,
|
||||
dataUpdatedAt,
|
||||
isFetching,
|
||||
isFetched,
|
||||
isError,
|
||||
@@ -184,6 +185,7 @@ export function ListMembers({
|
||||
(item as AppBskyGraphDefs.ListItemView).subject.handle
|
||||
}`}
|
||||
profile={(item as AppBskyGraphDefs.ListItemView).subject}
|
||||
dataUpdatedAt={dataUpdatedAt}
|
||||
renderButton={renderMemberButton}
|
||||
style={{paddingHorizontal: isMobile ? 8 : 14, paddingVertical: 4}}
|
||||
/>
|
||||
@@ -196,6 +198,7 @@ export function ListMembers({
|
||||
onPressTryAgain,
|
||||
onPressRetryLoadMore,
|
||||
isMobile,
|
||||
dataUpdatedAt,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
+144
-175
@@ -8,7 +8,6 @@ import {
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
import {FlatList} from '../util/Views'
|
||||
import {ListCard} from './ListCard'
|
||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||
@@ -16,7 +15,7 @@ import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useProfileListsQuery, RQKEY} from '#/state/queries/profile-lists'
|
||||
import {useProfileListsQuery} from '#/state/queries/profile-lists'
|
||||
import {OnScrollHandler} from '#/lib/hooks/useOnMainScroll'
|
||||
import {logger} from '#/logger'
|
||||
import {Trans} from '@lingui/macro'
|
||||
@@ -29,199 +28,169 @@ const EMPTY = {_reactKey: '__empty__'}
|
||||
const ERROR_ITEM = {_reactKey: '__error__'}
|
||||
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
|
||||
|
||||
interface SectionRef {
|
||||
scrollToTop: () => void
|
||||
}
|
||||
|
||||
interface ProfileListsProps {
|
||||
export function ProfileLists({
|
||||
did,
|
||||
scrollElRef,
|
||||
onScroll,
|
||||
scrollEventThrottle,
|
||||
headerOffset,
|
||||
enabled,
|
||||
style,
|
||||
testID,
|
||||
}: {
|
||||
did: string
|
||||
scrollElRef: MutableRefObject<FlatList<any> | null>
|
||||
scrollElRef?: MutableRefObject<FlatList<any> | null>
|
||||
onScroll?: OnScrollHandler
|
||||
scrollEventThrottle?: number
|
||||
headerOffset: number
|
||||
enabled?: boolean
|
||||
style?: StyleProp<ViewStyle>
|
||||
testID?: string
|
||||
}
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const theme = useTheme()
|
||||
const {track} = useAnalytics()
|
||||
const [isPTRing, setIsPTRing] = React.useState(false)
|
||||
const opts = React.useMemo(() => ({enabled}), [enabled])
|
||||
const {
|
||||
data,
|
||||
isFetching,
|
||||
isFetched,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
isError,
|
||||
error,
|
||||
refetch,
|
||||
} = useProfileListsQuery(did, opts)
|
||||
const isEmpty = !isFetching && !data?.pages[0]?.lists.length
|
||||
|
||||
export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
|
||||
function ProfileListsImpl(
|
||||
{
|
||||
did,
|
||||
scrollElRef,
|
||||
onScroll,
|
||||
scrollEventThrottle,
|
||||
headerOffset,
|
||||
enabled,
|
||||
style,
|
||||
testID,
|
||||
},
|
||||
ref,
|
||||
) {
|
||||
const pal = usePalette('default')
|
||||
const theme = useTheme()
|
||||
const {track} = useAnalytics()
|
||||
const [isPTRing, setIsPTRing] = React.useState(false)
|
||||
const opts = React.useMemo(() => ({enabled}), [enabled])
|
||||
const {
|
||||
data,
|
||||
isFetching,
|
||||
isFetched,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
isError,
|
||||
error,
|
||||
refetch,
|
||||
} = useProfileListsQuery(did, opts)
|
||||
const isEmpty = !isFetching && !data?.pages[0]?.lists.length
|
||||
|
||||
const items = React.useMemo(() => {
|
||||
let items: any[] = []
|
||||
if (isError && isEmpty) {
|
||||
items = items.concat([ERROR_ITEM])
|
||||
const items = React.useMemo(() => {
|
||||
let items: any[] = []
|
||||
if (isError && isEmpty) {
|
||||
items = items.concat([ERROR_ITEM])
|
||||
}
|
||||
if (!isFetched && isFetching) {
|
||||
items = items.concat([LOADING])
|
||||
} else if (isEmpty) {
|
||||
items = items.concat([EMPTY])
|
||||
} else if (data?.pages) {
|
||||
for (const page of data?.pages) {
|
||||
items = items.concat(page.lists)
|
||||
}
|
||||
if (!isFetched && isFetching) {
|
||||
items = items.concat([LOADING])
|
||||
} else if (isEmpty) {
|
||||
items = items.concat([EMPTY])
|
||||
} else if (data?.pages) {
|
||||
for (const page of data?.pages) {
|
||||
items = items.concat(
|
||||
page.lists.map(l => ({
|
||||
...l,
|
||||
_reactKey: l.uri,
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (isError && !isEmpty) {
|
||||
items = items.concat([LOAD_MORE_ERROR_ITEM])
|
||||
}
|
||||
return items
|
||||
}, [isError, isEmpty, isFetched, isFetching, data])
|
||||
}
|
||||
if (isError && !isEmpty) {
|
||||
items = items.concat([LOAD_MORE_ERROR_ITEM])
|
||||
}
|
||||
return items
|
||||
}, [isError, isEmpty, isFetched, isFetching, data])
|
||||
|
||||
// events
|
||||
// =
|
||||
// events
|
||||
// =
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const onRefresh = React.useCallback(async () => {
|
||||
track('Lists:onRefresh')
|
||||
setIsPTRing(true)
|
||||
try {
|
||||
await refetch()
|
||||
} catch (err) {
|
||||
logger.error('Failed to refresh lists', {error: err})
|
||||
}
|
||||
setIsPTRing(false)
|
||||
}, [refetch, track, setIsPTRing])
|
||||
|
||||
const onScrollToTop = React.useCallback(() => {
|
||||
scrollElRef.current?.scrollToOffset({offset: -headerOffset})
|
||||
queryClient.invalidateQueries({queryKey: RQKEY(did)})
|
||||
}, [scrollElRef, queryClient, headerOffset, did])
|
||||
const onEndReached = React.useCallback(async () => {
|
||||
if (isFetching || !hasNextPage || isError) return
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
scrollToTop: onScrollToTop,
|
||||
}))
|
||||
track('Lists:onEndReached')
|
||||
try {
|
||||
await fetchNextPage()
|
||||
} catch (err) {
|
||||
logger.error('Failed to load more lists', {error: err})
|
||||
}
|
||||
}, [isFetching, hasNextPage, isError, fetchNextPage, track])
|
||||
|
||||
const onRefresh = React.useCallback(async () => {
|
||||
track('Lists:onRefresh')
|
||||
setIsPTRing(true)
|
||||
try {
|
||||
await refetch()
|
||||
} catch (err) {
|
||||
logger.error('Failed to refresh lists', {error: err})
|
||||
}
|
||||
setIsPTRing(false)
|
||||
}, [refetch, track, setIsPTRing])
|
||||
const onPressRetryLoadMore = React.useCallback(() => {
|
||||
fetchNextPage()
|
||||
}, [fetchNextPage])
|
||||
|
||||
const onEndReached = React.useCallback(async () => {
|
||||
if (isFetching || !hasNextPage || isError) return
|
||||
// rendering
|
||||
// =
|
||||
|
||||
track('Lists:onEndReached')
|
||||
try {
|
||||
await fetchNextPage()
|
||||
} catch (err) {
|
||||
logger.error('Failed to load more lists', {error: err})
|
||||
}
|
||||
}, [isFetching, hasNextPage, isError, fetchNextPage, track])
|
||||
|
||||
const onPressRetryLoadMore = React.useCallback(() => {
|
||||
fetchNextPage()
|
||||
}, [fetchNextPage])
|
||||
|
||||
// rendering
|
||||
// =
|
||||
|
||||
const renderItemInner = React.useCallback(
|
||||
({item}: {item: any}) => {
|
||||
if (item === EMPTY) {
|
||||
return (
|
||||
<View
|
||||
testID="listsEmpty"
|
||||
style={[{padding: 18, borderTopWidth: 1}, pal.border]}>
|
||||
<Text style={pal.textLight}>
|
||||
<Trans>You have no lists.</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
} else if (item === ERROR_ITEM) {
|
||||
return (
|
||||
<ErrorMessage
|
||||
message={cleanError(error)}
|
||||
onPressTryAgain={refetch}
|
||||
/>
|
||||
)
|
||||
} else if (item === LOAD_MORE_ERROR_ITEM) {
|
||||
return (
|
||||
<LoadMoreRetryBtn
|
||||
label="There was an issue fetching your lists. Tap here to try again."
|
||||
onPress={onPressRetryLoadMore}
|
||||
/>
|
||||
)
|
||||
} else if (item === LOADING) {
|
||||
return (
|
||||
<View style={{padding: 20}}>
|
||||
<ActivityIndicator />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
const renderItemInner = React.useCallback(
|
||||
({item}: {item: any}) => {
|
||||
if (item === EMPTY) {
|
||||
return (
|
||||
<ListCard
|
||||
list={item}
|
||||
testID={`list-${item.name}`}
|
||||
style={styles.item}
|
||||
<View
|
||||
testID="listsEmpty"
|
||||
style={[{padding: 18, borderTopWidth: 1}, pal.border]}>
|
||||
<Text style={pal.textLight}>
|
||||
<Trans>You have no lists.</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
} else if (item === ERROR_ITEM) {
|
||||
return (
|
||||
<ErrorMessage message={cleanError(error)} onPressTryAgain={refetch} />
|
||||
)
|
||||
} else if (item === LOAD_MORE_ERROR_ITEM) {
|
||||
return (
|
||||
<LoadMoreRetryBtn
|
||||
label="There was an issue fetching your lists. Tap here to try again."
|
||||
onPress={onPressRetryLoadMore}
|
||||
/>
|
||||
)
|
||||
},
|
||||
[error, refetch, onPressRetryLoadMore, pal],
|
||||
)
|
||||
|
||||
const scrollHandler = useAnimatedScrollHandler(onScroll || {})
|
||||
return (
|
||||
<View testID={testID} style={style}>
|
||||
<FlatList
|
||||
testID={testID ? `${testID}-flatlist` : undefined}
|
||||
ref={scrollElRef}
|
||||
data={items}
|
||||
keyExtractor={(item: any) => item._reactKey}
|
||||
renderItem={renderItemInner}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={isPTRing}
|
||||
onRefresh={onRefresh}
|
||||
tintColor={pal.colors.text}
|
||||
titleColor={pal.colors.text}
|
||||
progressViewOffset={headerOffset}
|
||||
/>
|
||||
}
|
||||
contentContainerStyle={{
|
||||
minHeight: Dimensions.get('window').height * 1.5,
|
||||
}}
|
||||
style={{paddingTop: headerOffset}}
|
||||
onScroll={onScroll != null ? scrollHandler : undefined}
|
||||
scrollEventThrottle={scrollEventThrottle}
|
||||
indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
|
||||
removeClippedSubviews={true}
|
||||
contentOffset={{x: 0, y: headerOffset * -1}}
|
||||
// @ts-ignore our .web version only -prf
|
||||
desktopFixedHeight
|
||||
onEndReached={onEndReached}
|
||||
} else if (item === LOADING) {
|
||||
return (
|
||||
<View style={{padding: 20}}>
|
||||
<ActivityIndicator />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<ListCard
|
||||
list={item}
|
||||
testID={`list-${item.name}`}
|
||||
style={styles.item}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
},
|
||||
)
|
||||
)
|
||||
},
|
||||
[error, refetch, onPressRetryLoadMore, pal],
|
||||
)
|
||||
|
||||
const scrollHandler = useAnimatedScrollHandler(onScroll || {})
|
||||
return (
|
||||
<View testID={testID} style={style}>
|
||||
<FlatList
|
||||
testID={testID ? `${testID}-flatlist` : undefined}
|
||||
ref={scrollElRef}
|
||||
data={items}
|
||||
keyExtractor={(item: any) => item._reactKey}
|
||||
renderItem={renderItemInner}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={isPTRing}
|
||||
onRefresh={onRefresh}
|
||||
tintColor={pal.colors.text}
|
||||
titleColor={pal.colors.text}
|
||||
progressViewOffset={headerOffset}
|
||||
/>
|
||||
}
|
||||
contentContainerStyle={{
|
||||
minHeight: Dimensions.get('window').height * 1.5,
|
||||
}}
|
||||
style={{paddingTop: headerOffset}}
|
||||
onScroll={onScroll != null ? scrollHandler : undefined}
|
||||
scrollEventThrottle={scrollEventThrottle}
|
||||
indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
|
||||
removeClippedSubviews={true}
|
||||
contentOffset={{x: 0, y: headerOffset * -1}}
|
||||
// @ts-ignore our .web version only -prf
|
||||
desktopFixedHeight
|
||||
onEndReached={onEndReached}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
item: {
|
||||
|
||||
@@ -22,6 +22,7 @@ export function Component({did}: {did: string}) {
|
||||
const moderationOpts = useModerationOpts()
|
||||
const {
|
||||
data: profile,
|
||||
dataUpdatedAt,
|
||||
error: profileError,
|
||||
refetch: refetchProfile,
|
||||
isFetching: isFetchingProfile,
|
||||
@@ -50,7 +51,13 @@ export function Component({did}: {did: string}) {
|
||||
)
|
||||
}
|
||||
if (profile && moderationOpts) {
|
||||
return <ComponentLoaded profile={profile} moderationOpts={moderationOpts} />
|
||||
return (
|
||||
<ComponentLoaded
|
||||
profile={profile}
|
||||
dataUpdatedAt={dataUpdatedAt}
|
||||
moderationOpts={moderationOpts}
|
||||
/>
|
||||
)
|
||||
}
|
||||
// should never happen
|
||||
return (
|
||||
@@ -64,13 +71,15 @@ export function Component({did}: {did: string}) {
|
||||
|
||||
function ComponentLoaded({
|
||||
profile: profileUnshadowed,
|
||||
dataUpdatedAt,
|
||||
moderationOpts,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
dataUpdatedAt: number
|
||||
moderationOpts: ModerationOpts
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const profile = useProfileShadow(profileUnshadowed)
|
||||
const profile = useProfileShadow(profileUnshadowed, dataUpdatedAt)
|
||||
const {screen} = useAnalytics()
|
||||
const moderation = React.useMemo(
|
||||
() => moderateProfile(profile, moderationOpts),
|
||||
|
||||
@@ -75,7 +75,7 @@ function SwitchAccountCard({account}: {account: SessionAccount}) {
|
||||
did: currentAccount.did,
|
||||
handle: currentAccount.handle,
|
||||
})}
|
||||
title={_(msg`Your profile`)}
|
||||
title="Your profile"
|
||||
noFeedback>
|
||||
{contents}
|
||||
</Link>
|
||||
|
||||
@@ -38,6 +38,7 @@ export function Feed({
|
||||
const {markAllRead} = useUnreadNotificationsApi()
|
||||
const {
|
||||
data,
|
||||
dataUpdatedAt,
|
||||
isLoading,
|
||||
isFetching,
|
||||
isFetched,
|
||||
@@ -131,9 +132,15 @@ export function Feed({
|
||||
} else if (item === LOADING_ITEM) {
|
||||
return <NotificationFeedLoadingPlaceholder />
|
||||
}
|
||||
return <FeedItem item={item} moderationOpts={moderationOpts!} />
|
||||
return (
|
||||
<FeedItem
|
||||
item={item}
|
||||
dataUpdatedAt={dataUpdatedAt}
|
||||
moderationOpts={moderationOpts!}
|
||||
/>
|
||||
)
|
||||
},
|
||||
[onPressRetryLoadMore, moderationOpts],
|
||||
[onPressRetryLoadMore, dataUpdatedAt, moderationOpts],
|
||||
)
|
||||
|
||||
const showHeaderSpinner = !isPTRing && isFetching && !isLoading
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {memo, useMemo, useState, useEffect} from 'react'
|
||||
import React, {useMemo, useState, useEffect} from 'react'
|
||||
import {
|
||||
Animated,
|
||||
TouchableOpacity,
|
||||
@@ -56,13 +56,15 @@ interface Author {
|
||||
moderation: ProfileModeration
|
||||
}
|
||||
|
||||
let FeedItem = ({
|
||||
export function FeedItem({
|
||||
item,
|
||||
dataUpdatedAt,
|
||||
moderationOpts,
|
||||
}: {
|
||||
item: FeedNotification
|
||||
dataUpdatedAt: number
|
||||
moderationOpts: ModerationOpts
|
||||
}): React.ReactNode => {
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const [isAuthorsExpanded, setAuthorsExpanded] = useState<boolean>(false)
|
||||
const itemHref = useMemo(() => {
|
||||
@@ -133,6 +135,7 @@ let FeedItem = ({
|
||||
accessible={false}>
|
||||
<Post
|
||||
post={item.subject}
|
||||
dataUpdatedAt={dataUpdatedAt}
|
||||
style={
|
||||
item.notification.isRead
|
||||
? undefined
|
||||
@@ -232,8 +235,7 @@ let FeedItem = ({
|
||||
{authors.length > 1 ? (
|
||||
<>
|
||||
<Text style={[pal.text, s.mr5, s.ml5]}>
|
||||
{' '}
|
||||
<Trans>and</Trans>{' '}
|
||||
<Trans>and</Trans>
|
||||
</Text>
|
||||
<Text style={[pal.text, s.bold]}>
|
||||
{formatCount(authors.length - 1)}{' '}
|
||||
@@ -260,8 +262,6 @@ let FeedItem = ({
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
FeedItem = memo(FeedItem)
|
||||
export {FeedItem}
|
||||
|
||||
function ExpandListPressable({
|
||||
hasMultipleAuthors,
|
||||
|
||||
@@ -20,6 +20,7 @@ export function PostLikedBy({uri}: {uri: string}) {
|
||||
} = useResolveUriQuery(uri)
|
||||
const {
|
||||
data,
|
||||
dataUpdatedAt,
|
||||
isFetching,
|
||||
isFetched,
|
||||
isFetchingNextPage,
|
||||
@@ -54,11 +55,18 @@ export function PostLikedBy({uri}: {uri: string}) {
|
||||
}
|
||||
}, [isFetching, hasNextPage, isError, fetchNextPage])
|
||||
|
||||
const renderItem = useCallback(({item}: {item: GetLikes.Like}) => {
|
||||
return (
|
||||
<ProfileCardWithFollowBtn key={item.actor.did} profile={item.actor} />
|
||||
)
|
||||
}, [])
|
||||
const renderItem = useCallback(
|
||||
({item}: {item: GetLikes.Like}) => {
|
||||
return (
|
||||
<ProfileCardWithFollowBtn
|
||||
key={item.actor.did}
|
||||
profile={item.actor}
|
||||
dataUpdatedAt={dataUpdatedAt}
|
||||
/>
|
||||
)
|
||||
},
|
||||
[dataUpdatedAt],
|
||||
)
|
||||
|
||||
if (isFetchingResolvedUri || !isFetched) {
|
||||
return (
|
||||
|
||||
@@ -20,6 +20,7 @@ export function PostRepostedBy({uri}: {uri: string}) {
|
||||
} = useResolveUriQuery(uri)
|
||||
const {
|
||||
data,
|
||||
dataUpdatedAt,
|
||||
isFetching,
|
||||
isFetched,
|
||||
isFetchingNextPage,
|
||||
@@ -56,9 +57,15 @@ export function PostRepostedBy({uri}: {uri: string}) {
|
||||
|
||||
const renderItem = useCallback(
|
||||
({item}: {item: ActorDefs.ProfileViewBasic}) => {
|
||||
return <ProfileCardWithFollowBtn key={item.did} profile={item} />
|
||||
return (
|
||||
<ProfileCardWithFollowBtn
|
||||
key={item.did}
|
||||
profile={item}
|
||||
dataUpdatedAt={dataUpdatedAt}
|
||||
/>
|
||||
)
|
||||
},
|
||||
[],
|
||||
[dataUpdatedAt],
|
||||
)
|
||||
|
||||
if (isFetchingResolvedUri || !isFetched) {
|
||||
|
||||
@@ -38,7 +38,6 @@ import {
|
||||
UsePreferencesQueryResponse,
|
||||
usePreferencesQuery,
|
||||
} from '#/state/queries/preferences'
|
||||
import {useSession} from '#/state/session'
|
||||
|
||||
// const MAINTAIN_VISIBLE_CONTENT_POSITION = {minIndexForVisible: 2} TODO
|
||||
|
||||
@@ -74,6 +73,7 @@ export function PostThread({
|
||||
refetch,
|
||||
isRefetching,
|
||||
data: thread,
|
||||
dataUpdatedAt,
|
||||
} = usePostThreadQuery(uri)
|
||||
const {data: preferences} = usePreferencesQuery()
|
||||
const rootPost = thread?.type === 'post' ? thread.post : undefined
|
||||
@@ -111,6 +111,7 @@ export function PostThread({
|
||||
<PostThreadLoaded
|
||||
thread={thread}
|
||||
isRefetching={isRefetching}
|
||||
dataUpdatedAt={dataUpdatedAt}
|
||||
threadViewPrefs={preferences.threadViewPrefs}
|
||||
onRefresh={refetch}
|
||||
onPressReply={onPressReply}
|
||||
@@ -121,17 +122,18 @@ export function PostThread({
|
||||
function PostThreadLoaded({
|
||||
thread,
|
||||
isRefetching,
|
||||
dataUpdatedAt,
|
||||
threadViewPrefs,
|
||||
onRefresh,
|
||||
onPressReply,
|
||||
}: {
|
||||
thread: ThreadNode
|
||||
isRefetching: boolean
|
||||
dataUpdatedAt: number
|
||||
threadViewPrefs: UsePreferencesQueryResponse['threadViewPrefs']
|
||||
onRefresh: () => void
|
||||
onPressReply: () => void
|
||||
}) {
|
||||
const {hasSession} = useSession()
|
||||
const {_} = useLingui()
|
||||
const pal = usePalette('default')
|
||||
const {isTablet, isDesktop} = useWebMediaQueries()
|
||||
@@ -218,14 +220,14 @@ function PostThreadLoaded({
|
||||
const renderItem = React.useCallback(
|
||||
({item, index}: {item: YieldedItem; index: number}) => {
|
||||
if (item === TOP_COMPONENT) {
|
||||
return isTablet ? <ViewHeader title={_(msg`Post`)} /> : null
|
||||
return isTablet ? <ViewHeader title="Post" /> : null
|
||||
} else if (item === PARENT_SPINNER) {
|
||||
return (
|
||||
<View style={styles.parentSpinner}>
|
||||
<ActivityIndicator />
|
||||
</View>
|
||||
)
|
||||
} else if (item === REPLY_PROMPT && hasSession) {
|
||||
} else if (item === REPLY_PROMPT) {
|
||||
return (
|
||||
<View>
|
||||
{isDesktop && <ComposePrompt onPressCompose={onPressReply} />}
|
||||
@@ -293,6 +295,7 @@ function PostThreadLoaded({
|
||||
<PostThreadItem
|
||||
post={item.post}
|
||||
record={item.record}
|
||||
dataUpdatedAt={dataUpdatedAt}
|
||||
treeView={threadViewPrefs.lab_treeViewEnabled || false}
|
||||
depth={item.ctx.depth}
|
||||
isHighlightedPost={item.ctx.isHighlightedPost}
|
||||
@@ -307,7 +310,6 @@ function PostThreadLoaded({
|
||||
return null
|
||||
},
|
||||
[
|
||||
hasSession,
|
||||
isTablet,
|
||||
isDesktop,
|
||||
onPressReply,
|
||||
@@ -320,6 +322,7 @@ function PostThreadLoaded({
|
||||
posts,
|
||||
onRefresh,
|
||||
threadViewPrefs.lab_treeViewEnabled,
|
||||
dataUpdatedAt,
|
||||
_,
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {memo, useMemo} from 'react'
|
||||
import React, {useMemo} from 'react'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import {
|
||||
AtUri,
|
||||
@@ -35,8 +35,7 @@ import {TimeElapsed} from 'view/com/util/TimeElapsed'
|
||||
import {makeProfileLink} from 'lib/routes/links'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {MAX_POST_LINES} from 'lib/constants'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/macro'
|
||||
import {useLanguagePrefs} from '#/state/preferences'
|
||||
import {useComposerControls} from '#/state/shell/composer'
|
||||
import {useModerationOpts} from '#/state/queries/preferences'
|
||||
@@ -45,6 +44,7 @@ import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow'
|
||||
export function PostThreadItem({
|
||||
post,
|
||||
record,
|
||||
dataUpdatedAt,
|
||||
treeView,
|
||||
depth,
|
||||
isHighlightedPost,
|
||||
@@ -56,6 +56,7 @@ export function PostThreadItem({
|
||||
}: {
|
||||
post: AppBskyFeedDefs.PostView
|
||||
record: AppBskyFeedPost.Record
|
||||
dataUpdatedAt: number
|
||||
treeView: boolean
|
||||
depth: number
|
||||
isHighlightedPost?: boolean
|
||||
@@ -66,7 +67,7 @@ export function PostThreadItem({
|
||||
onPostReply: () => void
|
||||
}) {
|
||||
const moderationOpts = useModerationOpts()
|
||||
const postShadowed = usePostShadow(post)
|
||||
const postShadowed = usePostShadow(post, dataUpdatedAt)
|
||||
const richText = useMemo(
|
||||
() =>
|
||||
new RichTextAPI({
|
||||
@@ -117,7 +118,7 @@ function PostThreadItemDeleted() {
|
||||
)
|
||||
}
|
||||
|
||||
let PostThreadItemLoaded = ({
|
||||
function PostThreadItemLoaded({
|
||||
post,
|
||||
record,
|
||||
richText,
|
||||
@@ -143,12 +144,12 @@ let PostThreadItemLoaded = ({
|
||||
showParentReplyLine?: boolean
|
||||
hasPrecedingItem: boolean
|
||||
onPostReply: () => void
|
||||
}): React.ReactNode => {
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const langPrefs = useLanguagePrefs()
|
||||
const {openComposer} = useComposerControls()
|
||||
const [limitLines, setLimitLines] = React.useState(
|
||||
() => countLines(richText?.text) >= MAX_POST_LINES,
|
||||
countLines(richText?.text) >= MAX_POST_LINES,
|
||||
)
|
||||
const styles = useStyles()
|
||||
const hasEngagement = post.likeCount || post.repostCount
|
||||
@@ -564,7 +565,6 @@ let PostThreadItemLoaded = ({
|
||||
)
|
||||
}
|
||||
}
|
||||
PostThreadItemLoaded = memo(PostThreadItemLoaded)
|
||||
|
||||
function PostOuterWrapper({
|
||||
post,
|
||||
@@ -636,14 +636,13 @@ function ExpandedPostDetails({
|
||||
translatorUrl: string
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
return (
|
||||
<View style={[s.flexRow, s.mt2, s.mb10]}>
|
||||
<Text style={pal.textLight}>{niceDate(post.indexedAt)}</Text>
|
||||
{needsTranslation && (
|
||||
<>
|
||||
<Text style={[pal.textLight, s.ml5, s.mr5]}>•</Text>
|
||||
<Link href={translatorUrl} title={_(msg`Translate`)}>
|
||||
<Link href={translatorUrl} title="Translate">
|
||||
<Text style={pal.link}>
|
||||
<Trans>Translate</Trans>
|
||||
</Text>
|
||||
|
||||
@@ -30,10 +30,12 @@ import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow'
|
||||
|
||||
export function Post({
|
||||
post,
|
||||
dataUpdatedAt,
|
||||
showReplyLine,
|
||||
style,
|
||||
}: {
|
||||
post: AppBskyFeedDefs.PostView
|
||||
dataUpdatedAt: number
|
||||
showReplyLine?: boolean
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
@@ -46,7 +48,7 @@ export function Post({
|
||||
: undefined,
|
||||
[post],
|
||||
)
|
||||
const postShadowed = usePostShadow(post)
|
||||
const postShadowed = usePostShadow(post, dataUpdatedAt)
|
||||
const richText = useMemo(
|
||||
() =>
|
||||
record
|
||||
@@ -97,7 +99,7 @@ function PostInner({
|
||||
const pal = usePalette('default')
|
||||
const {openComposer} = useComposerControls()
|
||||
const [limitLines, setLimitLines] = useState(
|
||||
() => countLines(richText?.text) >= MAX_POST_LINES,
|
||||
countLines(richText?.text) >= MAX_POST_LINES,
|
||||
)
|
||||
const itemUrip = new AtUri(post.uri)
|
||||
const itemHref = makeProfileLink(post.author, 'post', itemUrip.rkey)
|
||||
|
||||
@@ -76,6 +76,7 @@ let Feed = ({
|
||||
const opts = React.useMemo(() => ({enabled}), [enabled])
|
||||
const {
|
||||
data,
|
||||
dataUpdatedAt,
|
||||
isFetching,
|
||||
isFetched,
|
||||
isError,
|
||||
@@ -199,6 +200,7 @@ let Feed = ({
|
||||
return (
|
||||
<FeedSlice
|
||||
slice={item}
|
||||
dataUpdatedAt={dataUpdatedAt}
|
||||
// we check for this before creating the feedItems array
|
||||
moderationOpts={moderationOpts!}
|
||||
/>
|
||||
@@ -206,6 +208,7 @@ let Feed = ({
|
||||
},
|
||||
[
|
||||
feed,
|
||||
dataUpdatedAt,
|
||||
error,
|
||||
onPressTryAgain,
|
||||
onPressRetryLoadMore,
|
||||
|
||||
@@ -10,8 +10,6 @@ import {useNavigation} from '@react-navigation/native'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
import {logger} from '#/logger'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {msg as msgLingui} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {FeedDescriptor} from '#/state/queries/post-feed'
|
||||
import {EmptyState} from '../util/EmptyState'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
@@ -88,7 +86,6 @@ function FeedgenErrorMessage({
|
||||
knownError: KnownError
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const {_: _l} = useLingui()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const msg = MESSAGES[knownError]
|
||||
const [_, uri] = feedDesc.split('|')
|
||||
@@ -103,8 +100,8 @@ function FeedgenErrorMessage({
|
||||
const onRemoveFeed = React.useCallback(async () => {
|
||||
openModal({
|
||||
name: 'confirm',
|
||||
title: _l(msgLingui`Remove feed`),
|
||||
message: _l(msgLingui`Remove this feed from your saved feeds?`),
|
||||
title: 'Remove feed',
|
||||
message: 'Remove this feed from your saved feeds?',
|
||||
async onPressConfirm() {
|
||||
try {
|
||||
await removeFeed({uri})
|
||||
@@ -119,7 +116,7 @@ function FeedgenErrorMessage({
|
||||
closeModal()
|
||||
},
|
||||
})
|
||||
}, [openModal, closeModal, uri, removeFeed, _l])
|
||||
}, [openModal, closeModal, uri, removeFeed])
|
||||
|
||||
return (
|
||||
<View
|
||||
|
||||
@@ -40,6 +40,7 @@ export function FeedItem({
|
||||
record,
|
||||
reason,
|
||||
moderation,
|
||||
dataUpdatedAt,
|
||||
isThreadChild,
|
||||
isThreadLastChild,
|
||||
isThreadParent,
|
||||
@@ -48,11 +49,12 @@ export function FeedItem({
|
||||
record: AppBskyFeedPost.Record
|
||||
reason: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource | undefined
|
||||
moderation: PostModeration
|
||||
dataUpdatedAt: number
|
||||
isThreadChild?: boolean
|
||||
isThreadLastChild?: boolean
|
||||
isThreadParent?: boolean
|
||||
}) {
|
||||
const postShadowed = usePostShadow(post)
|
||||
const postShadowed = usePostShadow(post, dataUpdatedAt)
|
||||
const richText = useMemo(
|
||||
() =>
|
||||
new RichTextAPI({
|
||||
@@ -104,7 +106,7 @@ let FeedItemInner = ({
|
||||
const pal = usePalette('default')
|
||||
const {track} = useAnalytics()
|
||||
const [limitLines, setLimitLines] = useState(
|
||||
() => countLines(richText.text) >= MAX_POST_LINES,
|
||||
countLines(richText.text) >= MAX_POST_LINES,
|
||||
)
|
||||
|
||||
const href = useMemo(() => {
|
||||
|
||||
@@ -11,10 +11,12 @@ import {makeProfileLink} from 'lib/routes/links'
|
||||
|
||||
let FeedSlice = ({
|
||||
slice,
|
||||
dataUpdatedAt,
|
||||
ignoreFilterFor,
|
||||
moderationOpts,
|
||||
}: {
|
||||
slice: FeedPostSlice
|
||||
dataUpdatedAt: number
|
||||
ignoreFilterFor?: string
|
||||
moderationOpts: ModerationOpts
|
||||
}): React.ReactNode => {
|
||||
@@ -42,6 +44,7 @@ let FeedSlice = ({
|
||||
record={slice.items[0].record}
|
||||
reason={slice.items[0].reason}
|
||||
moderation={moderations[0]}
|
||||
dataUpdatedAt={dataUpdatedAt}
|
||||
isThreadParent={isThreadParentAt(slice.items, 0)}
|
||||
isThreadChild={isThreadChildAt(slice.items, 0)}
|
||||
/>
|
||||
@@ -51,6 +54,7 @@ let FeedSlice = ({
|
||||
record={slice.items[1].record}
|
||||
reason={slice.items[1].reason}
|
||||
moderation={moderations[1]}
|
||||
dataUpdatedAt={dataUpdatedAt}
|
||||
isThreadParent={isThreadParentAt(slice.items, 1)}
|
||||
isThreadChild={isThreadChildAt(slice.items, 1)}
|
||||
/>
|
||||
@@ -61,6 +65,7 @@ let FeedSlice = ({
|
||||
record={slice.items[last].record}
|
||||
reason={slice.items[last].reason}
|
||||
moderation={moderations[last]}
|
||||
dataUpdatedAt={dataUpdatedAt}
|
||||
isThreadParent={isThreadParentAt(slice.items, last)}
|
||||
isThreadChild={isThreadChildAt(slice.items, last)}
|
||||
isThreadLastChild
|
||||
@@ -78,6 +83,7 @@ let FeedSlice = ({
|
||||
record={slice.items[i].record}
|
||||
reason={slice.items[i].reason}
|
||||
moderation={moderations[i]}
|
||||
dataUpdatedAt={dataUpdatedAt}
|
||||
isThreadParent={isThreadParentAt(slice.items, i)}
|
||||
isThreadChild={isThreadChildAt(slice.items, i)}
|
||||
isThreadLastChild={
|
||||
|
||||
@@ -27,6 +27,7 @@ import {useSession} from '#/state/session'
|
||||
export function ProfileCard({
|
||||
testID,
|
||||
profile: profileUnshadowed,
|
||||
dataUpdatedAt,
|
||||
noBg,
|
||||
noBorder,
|
||||
followers,
|
||||
@@ -35,6 +36,7 @@ export function ProfileCard({
|
||||
}: {
|
||||
testID?: string
|
||||
profile: AppBskyActorDefs.ProfileViewBasic
|
||||
dataUpdatedAt: number
|
||||
noBg?: boolean
|
||||
noBorder?: boolean
|
||||
followers?: AppBskyActorDefs.ProfileView[] | undefined
|
||||
@@ -44,7 +46,7 @@ export function ProfileCard({
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const profile = useProfileShadow(profileUnshadowed)
|
||||
const profile = useProfileShadow(profileUnshadowed, dataUpdatedAt)
|
||||
const moderationOpts = useModerationOpts()
|
||||
if (!moderationOpts) {
|
||||
return null
|
||||
@@ -200,11 +202,13 @@ export function ProfileCardWithFollowBtn({
|
||||
noBg,
|
||||
noBorder,
|
||||
followers,
|
||||
dataUpdatedAt,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewBasic
|
||||
noBg?: boolean
|
||||
noBorder?: boolean
|
||||
followers?: AppBskyActorDefs.ProfileView[] | undefined
|
||||
dataUpdatedAt: number
|
||||
}) {
|
||||
const {currentAccount} = useSession()
|
||||
const isMe = profile.did === currentAccount?.did
|
||||
@@ -220,6 +224,7 @@ export function ProfileCardWithFollowBtn({
|
||||
? undefined
|
||||
: profileShadow => <FollowButton profile={profileShadow} />
|
||||
}
|
||||
dataUpdatedAt={dataUpdatedAt}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ export function ProfileFollowers({name}: {name: string}) {
|
||||
} = useResolveDidQuery(name)
|
||||
const {
|
||||
data,
|
||||
dataUpdatedAt,
|
||||
isFetching,
|
||||
isFetched,
|
||||
isFetchingNextPage,
|
||||
@@ -57,9 +58,13 @@ export function ProfileFollowers({name}: {name: string}) {
|
||||
|
||||
const renderItem = React.useCallback(
|
||||
({item}: {item: ActorDefs.ProfileViewBasic}) => (
|
||||
<ProfileCardWithFollowBtn key={item.did} profile={item} />
|
||||
<ProfileCardWithFollowBtn
|
||||
key={item.did}
|
||||
profile={item}
|
||||
dataUpdatedAt={dataUpdatedAt}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
[dataUpdatedAt],
|
||||
)
|
||||
|
||||
if (isFetchingDid || !isFetched) {
|
||||
|
||||
@@ -20,6 +20,7 @@ export function ProfileFollows({name}: {name: string}) {
|
||||
} = useResolveDidQuery(name)
|
||||
const {
|
||||
data,
|
||||
dataUpdatedAt,
|
||||
isFetching,
|
||||
isFetched,
|
||||
isFetchingNextPage,
|
||||
@@ -57,9 +58,13 @@ export function ProfileFollows({name}: {name: string}) {
|
||||
|
||||
const renderItem = React.useCallback(
|
||||
({item}: {item: ActorDefs.ProfileViewBasic}) => (
|
||||
<ProfileCardWithFollowBtn key={item.did} profile={item} />
|
||||
<ProfileCardWithFollowBtn
|
||||
key={item.did}
|
||||
profile={item}
|
||||
dataUpdatedAt={dataUpdatedAt}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
[dataUpdatedAt],
|
||||
)
|
||||
|
||||
if (isFetchingDid || !isFetched) {
|
||||
|
||||
@@ -51,7 +51,6 @@ import {s, colors} from 'lib/styles'
|
||||
import {logger} from '#/logger'
|
||||
import {useSession} from '#/state/session'
|
||||
import {Shadow} from '#/state/cache/types'
|
||||
import {useRequireAuth} from '#/state/session'
|
||||
|
||||
interface Props {
|
||||
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> | null
|
||||
@@ -114,8 +113,7 @@ let ProfileHeaderLoaded = ({
|
||||
}: LoadedProps): React.ReactNode => {
|
||||
const pal = usePalette('default')
|
||||
const palInverted = usePalette('inverted')
|
||||
const {currentAccount, hasSession} = useSession()
|
||||
const requireAuth = useRequireAuth()
|
||||
const {currentAccount} = useSession()
|
||||
const {_} = useLingui()
|
||||
const {openModal} = useModalControls()
|
||||
const {openLightbox} = useLightboxControls()
|
||||
@@ -152,42 +150,38 @@ let ProfileHeaderLoaded = ({
|
||||
}
|
||||
}, [openLightbox, profile, moderation])
|
||||
|
||||
const onPressFollow = () => {
|
||||
requireAuth(async () => {
|
||||
try {
|
||||
track('ProfileHeader:FollowButtonClicked')
|
||||
await queueFollow()
|
||||
Toast.show(
|
||||
`Following ${sanitizeDisplayName(
|
||||
profile.displayName || profile.handle,
|
||||
)}`,
|
||||
)
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
logger.error('Failed to follow', {error: String(e)})
|
||||
Toast.show(`There was an issue! ${e.toString()}`)
|
||||
}
|
||||
const onPressFollow = async () => {
|
||||
try {
|
||||
track('ProfileHeader:FollowButtonClicked')
|
||||
await queueFollow()
|
||||
Toast.show(
|
||||
`Following ${sanitizeDisplayName(
|
||||
profile.displayName || profile.handle,
|
||||
)}`,
|
||||
)
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
logger.error('Failed to follow', {error: String(e)})
|
||||
Toast.show(`There was an issue! ${e.toString()}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const onPressUnfollow = () => {
|
||||
requireAuth(async () => {
|
||||
try {
|
||||
track('ProfileHeader:UnfollowButtonClicked')
|
||||
await queueUnfollow()
|
||||
Toast.show(
|
||||
`No longer following ${sanitizeDisplayName(
|
||||
profile.displayName || profile.handle,
|
||||
)}`,
|
||||
)
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
logger.error('Failed to unfollow', {error: String(e)})
|
||||
Toast.show(`There was an issue! ${e.toString()}`)
|
||||
}
|
||||
const onPressUnfollow = async () => {
|
||||
try {
|
||||
track('ProfileHeader:UnfollowButtonClicked')
|
||||
await queueUnfollow()
|
||||
Toast.show(
|
||||
`No longer following ${sanitizeDisplayName(
|
||||
profile.displayName || profile.handle,
|
||||
)}`,
|
||||
)
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
logger.error('Failed to unfollow', {error: String(e)})
|
||||
Toast.show(`There was an issue! ${e.toString()}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const onPressEditProfile = React.useCallback(() => {
|
||||
@@ -242,10 +236,9 @@ let ProfileHeaderLoaded = ({
|
||||
track('ProfileHeader:BlockAccountButtonClicked')
|
||||
openModal({
|
||||
name: 'confirm',
|
||||
title: _(msg`Block Account`),
|
||||
message: _(
|
||||
msg`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`,
|
||||
),
|
||||
title: 'Block Account',
|
||||
message:
|
||||
'Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.',
|
||||
onPressConfirm: async () => {
|
||||
try {
|
||||
await queueBlock()
|
||||
@@ -258,16 +251,15 @@ let ProfileHeaderLoaded = ({
|
||||
}
|
||||
},
|
||||
})
|
||||
}, [track, queueBlock, openModal, _])
|
||||
}, [track, queueBlock, openModal])
|
||||
|
||||
const onPressUnblockAccount = React.useCallback(async () => {
|
||||
track('ProfileHeader:UnblockAccountButtonClicked')
|
||||
openModal({
|
||||
name: 'confirm',
|
||||
title: _(msg`Unblock Account`),
|
||||
message: _(
|
||||
msg`The account will be able to interact with you after unblocking.`,
|
||||
),
|
||||
title: 'Unblock Account',
|
||||
message:
|
||||
'The account will be able to interact with you after unblocking.',
|
||||
onPressConfirm: async () => {
|
||||
try {
|
||||
await queueUnblock()
|
||||
@@ -280,7 +272,7 @@ let ProfileHeaderLoaded = ({
|
||||
}
|
||||
},
|
||||
})
|
||||
}, [track, queueUnblock, openModal, _])
|
||||
}, [track, queueUnblock, openModal])
|
||||
|
||||
const onPressReportAccount = React.useCallback(() => {
|
||||
track('ProfileHeader:ReportAccountButtonClicked')
|
||||
@@ -298,7 +290,7 @@ let ProfileHeaderLoaded = ({
|
||||
let items: DropdownItem[] = [
|
||||
{
|
||||
testID: 'profileHeaderDropdownShareBtn',
|
||||
label: _(msg`Share`),
|
||||
label: 'Share',
|
||||
onPress: onPressShare,
|
||||
icon: {
|
||||
ios: {
|
||||
@@ -309,75 +301,68 @@ let ProfileHeaderLoaded = ({
|
||||
},
|
||||
},
|
||||
]
|
||||
if (hasSession) {
|
||||
items.push({label: 'separator'})
|
||||
items.push({
|
||||
testID: 'profileHeaderDropdownListAddRemoveBtn',
|
||||
label: _(msg`Add to Lists`),
|
||||
onPress: onPressAddRemoveLists,
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'list.bullet',
|
||||
},
|
||||
android: 'ic_menu_add',
|
||||
web: 'list',
|
||||
items.push({label: 'separator'})
|
||||
items.push({
|
||||
testID: 'profileHeaderDropdownListAddRemoveBtn',
|
||||
label: 'Add to Lists',
|
||||
onPress: onPressAddRemoveLists,
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'list.bullet',
|
||||
},
|
||||
})
|
||||
if (!isMe) {
|
||||
if (!profile.viewer?.blocking) {
|
||||
items.push({
|
||||
testID: 'profileHeaderDropdownMuteBtn',
|
||||
label: profile.viewer?.muted
|
||||
? _(msg`Unmute Account`)
|
||||
: _(msg`Mute Account`),
|
||||
onPress: profile.viewer?.muted
|
||||
? onPressUnmuteAccount
|
||||
: onPressMuteAccount,
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'speaker.slash',
|
||||
},
|
||||
android: 'ic_lock_silent_mode',
|
||||
web: 'comment-slash',
|
||||
},
|
||||
})
|
||||
}
|
||||
if (!profile.viewer?.blockingByList) {
|
||||
items.push({
|
||||
testID: 'profileHeaderDropdownBlockBtn',
|
||||
label: profile.viewer?.blocking
|
||||
? _(msg`Unblock Account`)
|
||||
: _(msg`Block Account`),
|
||||
onPress: profile.viewer?.blocking
|
||||
? onPressUnblockAccount
|
||||
: onPressBlockAccount,
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'person.fill.xmark',
|
||||
},
|
||||
android: 'ic_menu_close_clear_cancel',
|
||||
web: 'user-slash',
|
||||
},
|
||||
})
|
||||
}
|
||||
android: 'ic_menu_add',
|
||||
web: 'list',
|
||||
},
|
||||
})
|
||||
if (!isMe) {
|
||||
if (!profile.viewer?.blocking) {
|
||||
items.push({
|
||||
testID: 'profileHeaderDropdownReportBtn',
|
||||
label: _(msg`Report Account`),
|
||||
onPress: onPressReportAccount,
|
||||
testID: 'profileHeaderDropdownMuteBtn',
|
||||
label: profile.viewer?.muted ? 'Unmute Account' : 'Mute Account',
|
||||
onPress: profile.viewer?.muted
|
||||
? onPressUnmuteAccount
|
||||
: onPressMuteAccount,
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'exclamationmark.triangle',
|
||||
name: 'speaker.slash',
|
||||
},
|
||||
android: 'ic_menu_report_image',
|
||||
web: 'circle-exclamation',
|
||||
android: 'ic_lock_silent_mode',
|
||||
web: 'comment-slash',
|
||||
},
|
||||
})
|
||||
}
|
||||
if (!profile.viewer?.blockingByList) {
|
||||
items.push({
|
||||
testID: 'profileHeaderDropdownBlockBtn',
|
||||
label: profile.viewer?.blocking ? 'Unblock Account' : 'Block Account',
|
||||
onPress: profile.viewer?.blocking
|
||||
? onPressUnblockAccount
|
||||
: onPressBlockAccount,
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'person.fill.xmark',
|
||||
},
|
||||
android: 'ic_menu_close_clear_cancel',
|
||||
web: 'user-slash',
|
||||
},
|
||||
})
|
||||
}
|
||||
items.push({
|
||||
testID: 'profileHeaderDropdownReportBtn',
|
||||
label: 'Report Account',
|
||||
onPress: onPressReportAccount,
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'exclamationmark.triangle',
|
||||
},
|
||||
android: 'ic_menu_report_image',
|
||||
web: 'circle-exclamation',
|
||||
},
|
||||
})
|
||||
}
|
||||
return items
|
||||
}, [
|
||||
isMe,
|
||||
hasSession,
|
||||
profile.viewer?.muted,
|
||||
profile.viewer?.blocking,
|
||||
profile.viewer?.blockingByList,
|
||||
@@ -388,7 +373,6 @@ let ProfileHeaderLoaded = ({
|
||||
onPressBlockAccount,
|
||||
onPressReportAccount,
|
||||
onPressAddRemoveLists,
|
||||
_,
|
||||
])
|
||||
|
||||
const blockHide =
|
||||
@@ -430,7 +414,7 @@ let ProfileHeaderLoaded = ({
|
||||
)
|
||||
) : !profile.viewer?.blockedBy ? (
|
||||
<>
|
||||
{!isProfilePreview && hasSession && (
|
||||
{!isProfilePreview && (
|
||||
<TouchableOpacity
|
||||
testID="suggestedFollowsBtn"
|
||||
onPress={() => setShowSuggestedFollows(!showSuggestedFollows)}
|
||||
|
||||
@@ -65,7 +65,7 @@ export function ProfileHeaderSuggestedFollows({
|
||||
}
|
||||
}, [active, animatedHeight, track])
|
||||
|
||||
const {isLoading, data} = useSuggestedFollowsByActorQuery({
|
||||
const {isLoading, data, dataUpdatedAt} = useSuggestedFollowsByActorQuery({
|
||||
did: actorDid,
|
||||
})
|
||||
|
||||
@@ -127,7 +127,11 @@ export function ProfileHeaderSuggestedFollows({
|
||||
</>
|
||||
) : data ? (
|
||||
data.suggestions.map(profile => (
|
||||
<SuggestedFollow key={profile.did} profile={profile} />
|
||||
<SuggestedFollow
|
||||
key={profile.did}
|
||||
profile={profile}
|
||||
dataUpdatedAt={dataUpdatedAt}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<View />
|
||||
@@ -192,13 +196,15 @@ function SuggestedFollowSkeleton() {
|
||||
|
||||
function SuggestedFollow({
|
||||
profile: profileUnshadowed,
|
||||
dataUpdatedAt,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileView
|
||||
dataUpdatedAt: number
|
||||
}) {
|
||||
const {track} = useAnalytics()
|
||||
const pal = usePalette('default')
|
||||
const moderationOpts = useModerationOpts()
|
||||
const profile = useProfileShadow(profileUnshadowed)
|
||||
const profile = useProfileShadow(profileUnshadowed, dataUpdatedAt)
|
||||
const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile)
|
||||
|
||||
const onPressFollow = React.useCallback(async () => {
|
||||
|
||||
@@ -19,7 +19,7 @@ export function AccountDropdownBtn({account}: {account: SessionAccount}) {
|
||||
|
||||
const items: DropdownItem[] = [
|
||||
{
|
||||
label: _(msg`Remove account`),
|
||||
label: 'Remove account',
|
||||
onPress: () => {
|
||||
removeAccount(account)
|
||||
Toast.show('Account removed from quick access')
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, {Component, ErrorInfo, ReactNode} from 'react'
|
||||
import {ErrorScreen} from './error/ErrorScreen'
|
||||
import {CenteredView} from './Views'
|
||||
import {t} from '@lingui/macro'
|
||||
|
||||
interface Props {
|
||||
children?: ReactNode
|
||||
@@ -31,8 +30,8 @@ export class ErrorBoundary extends Component<Props, State> {
|
||||
return (
|
||||
<CenteredView style={{height: '100%', flex: 1}}>
|
||||
<ErrorScreen
|
||||
title={t`Oh no!`}
|
||||
message={t`There was an unexpected issue in the application. Please let us know if this happened to you!`}
|
||||
title="Oh no!"
|
||||
message="There was an unexpected issue in the application. Please let us know if this happened to you!"
|
||||
details={this.state.error.toString()}
|
||||
/>
|
||||
</CenteredView>
|
||||
|
||||
@@ -3,6 +3,7 @@ import {ago} from 'lib/strings/time'
|
||||
import {useTickEveryMinute} from '#/state/shell'
|
||||
|
||||
// FIXME(dan): Figure out why the false positives
|
||||
/* eslint-disable react/prop-types */
|
||||
|
||||
export function TimeElapsed({
|
||||
timestamp,
|
||||
|
||||
@@ -43,13 +43,7 @@ interface PreviewableUserAvatarProps extends BaseUserAvatarProps {
|
||||
|
||||
const BLUR_AMOUNT = isWeb ? 5 : 100
|
||||
|
||||
export function DefaultAvatar({
|
||||
type,
|
||||
size,
|
||||
}: {
|
||||
type: UserAvatarType
|
||||
size: number
|
||||
}) {
|
||||
function DefaultAvatar({type, size}: {type: UserAvatarType; size: number}) {
|
||||
if (type === 'algo') {
|
||||
// Font Awesome Pro 6.4.0 by @fontawesome -https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc.
|
||||
return (
|
||||
@@ -214,7 +208,7 @@ export function EditableUserAvatar({
|
||||
[
|
||||
!isWeb && {
|
||||
testID: 'changeAvatarCameraBtn',
|
||||
label: _(msg`Camera`),
|
||||
label: 'Camera',
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'camera',
|
||||
@@ -238,7 +232,7 @@ export function EditableUserAvatar({
|
||||
},
|
||||
{
|
||||
testID: 'changeAvatarLibraryBtn',
|
||||
label: _(msg`Library`),
|
||||
label: 'Library',
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'photo.on.rectangle.angled',
|
||||
@@ -275,7 +269,7 @@ export function EditableUserAvatar({
|
||||
},
|
||||
!!avatar && {
|
||||
testID: 'changeAvatarRemoveBtn',
|
||||
label: _(msg`Remove`),
|
||||
label: 'Remove',
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'trash',
|
||||
@@ -293,7 +287,6 @@ export function EditableUserAvatar({
|
||||
onSelectNewAvatar,
|
||||
requestCameraAccessIfNeeded,
|
||||
requestPhotoAccessIfNeeded,
|
||||
_,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ export function UserBanner({
|
||||
[
|
||||
!isWeb && {
|
||||
testID: 'changeBannerCameraBtn',
|
||||
label: _(msg`Camera`),
|
||||
label: 'Camera',
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'camera',
|
||||
@@ -57,7 +57,7 @@ export function UserBanner({
|
||||
},
|
||||
{
|
||||
testID: 'changeBannerLibraryBtn',
|
||||
label: _(msg`Library`),
|
||||
label: 'Library',
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'photo.on.rectangle.angled',
|
||||
@@ -86,7 +86,7 @@ export function UserBanner({
|
||||
},
|
||||
!!banner && {
|
||||
testID: 'changeBannerRemoveBtn',
|
||||
label: _(msg`Remove`),
|
||||
label: 'Remove',
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'trash',
|
||||
@@ -104,7 +104,6 @@ export function UserBanner({
|
||||
onSelectNewBanner,
|
||||
requestCameraAccessIfNeeded,
|
||||
requestPhotoAccessIfNeeded,
|
||||
_,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -20,8 +20,6 @@ import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads'
|
||||
import {useLanguagePrefs} from '#/state/preferences'
|
||||
import {logger} from '#/logger'
|
||||
import {Shadow} from '#/state/cache/types'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useSession} from '#/state/session'
|
||||
|
||||
export function PostDropdownBtn({
|
||||
@@ -35,9 +33,8 @@ export function PostDropdownBtn({
|
||||
record: AppBskyFeedPost.Record
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const {hasSession, currentAccount} = useSession()
|
||||
const {currentAccount} = useSession()
|
||||
const theme = useTheme()
|
||||
const {_} = useLingui()
|
||||
const defaultCtrlColor = theme.palette.default.postCtrl
|
||||
const {openModal} = useModalControls()
|
||||
const langPrefs = useLanguagePrefs()
|
||||
@@ -94,7 +91,7 @@ export function PostDropdownBtn({
|
||||
|
||||
const dropdownItems: NativeDropdownItem[] = [
|
||||
{
|
||||
label: _(msg`Translate`),
|
||||
label: 'Translate',
|
||||
onPress() {
|
||||
onOpenTranslate()
|
||||
},
|
||||
@@ -108,7 +105,7 @@ export function PostDropdownBtn({
|
||||
},
|
||||
},
|
||||
{
|
||||
label: _(msg`Copy post text`),
|
||||
label: 'Copy post text',
|
||||
onPress() {
|
||||
onCopyPostText()
|
||||
},
|
||||
@@ -122,7 +119,7 @@ export function PostDropdownBtn({
|
||||
},
|
||||
},
|
||||
{
|
||||
label: _(msg`Share`),
|
||||
label: 'Share',
|
||||
onPress() {
|
||||
const url = toShareUrl(href)
|
||||
shareUrl(url)
|
||||
@@ -136,11 +133,11 @@ export function PostDropdownBtn({
|
||||
web: 'share',
|
||||
},
|
||||
},
|
||||
hasSession && {
|
||||
{
|
||||
label: 'separator',
|
||||
},
|
||||
hasSession && {
|
||||
label: isThreadMuted ? _(msg`Unmute thread`) : _(msg`Mute thread`),
|
||||
{
|
||||
label: isThreadMuted ? 'Unmute thread' : 'Mute thread',
|
||||
onPress() {
|
||||
onToggleThreadMute()
|
||||
},
|
||||
@@ -153,38 +150,37 @@ export function PostDropdownBtn({
|
||||
web: 'comment-slash',
|
||||
},
|
||||
},
|
||||
hasSession && {
|
||||
{
|
||||
label: 'separator',
|
||||
},
|
||||
!isAuthor &&
|
||||
hasSession && {
|
||||
label: _(msg`Report post`),
|
||||
onPress() {
|
||||
openModal({
|
||||
name: 'report',
|
||||
uri: post.uri,
|
||||
cid: post.cid,
|
||||
})
|
||||
},
|
||||
testID: 'postDropdownReportBtn',
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'exclamationmark.triangle',
|
||||
},
|
||||
android: 'ic_menu_report_image',
|
||||
web: 'circle-exclamation',
|
||||
},
|
||||
!isAuthor && {
|
||||
label: 'Report post',
|
||||
onPress() {
|
||||
openModal({
|
||||
name: 'report',
|
||||
uri: post.uri,
|
||||
cid: post.cid,
|
||||
})
|
||||
},
|
||||
testID: 'postDropdownReportBtn',
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'exclamationmark.triangle',
|
||||
},
|
||||
android: 'ic_menu_report_image',
|
||||
web: 'circle-exclamation',
|
||||
},
|
||||
},
|
||||
isAuthor && {
|
||||
label: 'separator',
|
||||
},
|
||||
isAuthor && {
|
||||
label: _(msg`Delete post`),
|
||||
label: 'Delete post',
|
||||
onPress() {
|
||||
openModal({
|
||||
name: 'confirm',
|
||||
title: _(msg`Delete this post?`),
|
||||
message: _(msg`Are you sure? This cannot be undone.`),
|
||||
title: 'Delete this post?',
|
||||
message: 'Are you sure? This can not be undone.',
|
||||
onPressConfirm: onDeletePost,
|
||||
})
|
||||
},
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
} from '#/state/queries/post'
|
||||
import {useComposerControls} from '#/state/shell/composer'
|
||||
import {Shadow} from '#/state/cache/types'
|
||||
import {useRequireAuth} from '#/state/session'
|
||||
|
||||
export function PostCtrls({
|
||||
big,
|
||||
@@ -47,7 +46,6 @@ export function PostCtrls({
|
||||
const postUnlikeMutation = usePostUnlikeMutation()
|
||||
const postRepostMutation = usePostRepostMutation()
|
||||
const postUnrepostMutation = usePostUnrepostMutation()
|
||||
const requireAuth = useRequireAuth()
|
||||
|
||||
const defaultCtrlColor = React.useMemo(
|
||||
() => ({
|
||||
@@ -109,9 +107,7 @@ export function PostCtrls({
|
||||
<TouchableOpacity
|
||||
testID="replyBtn"
|
||||
style={[styles.ctrl, !big && styles.ctrlPad, {paddingLeft: 0}]}
|
||||
onPress={() => {
|
||||
requireAuth(() => onPressReply())
|
||||
}}
|
||||
onPress={onPressReply}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`Reply (${post.replyCount} ${
|
||||
post.replyCount === 1 ? 'reply' : 'replies'
|
||||
@@ -139,9 +135,7 @@ export function PostCtrls({
|
||||
<TouchableOpacity
|
||||
testID="likeBtn"
|
||||
style={[styles.ctrl, !big && styles.ctrlPad]}
|
||||
onPress={() => {
|
||||
requireAuth(() => onPressToggleLike())
|
||||
}}
|
||||
onPress={onPressToggleLike}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`${post.viewer?.like ? 'Unlike' : 'Like'} (${
|
||||
post.likeCount
|
||||
|
||||
@@ -7,7 +7,6 @@ import {Text} from '../text/Text'
|
||||
import {pluralize} from 'lib/strings/helpers'
|
||||
import {HITSLOP_10, HITSLOP_20} from 'lib/constants'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {useRequireAuth} from '#/state/session'
|
||||
|
||||
interface Props {
|
||||
isReposted: boolean
|
||||
@@ -26,7 +25,6 @@ export const RepostButton = ({
|
||||
}: Props) => {
|
||||
const theme = useTheme()
|
||||
const {openModal} = useModalControls()
|
||||
const requireAuth = useRequireAuth()
|
||||
|
||||
const defaultControlColor = React.useMemo(
|
||||
() => ({
|
||||
@@ -47,9 +45,7 @@ export const RepostButton = ({
|
||||
return (
|
||||
<TouchableOpacity
|
||||
testID="repostBtn"
|
||||
onPress={() => {
|
||||
requireAuth(() => onPressToggleRepostWrapper())
|
||||
}}
|
||||
onPress={onPressToggleRepostWrapper}
|
||||
style={[styles.control, !big && styles.controlPad]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`${
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import {StyleProp, StyleSheet, View, ViewStyle, Pressable} from 'react-native'
|
||||
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
|
||||
import {RepostIcon} from 'lib/icons'
|
||||
import {colors} from 'lib/styles'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
@@ -12,8 +12,6 @@ import {
|
||||
import {EventStopper} from '../EventStopper'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useRequireAuth} from '#/state/session'
|
||||
import {useSession} from '#/state/session'
|
||||
|
||||
interface Props {
|
||||
isReposted: boolean
|
||||
@@ -33,8 +31,6 @@ export const RepostButton = ({
|
||||
}: Props) => {
|
||||
const theme = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {hasSession} = useSession()
|
||||
const requireAuth = useRequireAuth()
|
||||
|
||||
const defaultControlColor = React.useMemo(
|
||||
() => ({
|
||||
@@ -45,7 +41,7 @@ export const RepostButton = ({
|
||||
|
||||
const dropdownItems: NativeDropdownItem[] = [
|
||||
{
|
||||
label: isReposted ? _(msg`Undo repost`) : _(msg`Repost`),
|
||||
label: isReposted ? 'Undo repost' : 'Repost',
|
||||
testID: 'repostDropdownRepostBtn',
|
||||
icon: {
|
||||
ios: {name: 'repeat'},
|
||||
@@ -55,7 +51,7 @@ export const RepostButton = ({
|
||||
onPress: onRepost,
|
||||
},
|
||||
{
|
||||
label: _(msg`Quote post`),
|
||||
label: 'Quote post',
|
||||
testID: 'repostDropdownQuoteBtn',
|
||||
icon: {
|
||||
ios: {name: 'quote.bubble'},
|
||||
@@ -66,46 +62,32 @@ export const RepostButton = ({
|
||||
},
|
||||
]
|
||||
|
||||
const inner = (
|
||||
<View
|
||||
style={[
|
||||
styles.control,
|
||||
!big && styles.controlPad,
|
||||
(isReposted
|
||||
? styles.reposted
|
||||
: defaultControlColor) as StyleProp<ViewStyle>,
|
||||
]}>
|
||||
<RepostIcon strokeWidth={2.2} size={big ? 24 : 20} />
|
||||
{typeof repostCount !== 'undefined' ? (
|
||||
<Text
|
||||
testID="repostCount"
|
||||
type={isReposted ? 'md-bold' : 'md'}
|
||||
style={styles.repostCount}>
|
||||
{repostCount ?? 0}
|
||||
</Text>
|
||||
) : undefined}
|
||||
</View>
|
||||
)
|
||||
|
||||
return hasSession ? (
|
||||
return (
|
||||
<EventStopper>
|
||||
<NativeDropdown
|
||||
items={dropdownItems}
|
||||
accessibilityLabel={_(msg`Repost or quote post`)}
|
||||
accessibilityHint="">
|
||||
{inner}
|
||||
<View
|
||||
style={[
|
||||
styles.control,
|
||||
!big && styles.controlPad,
|
||||
(isReposted
|
||||
? styles.reposted
|
||||
: defaultControlColor) as StyleProp<ViewStyle>,
|
||||
]}>
|
||||
<RepostIcon strokeWidth={2.2} size={big ? 24 : 20} />
|
||||
{typeof repostCount !== 'undefined' ? (
|
||||
<Text
|
||||
testID="repostCount"
|
||||
type={isReposted ? 'md-bold' : 'md'}
|
||||
style={styles.repostCount}>
|
||||
{repostCount ?? 0}
|
||||
</Text>
|
||||
) : undefined}
|
||||
</View>
|
||||
</NativeDropdown>
|
||||
</EventStopper>
|
||||
) : (
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
onPress={() => {
|
||||
requireAuth(() => {})
|
||||
}}
|
||||
accessibilityLabel={_(msg`Repost or quote post`)}
|
||||
accessibilityHint="">
|
||||
{inner}
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+142
-144
@@ -12,6 +12,7 @@ import {Button} from '../com/util/forms/Button'
|
||||
import * as Toast from '../com/util/Toast'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
|
||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
import {CommonNavigatorParams} from 'lib/routes/types'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
@@ -31,111 +32,125 @@ import {ErrorScreen} from '../com/util/error/ErrorScreen'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppPasswords'>
|
||||
export function AppPasswords({}: Props) {
|
||||
const pal = usePalette('default')
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {screen} = useAnalytics()
|
||||
const {isTabletOrDesktop} = useWebMediaQueries()
|
||||
const {openModal} = useModalControls()
|
||||
const {data: appPasswords, error} = useAppPasswordsQuery()
|
||||
export const AppPasswords = withAuthRequired(
|
||||
function AppPasswordsImpl({}: Props) {
|
||||
const pal = usePalette('default')
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {screen} = useAnalytics()
|
||||
const {isTabletOrDesktop} = useWebMediaQueries()
|
||||
const {openModal} = useModalControls()
|
||||
const {data: appPasswords, error} = useAppPasswordsQuery()
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
screen('AppPasswords')
|
||||
setMinimalShellMode(false)
|
||||
}, [screen, setMinimalShellMode]),
|
||||
)
|
||||
|
||||
const onAdd = React.useCallback(async () => {
|
||||
openModal({name: 'add-app-password'})
|
||||
}, [openModal])
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<CenteredView
|
||||
style={[
|
||||
styles.container,
|
||||
isTabletOrDesktop && styles.containerDesktop,
|
||||
pal.view,
|
||||
pal.border,
|
||||
]}
|
||||
testID="appPasswordsScreen">
|
||||
<ErrorScreen
|
||||
title="Oops!"
|
||||
message="There was an issue with fetching your app passwords"
|
||||
details={cleanError(error)}
|
||||
/>
|
||||
</CenteredView>
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
screen('AppPasswords')
|
||||
setMinimalShellMode(false)
|
||||
}, [screen, setMinimalShellMode]),
|
||||
)
|
||||
}
|
||||
|
||||
// no app passwords (empty) state
|
||||
if (appPasswords?.length === 0) {
|
||||
return (
|
||||
<CenteredView
|
||||
style={[
|
||||
styles.container,
|
||||
isTabletOrDesktop && styles.containerDesktop,
|
||||
pal.view,
|
||||
pal.border,
|
||||
]}
|
||||
testID="appPasswordsScreen">
|
||||
<AppPasswordsHeader />
|
||||
<View style={[styles.empty, pal.viewLight]}>
|
||||
<Text type="lg" style={[pal.text, styles.emptyText]}>
|
||||
<Trans>
|
||||
You have not created any app passwords yet. You can create one by
|
||||
pressing the button below.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
{!isTabletOrDesktop && <View style={styles.flex1} />}
|
||||
<View
|
||||
style={[
|
||||
styles.btnContainer,
|
||||
isTabletOrDesktop && styles.btnContainerDesktop,
|
||||
]}>
|
||||
<Button
|
||||
testID="appPasswordBtn"
|
||||
type="primary"
|
||||
label="Add App Password"
|
||||
style={styles.btn}
|
||||
labelStyle={styles.btnLabel}
|
||||
onPress={onAdd}
|
||||
/>
|
||||
</View>
|
||||
</CenteredView>
|
||||
)
|
||||
}
|
||||
const onAdd = React.useCallback(async () => {
|
||||
openModal({name: 'add-app-password'})
|
||||
}, [openModal])
|
||||
|
||||
if (appPasswords?.length) {
|
||||
// has app passwords
|
||||
return (
|
||||
<CenteredView
|
||||
style={[
|
||||
styles.container,
|
||||
isTabletOrDesktop && styles.containerDesktop,
|
||||
pal.view,
|
||||
pal.border,
|
||||
]}
|
||||
testID="appPasswordsScreen">
|
||||
<AppPasswordsHeader />
|
||||
<ScrollView
|
||||
if (error) {
|
||||
return (
|
||||
<CenteredView
|
||||
style={[
|
||||
styles.scrollContainer,
|
||||
styles.container,
|
||||
isTabletOrDesktop && styles.containerDesktop,
|
||||
pal.view,
|
||||
pal.border,
|
||||
!isTabletOrDesktop && styles.flex1,
|
||||
]}>
|
||||
{appPasswords.map((password, i) => (
|
||||
<AppPassword
|
||||
key={password.name}
|
||||
testID={`appPassword-${i}`}
|
||||
name={password.name}
|
||||
createdAt={password.createdAt}
|
||||
]}
|
||||
testID="appPasswordsScreen">
|
||||
<ErrorScreen
|
||||
title="Oops!"
|
||||
message="There was an issue with fetching your app passwords"
|
||||
details={cleanError(error)}
|
||||
/>
|
||||
</CenteredView>
|
||||
)
|
||||
}
|
||||
|
||||
// no app passwords (empty) state
|
||||
if (appPasswords?.length === 0) {
|
||||
return (
|
||||
<CenteredView
|
||||
style={[
|
||||
styles.container,
|
||||
isTabletOrDesktop && styles.containerDesktop,
|
||||
pal.view,
|
||||
pal.border,
|
||||
]}
|
||||
testID="appPasswordsScreen">
|
||||
<AppPasswordsHeader />
|
||||
<View style={[styles.empty, pal.viewLight]}>
|
||||
<Text type="lg" style={[pal.text, styles.emptyText]}>
|
||||
<Trans>
|
||||
You have not created any app passwords yet. You can create one
|
||||
by pressing the button below.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
{!isTabletOrDesktop && <View style={styles.flex1} />}
|
||||
<View
|
||||
style={[
|
||||
styles.btnContainer,
|
||||
isTabletOrDesktop && styles.btnContainerDesktop,
|
||||
]}>
|
||||
<Button
|
||||
testID="appPasswordBtn"
|
||||
type="primary"
|
||||
label="Add App Password"
|
||||
style={styles.btn}
|
||||
labelStyle={styles.btnLabel}
|
||||
onPress={onAdd}
|
||||
/>
|
||||
))}
|
||||
{isTabletOrDesktop && (
|
||||
<View style={[styles.btnContainer, styles.btnContainerDesktop]}>
|
||||
</View>
|
||||
</CenteredView>
|
||||
)
|
||||
}
|
||||
|
||||
if (appPasswords?.length) {
|
||||
// has app passwords
|
||||
return (
|
||||
<CenteredView
|
||||
style={[
|
||||
styles.container,
|
||||
isTabletOrDesktop && styles.containerDesktop,
|
||||
pal.view,
|
||||
pal.border,
|
||||
]}
|
||||
testID="appPasswordsScreen">
|
||||
<AppPasswordsHeader />
|
||||
<ScrollView
|
||||
style={[
|
||||
styles.scrollContainer,
|
||||
pal.border,
|
||||
!isTabletOrDesktop && styles.flex1,
|
||||
]}>
|
||||
{appPasswords.map((password, i) => (
|
||||
<AppPassword
|
||||
key={password.name}
|
||||
testID={`appPassword-${i}`}
|
||||
name={password.name}
|
||||
createdAt={password.createdAt}
|
||||
/>
|
||||
))}
|
||||
{isTabletOrDesktop && (
|
||||
<View style={[styles.btnContainer, styles.btnContainerDesktop]}>
|
||||
<Button
|
||||
testID="appPasswordBtn"
|
||||
type="primary"
|
||||
label="Add App Password"
|
||||
style={styles.btn}
|
||||
labelStyle={styles.btnLabel}
|
||||
onPress={onAdd}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
{!isTabletOrDesktop && (
|
||||
<View style={styles.btnContainer}>
|
||||
<Button
|
||||
testID="appPasswordBtn"
|
||||
type="primary"
|
||||
@@ -146,44 +161,31 @@ export function AppPasswords({}: Props) {
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
{!isTabletOrDesktop && (
|
||||
<View style={styles.btnContainer}>
|
||||
<Button
|
||||
testID="appPasswordBtn"
|
||||
type="primary"
|
||||
label="Add App Password"
|
||||
style={styles.btn}
|
||||
labelStyle={styles.btnLabel}
|
||||
onPress={onAdd}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</CenteredView>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<CenteredView
|
||||
style={[
|
||||
styles.container,
|
||||
isTabletOrDesktop && styles.containerDesktop,
|
||||
pal.view,
|
||||
pal.border,
|
||||
]}
|
||||
testID="appPasswordsScreen">
|
||||
<ActivityIndicator />
|
||||
</CenteredView>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<CenteredView
|
||||
style={[
|
||||
styles.container,
|
||||
isTabletOrDesktop && styles.containerDesktop,
|
||||
pal.view,
|
||||
pal.border,
|
||||
]}
|
||||
testID="appPasswordsScreen">
|
||||
<ActivityIndicator />
|
||||
</CenteredView>
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
function AppPasswordsHeader() {
|
||||
const {isTabletOrDesktop} = useWebMediaQueries()
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
return (
|
||||
<>
|
||||
<ViewHeader title={_(msg`App Passwords`)} showOnDesktop />
|
||||
<ViewHeader title="App Passwords" showOnDesktop />
|
||||
<Text
|
||||
type="sm"
|
||||
style={[
|
||||
@@ -218,16 +220,14 @@ function AppPassword({
|
||||
const onDelete = React.useCallback(async () => {
|
||||
openModal({
|
||||
name: 'confirm',
|
||||
title: _(msg`Delete app password`),
|
||||
message: _(
|
||||
msg`Are you sure you want to delete the app password "${name}"?`,
|
||||
),
|
||||
title: 'Delete App Password',
|
||||
message: `Are you sure you want to delete the app password "${name}"?`,
|
||||
async onPressConfirm() {
|
||||
await deleteMutation.mutateAsync({name})
|
||||
Toast.show('App password deleted')
|
||||
},
|
||||
})
|
||||
}, [deleteMutation, openModal, name, _])
|
||||
}, [deleteMutation, openModal, name])
|
||||
|
||||
const primaryLocale =
|
||||
contentLanguages.length > 0 ? contentLanguages[0] : 'en-US'
|
||||
@@ -245,17 +245,15 @@ function AppPassword({
|
||||
{name}
|
||||
</Text>
|
||||
<Text type="md" style={[pal.text, styles.pr10]} numberOfLines={1}>
|
||||
<Trans>
|
||||
Created{' '}
|
||||
{Intl.DateTimeFormat(primaryLocale, {
|
||||
year: 'numeric',
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
}).format(new Date(createdAt))}
|
||||
</Trans>
|
||||
Created{' '}
|
||||
{Intl.DateTimeFormat(primaryLocale, {
|
||||
year: 'numeric',
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
}).format(new Date(createdAt))}
|
||||
</Text>
|
||||
</View>
|
||||
<FontAwesomeIcon icon={['far', 'trash-can']} style={styles.trashIcon} />
|
||||
|
||||
@@ -9,8 +9,6 @@ import {ScrollView} from 'view/com/util/Views'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {s} from 'lib/styles'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
type Props = NativeStackScreenProps<
|
||||
CommonNavigatorParams,
|
||||
@@ -18,7 +16,6 @@ type Props = NativeStackScreenProps<
|
||||
>
|
||||
export const CommunityGuidelinesScreen = (_props: Props) => {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
|
||||
useFocusEffect(
|
||||
@@ -29,18 +26,16 @@ export const CommunityGuidelinesScreen = (_props: Props) => {
|
||||
|
||||
return (
|
||||
<View>
|
||||
<ViewHeader title={_(msg`Community Guidelines`)} />
|
||||
<ViewHeader title="Community Guidelines" />
|
||||
<ScrollView style={[s.hContentRegion, pal.view]}>
|
||||
<View style={[s.p20]}>
|
||||
<Text style={pal.text}>
|
||||
<Trans>
|
||||
The Community Guidelines have been moved to{' '}
|
||||
<TextLink
|
||||
style={pal.link}
|
||||
href="https://blueskyweb.xyz/support/community-guidelines"
|
||||
text="blueskyweb.xyz/support/community-guidelines"
|
||||
/>
|
||||
</Trans>
|
||||
The Community Guidelines have been moved to{' '}
|
||||
<TextLink
|
||||
style={pal.link}
|
||||
href="https://blueskyweb.xyz/support/community-guidelines"
|
||||
text="blueskyweb.xyz/support/community-guidelines"
|
||||
/>
|
||||
</Text>
|
||||
</View>
|
||||
<View style={s.footerSpacer} />
|
||||
|
||||
@@ -9,13 +9,10 @@ import {ScrollView} from 'view/com/util/Views'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {s} from 'lib/styles'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'CopyrightPolicy'>
|
||||
export const CopyrightPolicyScreen = (_props: Props) => {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
|
||||
useFocusEffect(
|
||||
@@ -26,18 +23,16 @@ export const CopyrightPolicyScreen = (_props: Props) => {
|
||||
|
||||
return (
|
||||
<View>
|
||||
<ViewHeader title={_(msg`Copyright Policy`)} />
|
||||
<ViewHeader title="Copyright Policy" />
|
||||
<ScrollView style={[s.hContentRegion, pal.view]}>
|
||||
<View style={[s.p20]}>
|
||||
<Text style={pal.text}>
|
||||
<Trans>
|
||||
The Copyright Policy has been moved to{' '}
|
||||
<TextLink
|
||||
style={pal.link}
|
||||
href="https://blueskyweb.xyz/support/community-guidelines"
|
||||
text="blueskyweb.xyz/support/community-guidelines"
|
||||
/>
|
||||
</Trans>
|
||||
The Copyright Policy has been moved to{' '}
|
||||
<TextLink
|
||||
style={pal.link}
|
||||
href="https://blueskyweb.xyz/support/community-guidelines"
|
||||
text="blueskyweb.xyz/support/community-guidelines"
|
||||
/>
|
||||
</Text>
|
||||
</View>
|
||||
<View style={s.footerSpacer} />
|
||||
|
||||
+48
-54
@@ -2,6 +2,7 @@ import React from 'react'
|
||||
import {ActivityIndicator, StyleSheet, View, RefreshControl} from 'react-native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
|
||||
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
|
||||
import {ViewHeader} from 'view/com/util/ViewHeader'
|
||||
import {FAB} from 'view/com/util/fab/FAB'
|
||||
import {Link} from 'view/com/util/Link'
|
||||
@@ -33,7 +34,6 @@ import {
|
||||
} from '#/state/queries/feed'
|
||||
import {cleanError} from 'lib/strings/errors'
|
||||
import {useComposerControls} from '#/state/shell/composer'
|
||||
import {useSession} from '#/state/session'
|
||||
|
||||
type Props = NativeStackScreenProps<FeedsTabNavigatorParams, 'Feeds'>
|
||||
|
||||
@@ -87,7 +87,9 @@ type FlatlistSlice =
|
||||
key: string
|
||||
}
|
||||
|
||||
export function FeedsScreen(_props: Props) {
|
||||
export const FeedsScreen = withAuthRequired(function FeedsScreenImpl(
|
||||
_props: Props,
|
||||
) {
|
||||
const pal = usePalette('default')
|
||||
const {openComposer} = useComposerControls()
|
||||
const {isMobile, isTabletOrDesktop} = useWebMediaQueries()
|
||||
@@ -116,7 +118,6 @@ export function FeedsScreen(_props: Props) {
|
||||
isPending: isSearchPending,
|
||||
error: searchError,
|
||||
} = useSearchPopularFeedsMutation()
|
||||
const {hasSession} = useSession()
|
||||
|
||||
/**
|
||||
* A search query is present. We may not have search results yet.
|
||||
@@ -180,52 +181,50 @@ export function FeedsScreen(_props: Props) {
|
||||
const items = React.useMemo(() => {
|
||||
let slices: FlatlistSlice[] = []
|
||||
|
||||
if (hasSession) {
|
||||
slices.push({
|
||||
key: 'savedFeedsHeader',
|
||||
type: 'savedFeedsHeader',
|
||||
})
|
||||
slices.push({
|
||||
key: 'savedFeedsHeader',
|
||||
type: 'savedFeedsHeader',
|
||||
})
|
||||
|
||||
if (preferencesError) {
|
||||
if (preferencesError) {
|
||||
slices.push({
|
||||
key: 'savedFeedsError',
|
||||
type: 'error',
|
||||
error: cleanError(preferencesError.toString()),
|
||||
})
|
||||
} else {
|
||||
if (isPreferencesLoading || !preferences?.feeds?.saved) {
|
||||
slices.push({
|
||||
key: 'savedFeedsError',
|
||||
type: 'error',
|
||||
error: cleanError(preferencesError.toString()),
|
||||
key: 'savedFeedsLoading',
|
||||
type: 'savedFeedsLoading',
|
||||
// pendingItems: this.rootStore.preferences.savedFeeds.length || 3,
|
||||
})
|
||||
} else {
|
||||
if (isPreferencesLoading || !preferences?.feeds?.saved) {
|
||||
if (preferences?.feeds?.saved.length === 0) {
|
||||
slices.push({
|
||||
key: 'savedFeedsLoading',
|
||||
type: 'savedFeedsLoading',
|
||||
// pendingItems: this.rootStore.preferences.savedFeeds.length || 3,
|
||||
key: 'savedFeedNoResults',
|
||||
type: 'savedFeedNoResults',
|
||||
})
|
||||
} else {
|
||||
if (preferences?.feeds?.saved.length === 0) {
|
||||
slices.push({
|
||||
key: 'savedFeedNoResults',
|
||||
type: 'savedFeedNoResults',
|
||||
})
|
||||
} else {
|
||||
const {saved, pinned} = preferences.feeds
|
||||
const {saved, pinned} = preferences.feeds
|
||||
|
||||
slices = slices.concat(
|
||||
pinned.map(uri => ({
|
||||
slices = slices.concat(
|
||||
pinned.map(uri => ({
|
||||
key: `savedFeed:${uri}`,
|
||||
type: 'savedFeed',
|
||||
feedUri: uri,
|
||||
})),
|
||||
)
|
||||
|
||||
slices = slices.concat(
|
||||
saved
|
||||
.filter(uri => !pinned.includes(uri))
|
||||
.map(uri => ({
|
||||
key: `savedFeed:${uri}`,
|
||||
type: 'savedFeed',
|
||||
feedUri: uri,
|
||||
})),
|
||||
)
|
||||
|
||||
slices = slices.concat(
|
||||
saved
|
||||
.filter(uri => !pinned.includes(uri))
|
||||
.map(uri => ({
|
||||
key: `savedFeed:${uri}`,
|
||||
type: 'savedFeed',
|
||||
feedUri: uri,
|
||||
})),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -307,7 +306,6 @@ export function FeedsScreen(_props: Props) {
|
||||
|
||||
return slices
|
||||
}, [
|
||||
hasSession,
|
||||
preferences,
|
||||
isPreferencesLoading,
|
||||
preferencesError,
|
||||
@@ -394,8 +392,7 @@ export function FeedsScreen(_props: Props) {
|
||||
pal.view,
|
||||
styles.header,
|
||||
{
|
||||
// This is first in the flatlist without a session -esb
|
||||
marginTop: hasSession ? 16 : 0,
|
||||
marginTop: 16,
|
||||
paddingLeft: isMobile ? 12 : undefined,
|
||||
paddingRight: 10,
|
||||
paddingBottom: isMobile ? 6 : undefined,
|
||||
@@ -434,7 +431,7 @@ export function FeedsScreen(_props: Props) {
|
||||
return (
|
||||
<FeedSourceCard
|
||||
feedUri={item.feedUri}
|
||||
showSaveBtn={hasSession}
|
||||
showSaveBtn
|
||||
showDescription
|
||||
showLikes
|
||||
/>
|
||||
@@ -457,7 +454,6 @@ export function FeedsScreen(_props: Props) {
|
||||
},
|
||||
[
|
||||
_,
|
||||
hasSession,
|
||||
isMobile,
|
||||
pal,
|
||||
query,
|
||||
@@ -471,7 +467,7 @@ export function FeedsScreen(_props: Props) {
|
||||
<View style={[pal.view, styles.container]}>
|
||||
{isMobile && (
|
||||
<ViewHeader
|
||||
title={_(msg`Feeds`)}
|
||||
title="Feeds"
|
||||
canGoBack={false}
|
||||
renderButton={renderHeaderBtn}
|
||||
showBorder
|
||||
@@ -500,19 +496,17 @@ export function FeedsScreen(_props: Props) {
|
||||
desktopFixedHeight
|
||||
/>
|
||||
|
||||
{hasSession && (
|
||||
<FAB
|
||||
testID="composeFAB"
|
||||
onPress={onPressCompose}
|
||||
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`New post`)}
|
||||
accessibilityHint=""
|
||||
/>
|
||||
)}
|
||||
<FAB
|
||||
testID="composeFAB"
|
||||
onPress={onPressCompose}
|
||||
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`New post`)}
|
||||
accessibilityHint=""
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
function SavedFeed({feedUri}: {feedUri: string}) {
|
||||
const pal = usePalette('default')
|
||||
|
||||
@@ -3,6 +3,7 @@ import {View, ActivityIndicator, StyleSheet} from 'react-native'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
import {NativeStackScreenProps, HomeTabNavigatorParams} from 'lib/routes/types'
|
||||
import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed'
|
||||
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
|
||||
import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState'
|
||||
import {FollowingEndOfFeed} from 'view/com/posts/FollowingEndOfFeed'
|
||||
import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState'
|
||||
@@ -13,17 +14,12 @@ import {useSetMinimalShellMode, useSetDrawerSwipeDisabled} from '#/state/shell'
|
||||
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||
import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
|
||||
import {emitSoftReset} from '#/state/events'
|
||||
import {useSession} from '#/state/session'
|
||||
|
||||
type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
|
||||
export function HomeScreen(props: Props) {
|
||||
const {hasSession} = useSession()
|
||||
export const HomeScreen = withAuthRequired(function HomeScreenImpl(
|
||||
props: Props,
|
||||
) {
|
||||
const {data: preferences} = usePreferencesQuery()
|
||||
|
||||
if (!hasSession) {
|
||||
return <HomeScreenPublic />
|
||||
}
|
||||
|
||||
if (preferences) {
|
||||
return <HomeScreenReady {...props} preferences={preferences} />
|
||||
} else {
|
||||
@@ -33,31 +29,7 @@ export function HomeScreen(props: Props) {
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function HomeScreenPublic() {
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
|
||||
|
||||
const renderCustomFeedEmptyState = React.useCallback(() => {
|
||||
return <CustomFeedEmptyState />
|
||||
}, [])
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
setMinimalShellMode(false)
|
||||
setDrawerSwipeDisabled(false)
|
||||
}, [setDrawerSwipeDisabled, setMinimalShellMode]),
|
||||
)
|
||||
|
||||
return (
|
||||
<FeedPage
|
||||
isPageFocused
|
||||
feed={`feedgen|at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot`}
|
||||
renderEmptyState={renderCustomFeedEmptyState}
|
||||
/>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
function HomeScreenReady({
|
||||
preferences,
|
||||
@@ -111,7 +83,6 @@ function HomeScreenReady({
|
||||
emitSoftReset()
|
||||
}, [])
|
||||
|
||||
// TODO(pwi) may need this in public view
|
||||
const onPageScrollStateChanged = React.useCallback(
|
||||
(state: 'idle' | 'dragging' | 'settling') => {
|
||||
if (state === 'dragging') {
|
||||
|
||||
@@ -14,19 +14,16 @@ import {
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
import {APP_LANGUAGES, LANGUAGES} from 'lib/../locale/languages'
|
||||
import {LANGUAGES} from 'lib/../locale/languages'
|
||||
import RNPickerSelect, {PickerSelectProps} from 'react-native-picker-select'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'LanguageSettings'>
|
||||
|
||||
export function LanguageSettingsScreen(_props: Props) {
|
||||
export function LanguageSettingsScreen(_: Props) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const langPrefs = useLanguagePrefs()
|
||||
const setLangPrefs = useLanguagePrefsApi()
|
||||
const {isTabletOrDesktop} = useWebMediaQueries()
|
||||
@@ -55,15 +52,6 @@ export function LanguageSettingsScreen(_props: Props) {
|
||||
[langPrefs, setLangPrefs],
|
||||
)
|
||||
|
||||
const onChangeAppLanguage = React.useCallback(
|
||||
(value: Parameters<PickerSelectProps['onValueChange']>[0]) => {
|
||||
if (langPrefs.appLanguage !== value) {
|
||||
setLangPrefs.setAppLanguage(value)
|
||||
}
|
||||
},
|
||||
[langPrefs, setLangPrefs],
|
||||
)
|
||||
|
||||
const myLanguages = React.useMemo(() => {
|
||||
return (
|
||||
langPrefs.contentLanguages
|
||||
@@ -83,109 +71,15 @@ export function LanguageSettingsScreen(_props: Props) {
|
||||
styles.container,
|
||||
isTabletOrDesktop && styles.desktopContainer,
|
||||
]}>
|
||||
<ViewHeader title={_(msg`Language Settings`)} showOnDesktop />
|
||||
<ViewHeader title="Language Settings" showOnDesktop />
|
||||
|
||||
<View style={{paddingTop: 20, paddingHorizontal: 20}}>
|
||||
{/* APP LANGUAGE */}
|
||||
<View style={{paddingBottom: 20}}>
|
||||
<Text type="title-sm" style={[pal.text, s.pb5]}>
|
||||
<Trans>App Language</Trans>
|
||||
Primary Language
|
||||
</Text>
|
||||
<Text style={[pal.text, s.pb10]}>
|
||||
<Trans>
|
||||
Select your app language for the default text to display in the
|
||||
app
|
||||
</Trans>
|
||||
</Text>
|
||||
|
||||
<View style={{position: 'relative'}}>
|
||||
<RNPickerSelect
|
||||
value={langPrefs.appLanguage}
|
||||
onValueChange={onChangeAppLanguage}
|
||||
items={APP_LANGUAGES.filter(l => Boolean(l.code2)).map(l => ({
|
||||
label: l.name,
|
||||
value: l.code2,
|
||||
key: l.code2,
|
||||
}))}
|
||||
style={{
|
||||
inputAndroid: {
|
||||
backgroundColor: pal.viewLight.backgroundColor,
|
||||
color: pal.text.color,
|
||||
fontSize: 14,
|
||||
letterSpacing: 0.5,
|
||||
fontWeight: '500',
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 24,
|
||||
},
|
||||
inputIOS: {
|
||||
backgroundColor: pal.viewLight.backgroundColor,
|
||||
color: pal.text.color,
|
||||
fontSize: 14,
|
||||
letterSpacing: 0.5,
|
||||
fontWeight: '500',
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 24,
|
||||
},
|
||||
inputWeb: {
|
||||
// @ts-ignore web only
|
||||
cursor: 'pointer',
|
||||
'-moz-appearance': 'none',
|
||||
'-webkit-appearance': 'none',
|
||||
appearance: 'none',
|
||||
outline: 0,
|
||||
borderWidth: 0,
|
||||
backgroundColor: pal.viewLight.backgroundColor,
|
||||
color: pal.text.color,
|
||||
fontSize: 14,
|
||||
letterSpacing: 0.5,
|
||||
fontWeight: '500',
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 24,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 1,
|
||||
right: 1,
|
||||
bottom: 1,
|
||||
width: 40,
|
||||
backgroundColor: pal.viewLight.backgroundColor,
|
||||
borderRadius: 24,
|
||||
pointerEvents: 'none',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}>
|
||||
<FontAwesomeIcon
|
||||
icon="chevron-down"
|
||||
style={pal.text as FontAwesomeIconStyle}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
height: 1,
|
||||
backgroundColor: pal.border.borderColor,
|
||||
marginBottom: 20,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* PRIMARY LANGUAGE */}
|
||||
<View style={{paddingBottom: 20}}>
|
||||
<Text type="title-sm" style={[pal.text, s.pb5]}>
|
||||
<Trans>Primary Language</Trans>
|
||||
</Text>
|
||||
<Text style={[pal.text, s.pb10]}>
|
||||
<Trans>
|
||||
Select your preferred language for translations in your feed.
|
||||
</Trans>
|
||||
Select your preferred language for translations in your feed.
|
||||
</Text>
|
||||
|
||||
<View style={{position: 'relative'}}>
|
||||
@@ -267,16 +161,13 @@ export function LanguageSettingsScreen(_props: Props) {
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* CONTENT LANGUAGES */}
|
||||
<View style={{paddingBottom: 20}}>
|
||||
<Text type="title-sm" style={[pal.text, s.pb5]}>
|
||||
<Trans>Content Languages</Trans>
|
||||
Content Languages
|
||||
</Text>
|
||||
<Text style={[pal.text, s.pb10]}>
|
||||
<Trans>
|
||||
Select which languages you want your subscribed feeds to include.
|
||||
If none are selected, all languages will be shown.
|
||||
</Trans>
|
||||
Select which languages you want your subscribed feeds to include. If
|
||||
none are selected, all languages will be shown.
|
||||
</Text>
|
||||
|
||||
<Button
|
||||
|
||||
+64
-62
@@ -4,6 +4,7 @@ import {useFocusEffect, useNavigation} from '@react-navigation/native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {AtUri} from '@atproto/api'
|
||||
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
|
||||
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
|
||||
import {MyLists} from '#/view/com/lists/MyLists'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {Button} from 'view/com/util/forms/Button'
|
||||
@@ -14,71 +15,72 @@ import {SimpleViewHeader} from 'view/com/util/SimpleViewHeader'
|
||||
import {s} from 'lib/styles'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {Trans} from '@lingui/macro'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Lists'>
|
||||
export function ListsScreen({}: Props) {
|
||||
const pal = usePalette('default')
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {openModal} = useModalControls()
|
||||
export const ListsScreen = withAuthRequired(
|
||||
function ListsScreenImpl({}: Props) {
|
||||
const pal = usePalette('default')
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {openModal} = useModalControls()
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
setMinimalShellMode(false)
|
||||
}, [setMinimalShellMode]),
|
||||
)
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
setMinimalShellMode(false)
|
||||
}, [setMinimalShellMode]),
|
||||
)
|
||||
|
||||
const onPressNewList = React.useCallback(() => {
|
||||
openModal({
|
||||
name: 'create-or-edit-list',
|
||||
purpose: 'app.bsky.graph.defs#curatelist',
|
||||
onSave: (uri: string) => {
|
||||
try {
|
||||
const urip = new AtUri(uri)
|
||||
navigation.navigate('ProfileList', {
|
||||
name: urip.hostname,
|
||||
rkey: urip.rkey,
|
||||
})
|
||||
} catch {}
|
||||
},
|
||||
})
|
||||
}, [openModal, navigation])
|
||||
const onPressNewList = React.useCallback(() => {
|
||||
openModal({
|
||||
name: 'create-or-edit-list',
|
||||
purpose: 'app.bsky.graph.defs#curatelist',
|
||||
onSave: (uri: string) => {
|
||||
try {
|
||||
const urip = new AtUri(uri)
|
||||
navigation.navigate('ProfileList', {
|
||||
name: urip.hostname,
|
||||
rkey: urip.rkey,
|
||||
})
|
||||
} catch {}
|
||||
},
|
||||
})
|
||||
}, [openModal, navigation])
|
||||
|
||||
return (
|
||||
<View style={s.hContentRegion} testID="listsScreen">
|
||||
<SimpleViewHeader
|
||||
showBackButton={isMobile}
|
||||
style={
|
||||
!isMobile && [pal.border, {borderLeftWidth: 1, borderRightWidth: 1}]
|
||||
}>
|
||||
<View style={{flex: 1}}>
|
||||
<Text type="title-lg" style={[pal.text, {fontWeight: 'bold'}]}>
|
||||
<Trans>User Lists</Trans>
|
||||
</Text>
|
||||
<Text style={pal.textLight}>
|
||||
<Trans>Public, shareable lists which can drive feeds.</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Button
|
||||
testID="newUserListBtn"
|
||||
type="default"
|
||||
onPress={onPressNewList}
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
}}>
|
||||
<FontAwesomeIcon icon="plus" color={pal.colors.text} />
|
||||
<Text type="button" style={pal.text}>
|
||||
<Trans>New</Trans>
|
||||
return (
|
||||
<View style={s.hContentRegion} testID="listsScreen">
|
||||
<SimpleViewHeader
|
||||
showBackButton={isMobile}
|
||||
style={
|
||||
!isMobile && [pal.border, {borderLeftWidth: 1, borderRightWidth: 1}]
|
||||
}>
|
||||
<View style={{flex: 1}}>
|
||||
<Text type="title-lg" style={[pal.text, {fontWeight: 'bold'}]}>
|
||||
User Lists
|
||||
</Text>
|
||||
</Button>
|
||||
</View>
|
||||
</SimpleViewHeader>
|
||||
<MyLists filter="curate" style={s.flexGrow1} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
<Text style={pal.textLight}>
|
||||
Public, shareable lists which can drive feeds.
|
||||
</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Button
|
||||
testID="newUserListBtn"
|
||||
type="default"
|
||||
onPress={onPressNewList}
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
}}>
|
||||
<FontAwesomeIcon icon="plus" color={pal.colors.text} />
|
||||
<Text type="button" style={pal.text}>
|
||||
New
|
||||
</Text>
|
||||
</Button>
|
||||
</View>
|
||||
</SimpleViewHeader>
|
||||
<MyLists filter="curate" style={s.flexGrow1} />
|
||||
</View>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
FontAwesomeIconStyle,
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
|
||||
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
|
||||
import {s} from 'lib/styles'
|
||||
import {CenteredView} from '../com/util/Views'
|
||||
import {ViewHeader} from '../com/util/ViewHeader'
|
||||
@@ -16,102 +17,101 @@ import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Moderation'>
|
||||
export function ModerationScreen({}: Props) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {screen, track} = useAnalytics()
|
||||
const {isTabletOrDesktop} = useWebMediaQueries()
|
||||
const {openModal} = useModalControls()
|
||||
export const ModerationScreen = withAuthRequired(
|
||||
function Moderation({}: Props) {
|
||||
const pal = usePalette('default')
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {screen, track} = useAnalytics()
|
||||
const {isTabletOrDesktop} = useWebMediaQueries()
|
||||
const {openModal} = useModalControls()
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
screen('Moderation')
|
||||
setMinimalShellMode(false)
|
||||
}, [screen, setMinimalShellMode]),
|
||||
)
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
screen('Moderation')
|
||||
setMinimalShellMode(false)
|
||||
}, [screen, setMinimalShellMode]),
|
||||
)
|
||||
|
||||
const onPressContentFiltering = React.useCallback(() => {
|
||||
track('Moderation:ContentfilteringButtonClicked')
|
||||
openModal({name: 'content-filtering-settings'})
|
||||
}, [track, openModal])
|
||||
const onPressContentFiltering = React.useCallback(() => {
|
||||
track('Moderation:ContentfilteringButtonClicked')
|
||||
openModal({name: 'content-filtering-settings'})
|
||||
}, [track, openModal])
|
||||
|
||||
return (
|
||||
<CenteredView
|
||||
style={[
|
||||
s.hContentRegion,
|
||||
pal.border,
|
||||
isTabletOrDesktop ? styles.desktopContainer : pal.viewLight,
|
||||
]}
|
||||
testID="moderationScreen">
|
||||
<ViewHeader title={_(msg`Moderation`)} showOnDesktop />
|
||||
<View style={styles.spacer} />
|
||||
<TouchableOpacity
|
||||
testID="contentFilteringBtn"
|
||||
style={[styles.linkCard, pal.view]}
|
||||
onPress={onPressContentFiltering}
|
||||
accessibilityRole="tab"
|
||||
accessibilityHint="Content filtering"
|
||||
accessibilityLabel="">
|
||||
<View style={[styles.iconContainer, pal.btn]}>
|
||||
<FontAwesomeIcon
|
||||
icon="eye"
|
||||
style={pal.text as FontAwesomeIconStyle}
|
||||
/>
|
||||
</View>
|
||||
<Text type="lg" style={pal.text}>
|
||||
<Trans>Content filtering</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<Link
|
||||
testID="moderationlistsBtn"
|
||||
style={[styles.linkCard, pal.view]}
|
||||
href="/moderation/modlists">
|
||||
<View style={[styles.iconContainer, pal.btn]}>
|
||||
<FontAwesomeIcon
|
||||
icon="users-slash"
|
||||
style={pal.text as FontAwesomeIconStyle}
|
||||
/>
|
||||
</View>
|
||||
<Text type="lg" style={pal.text}>
|
||||
<Trans>Moderation lists</Trans>
|
||||
</Text>
|
||||
</Link>
|
||||
<Link
|
||||
testID="mutedAccountsBtn"
|
||||
style={[styles.linkCard, pal.view]}
|
||||
href="/moderation/muted-accounts">
|
||||
<View style={[styles.iconContainer, pal.btn]}>
|
||||
<FontAwesomeIcon
|
||||
icon="user-slash"
|
||||
style={pal.text as FontAwesomeIconStyle}
|
||||
/>
|
||||
</View>
|
||||
<Text type="lg" style={pal.text}>
|
||||
<Trans>Muted accounts</Trans>
|
||||
</Text>
|
||||
</Link>
|
||||
<Link
|
||||
testID="blockedAccountsBtn"
|
||||
style={[styles.linkCard, pal.view]}
|
||||
href="/moderation/blocked-accounts">
|
||||
<View style={[styles.iconContainer, pal.btn]}>
|
||||
<FontAwesomeIcon
|
||||
icon="ban"
|
||||
style={pal.text as FontAwesomeIconStyle}
|
||||
/>
|
||||
</View>
|
||||
<Text type="lg" style={pal.text}>
|
||||
<Trans>Blocked accounts</Trans>
|
||||
</Text>
|
||||
</Link>
|
||||
</CenteredView>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<CenteredView
|
||||
style={[
|
||||
s.hContentRegion,
|
||||
pal.border,
|
||||
isTabletOrDesktop ? styles.desktopContainer : pal.viewLight,
|
||||
]}
|
||||
testID="moderationScreen">
|
||||
<ViewHeader title="Moderation" showOnDesktop />
|
||||
<View style={styles.spacer} />
|
||||
<TouchableOpacity
|
||||
testID="contentFilteringBtn"
|
||||
style={[styles.linkCard, pal.view]}
|
||||
onPress={onPressContentFiltering}
|
||||
accessibilityRole="tab"
|
||||
accessibilityHint="Content filtering"
|
||||
accessibilityLabel="">
|
||||
<View style={[styles.iconContainer, pal.btn]}>
|
||||
<FontAwesomeIcon
|
||||
icon="eye"
|
||||
style={pal.text as FontAwesomeIconStyle}
|
||||
/>
|
||||
</View>
|
||||
<Text type="lg" style={pal.text}>
|
||||
Content filtering
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<Link
|
||||
testID="moderationlistsBtn"
|
||||
style={[styles.linkCard, pal.view]}
|
||||
href="/moderation/modlists">
|
||||
<View style={[styles.iconContainer, pal.btn]}>
|
||||
<FontAwesomeIcon
|
||||
icon="users-slash"
|
||||
style={pal.text as FontAwesomeIconStyle}
|
||||
/>
|
||||
</View>
|
||||
<Text type="lg" style={pal.text}>
|
||||
Moderation lists
|
||||
</Text>
|
||||
</Link>
|
||||
<Link
|
||||
testID="mutedAccountsBtn"
|
||||
style={[styles.linkCard, pal.view]}
|
||||
href="/moderation/muted-accounts">
|
||||
<View style={[styles.iconContainer, pal.btn]}>
|
||||
<FontAwesomeIcon
|
||||
icon="user-slash"
|
||||
style={pal.text as FontAwesomeIconStyle}
|
||||
/>
|
||||
</View>
|
||||
<Text type="lg" style={pal.text}>
|
||||
Muted accounts
|
||||
</Text>
|
||||
</Link>
|
||||
<Link
|
||||
testID="blockedAccountsBtn"
|
||||
style={[styles.linkCard, pal.view]}
|
||||
href="/moderation/blocked-accounts">
|
||||
<View style={[styles.iconContainer, pal.btn]}>
|
||||
<FontAwesomeIcon
|
||||
icon="ban"
|
||||
style={pal.text as FontAwesomeIconStyle}
|
||||
/>
|
||||
</View>
|
||||
<Text type="lg" style={pal.text}>
|
||||
Blocked accounts
|
||||
</Text>
|
||||
</Link>
|
||||
</CenteredView>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
desktopContainer: {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
|
||||
import {Text} from '../com/util/text/Text'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
|
||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
import {CommonNavigatorParams} from 'lib/routes/types'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
@@ -20,8 +21,6 @@ import {ErrorScreen} from '../com/util/error/ErrorScreen'
|
||||
import {ProfileCard} from 'view/com/profile/ProfileCard'
|
||||
import {logger} from '#/logger'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useMyBlockedAccountsQuery} from '#/state/queries/my-blocked-accounts'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
|
||||
@@ -29,144 +28,143 @@ type Props = NativeStackScreenProps<
|
||||
CommonNavigatorParams,
|
||||
'ModerationBlockedAccounts'
|
||||
>
|
||||
export function ModerationBlockedAccounts({}: Props) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {isTabletOrDesktop} = useWebMediaQueries()
|
||||
const {screen} = useAnalytics()
|
||||
const [isPTRing, setIsPTRing] = React.useState(false)
|
||||
const {
|
||||
data,
|
||||
isFetching,
|
||||
isError,
|
||||
error,
|
||||
refetch,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
isFetchingNextPage,
|
||||
} = useMyBlockedAccountsQuery()
|
||||
const isEmpty = !isFetching && !data?.pages[0]?.blocks.length
|
||||
const profiles = React.useMemo(() => {
|
||||
if (data?.pages) {
|
||||
return data.pages.flatMap(page => page.blocks)
|
||||
}
|
||||
return []
|
||||
}, [data])
|
||||
export const ModerationBlockedAccounts = withAuthRequired(
|
||||
function ModerationBlockedAccountsImpl({}: Props) {
|
||||
const pal = usePalette('default')
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {isTabletOrDesktop} = useWebMediaQueries()
|
||||
const {screen} = useAnalytics()
|
||||
const [isPTRing, setIsPTRing] = React.useState(false)
|
||||
const {
|
||||
data,
|
||||
dataUpdatedAt,
|
||||
isFetching,
|
||||
isError,
|
||||
error,
|
||||
refetch,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
isFetchingNextPage,
|
||||
} = useMyBlockedAccountsQuery()
|
||||
const isEmpty = !isFetching && !data?.pages[0]?.blocks.length
|
||||
const profiles = React.useMemo(() => {
|
||||
if (data?.pages) {
|
||||
return data.pages.flatMap(page => page.blocks)
|
||||
}
|
||||
return []
|
||||
}, [data])
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
screen('BlockedAccounts')
|
||||
setMinimalShellMode(false)
|
||||
}, [screen, setMinimalShellMode]),
|
||||
)
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
screen('BlockedAccounts')
|
||||
setMinimalShellMode(false)
|
||||
}, [screen, setMinimalShellMode]),
|
||||
)
|
||||
|
||||
const onRefresh = React.useCallback(async () => {
|
||||
setIsPTRing(true)
|
||||
try {
|
||||
await refetch()
|
||||
} catch (err) {
|
||||
logger.error('Failed to refresh my muted accounts', {error: err})
|
||||
}
|
||||
setIsPTRing(false)
|
||||
}, [refetch, setIsPTRing])
|
||||
const onRefresh = React.useCallback(async () => {
|
||||
setIsPTRing(true)
|
||||
try {
|
||||
await refetch()
|
||||
} catch (err) {
|
||||
logger.error('Failed to refresh my muted accounts', {error: err})
|
||||
}
|
||||
setIsPTRing(false)
|
||||
}, [refetch, setIsPTRing])
|
||||
|
||||
const onEndReached = React.useCallback(async () => {
|
||||
if (isFetching || !hasNextPage || isError) return
|
||||
const onEndReached = React.useCallback(async () => {
|
||||
if (isFetching || !hasNextPage || isError) return
|
||||
|
||||
try {
|
||||
await fetchNextPage()
|
||||
} catch (err) {
|
||||
logger.error('Failed to load more of my muted accounts', {error: err})
|
||||
}
|
||||
}, [isFetching, hasNextPage, isError, fetchNextPage])
|
||||
try {
|
||||
await fetchNextPage()
|
||||
} catch (err) {
|
||||
logger.error('Failed to load more of my muted accounts', {error: err})
|
||||
}
|
||||
}, [isFetching, hasNextPage, isError, fetchNextPage])
|
||||
|
||||
const renderItem = ({
|
||||
item,
|
||||
index,
|
||||
}: {
|
||||
item: ActorDefs.ProfileView
|
||||
index: number
|
||||
}) => (
|
||||
<ProfileCard
|
||||
testID={`blockedAccount-${index}`}
|
||||
key={item.did}
|
||||
profile={item}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<CenteredView
|
||||
style={[
|
||||
styles.container,
|
||||
isTabletOrDesktop && styles.containerDesktop,
|
||||
pal.view,
|
||||
pal.border,
|
||||
]}
|
||||
testID="blockedAccountsScreen">
|
||||
<ViewHeader title={_(msg`Blocked Accounts`)} showOnDesktop />
|
||||
<Text
|
||||
type="sm"
|
||||
const renderItem = ({
|
||||
item,
|
||||
index,
|
||||
}: {
|
||||
item: ActorDefs.ProfileView
|
||||
index: number
|
||||
}) => (
|
||||
<ProfileCard
|
||||
testID={`blockedAccount-${index}`}
|
||||
key={item.did}
|
||||
profile={item}
|
||||
dataUpdatedAt={dataUpdatedAt}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<CenteredView
|
||||
style={[
|
||||
styles.description,
|
||||
pal.text,
|
||||
isTabletOrDesktop && styles.descriptionDesktop,
|
||||
]}>
|
||||
<Trans>
|
||||
styles.container,
|
||||
isTabletOrDesktop && styles.containerDesktop,
|
||||
pal.view,
|
||||
pal.border,
|
||||
]}
|
||||
testID="blockedAccountsScreen">
|
||||
<ViewHeader title="Blocked Accounts" showOnDesktop />
|
||||
<Text
|
||||
type="sm"
|
||||
style={[
|
||||
styles.description,
|
||||
pal.text,
|
||||
isTabletOrDesktop && styles.descriptionDesktop,
|
||||
]}>
|
||||
Blocked accounts cannot reply in your threads, mention you, or
|
||||
otherwise interact with you. You will not see their content and they
|
||||
will be prevented from seeing yours.
|
||||
</Trans>
|
||||
</Text>
|
||||
{isEmpty ? (
|
||||
<View style={[pal.border, !isTabletOrDesktop && styles.flex1]}>
|
||||
{isError ? (
|
||||
<ErrorScreen
|
||||
title="Oops!"
|
||||
message={cleanError(error)}
|
||||
onPressTryAgain={refetch}
|
||||
/>
|
||||
) : (
|
||||
<View style={[styles.empty, pal.viewLight]}>
|
||||
<Text type="lg" style={[pal.text, styles.emptyText]}>
|
||||
<Trans>
|
||||
</Text>
|
||||
{isEmpty ? (
|
||||
<View style={[pal.border, !isTabletOrDesktop && styles.flex1]}>
|
||||
{isError ? (
|
||||
<ErrorScreen
|
||||
title="Oops!"
|
||||
message={cleanError(error)}
|
||||
onPressTryAgain={refetch}
|
||||
/>
|
||||
) : (
|
||||
<View style={[styles.empty, pal.viewLight]}>
|
||||
<Text type="lg" style={[pal.text, styles.emptyText]}>
|
||||
You have not blocked any accounts yet. To block an account, go
|
||||
to their profile and selected "Block account" from the menu on
|
||||
their account.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
style={[!isTabletOrDesktop && styles.flex1]}
|
||||
data={profiles}
|
||||
keyExtractor={(item: ActorDefs.ProfileView) => item.did}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={isPTRing}
|
||||
onRefresh={onRefresh}
|
||||
tintColor={pal.colors.text}
|
||||
titleColor={pal.colors.text}
|
||||
/>
|
||||
}
|
||||
onEndReached={onEndReached}
|
||||
renderItem={renderItem}
|
||||
initialNumToRender={15}
|
||||
// FIXME(dan)
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
style={[!isTabletOrDesktop && styles.flex1]}
|
||||
data={profiles}
|
||||
keyExtractor={(item: ActorDefs.ProfileView) => item.did}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={isPTRing}
|
||||
onRefresh={onRefresh}
|
||||
tintColor={pal.colors.text}
|
||||
titleColor={pal.colors.text}
|
||||
/>
|
||||
}
|
||||
onEndReached={onEndReached}
|
||||
renderItem={renderItem}
|
||||
initialNumToRender={15}
|
||||
// FIXME(dan)
|
||||
|
||||
ListFooterComponent={() => (
|
||||
<View style={styles.footer}>
|
||||
{(isFetching || isFetchingNextPage) && <ActivityIndicator />}
|
||||
</View>
|
||||
)}
|
||||
// @ts-ignore our .web version only -prf
|
||||
desktopFixedHeight
|
||||
/>
|
||||
)}
|
||||
</CenteredView>
|
||||
)
|
||||
}
|
||||
ListFooterComponent={() => (
|
||||
<View style={styles.footer}>
|
||||
{(isFetching || isFetchingNextPage) && <ActivityIndicator />}
|
||||
</View>
|
||||
)}
|
||||
// @ts-ignore our .web version only -prf
|
||||
desktopFixedHeight
|
||||
/>
|
||||
)}
|
||||
</CenteredView>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
|
||||
@@ -4,6 +4,7 @@ import {useFocusEffect, useNavigation} from '@react-navigation/native'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {AtUri} from '@atproto/api'
|
||||
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
|
||||
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
|
||||
import {MyLists} from '#/view/com/lists/MyLists'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {Button} from 'view/com/util/forms/Button'
|
||||
@@ -16,68 +17,70 @@ import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ModerationModlists'>
|
||||
export function ModerationModlistsScreen({}: Props) {
|
||||
const pal = usePalette('default')
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {openModal} = useModalControls()
|
||||
export const ModerationModlistsScreen = withAuthRequired(
|
||||
function ModerationModlistsScreenImpl({}: Props) {
|
||||
const pal = usePalette('default')
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {openModal} = useModalControls()
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
setMinimalShellMode(false)
|
||||
}, [setMinimalShellMode]),
|
||||
)
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
setMinimalShellMode(false)
|
||||
}, [setMinimalShellMode]),
|
||||
)
|
||||
|
||||
const onPressNewList = React.useCallback(() => {
|
||||
openModal({
|
||||
name: 'create-or-edit-list',
|
||||
purpose: 'app.bsky.graph.defs#modlist',
|
||||
onSave: (uri: string) => {
|
||||
try {
|
||||
const urip = new AtUri(uri)
|
||||
navigation.navigate('ProfileList', {
|
||||
name: urip.hostname,
|
||||
rkey: urip.rkey,
|
||||
})
|
||||
} catch {}
|
||||
},
|
||||
})
|
||||
}, [openModal, navigation])
|
||||
const onPressNewList = React.useCallback(() => {
|
||||
openModal({
|
||||
name: 'create-or-edit-list',
|
||||
purpose: 'app.bsky.graph.defs#modlist',
|
||||
onSave: (uri: string) => {
|
||||
try {
|
||||
const urip = new AtUri(uri)
|
||||
navigation.navigate('ProfileList', {
|
||||
name: urip.hostname,
|
||||
rkey: urip.rkey,
|
||||
})
|
||||
} catch {}
|
||||
},
|
||||
})
|
||||
}, [openModal, navigation])
|
||||
|
||||
return (
|
||||
<View style={s.hContentRegion} testID="moderationModlistsScreen">
|
||||
<SimpleViewHeader
|
||||
showBackButton={isMobile}
|
||||
style={
|
||||
!isMobile && [pal.border, {borderLeftWidth: 1, borderRightWidth: 1}]
|
||||
}>
|
||||
<View style={{flex: 1}}>
|
||||
<Text type="title-lg" style={[pal.text, {fontWeight: 'bold'}]}>
|
||||
Moderation Lists
|
||||
</Text>
|
||||
<Text style={pal.textLight}>
|
||||
Public, shareable lists of users to mute or block in bulk.
|
||||
</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Button
|
||||
testID="newModListBtn"
|
||||
type="default"
|
||||
onPress={onPressNewList}
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
}}>
|
||||
<FontAwesomeIcon icon="plus" color={pal.colors.text} />
|
||||
<Text type="button" style={pal.text}>
|
||||
New
|
||||
return (
|
||||
<View style={s.hContentRegion} testID="moderationModlistsScreen">
|
||||
<SimpleViewHeader
|
||||
showBackButton={isMobile}
|
||||
style={
|
||||
!isMobile && [pal.border, {borderLeftWidth: 1, borderRightWidth: 1}]
|
||||
}>
|
||||
<View style={{flex: 1}}>
|
||||
<Text type="title-lg" style={[pal.text, {fontWeight: 'bold'}]}>
|
||||
Moderation Lists
|
||||
</Text>
|
||||
</Button>
|
||||
</View>
|
||||
</SimpleViewHeader>
|
||||
<MyLists filter="mod" style={s.flexGrow1} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
<Text style={pal.textLight}>
|
||||
Public, shareable lists of users to mute or block in bulk.
|
||||
</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Button
|
||||
testID="newModListBtn"
|
||||
type="default"
|
||||
onPress={onPressNewList}
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
}}>
|
||||
<FontAwesomeIcon icon="plus" color={pal.colors.text} />
|
||||
<Text type="button" style={pal.text}>
|
||||
New
|
||||
</Text>
|
||||
</Button>
|
||||
</View>
|
||||
</SimpleViewHeader>
|
||||
<MyLists filter="mod" style={s.flexGrow1} />
|
||||
</View>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@ import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
|
||||
import {Text} from '../com/util/text/Text'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
|
||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
import {CommonNavigatorParams} from 'lib/routes/types'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
@@ -20,8 +21,6 @@ import {ErrorScreen} from '../com/util/error/ErrorScreen'
|
||||
import {ProfileCard} from 'view/com/profile/ProfileCard'
|
||||
import {logger} from '#/logger'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useMyMutedAccountsQuery} from '#/state/queries/my-muted-accounts'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
|
||||
@@ -29,143 +28,142 @@ type Props = NativeStackScreenProps<
|
||||
CommonNavigatorParams,
|
||||
'ModerationMutedAccounts'
|
||||
>
|
||||
export function ModerationMutedAccounts({}: Props) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {isTabletOrDesktop} = useWebMediaQueries()
|
||||
const {screen} = useAnalytics()
|
||||
const [isPTRing, setIsPTRing] = React.useState(false)
|
||||
const {
|
||||
data,
|
||||
isFetching,
|
||||
isError,
|
||||
error,
|
||||
refetch,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
isFetchingNextPage,
|
||||
} = useMyMutedAccountsQuery()
|
||||
const isEmpty = !isFetching && !data?.pages[0]?.mutes.length
|
||||
const profiles = React.useMemo(() => {
|
||||
if (data?.pages) {
|
||||
return data.pages.flatMap(page => page.mutes)
|
||||
}
|
||||
return []
|
||||
}, [data])
|
||||
export const ModerationMutedAccounts = withAuthRequired(
|
||||
function ModerationMutedAccountsImpl({}: Props) {
|
||||
const pal = usePalette('default')
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {isTabletOrDesktop} = useWebMediaQueries()
|
||||
const {screen} = useAnalytics()
|
||||
const [isPTRing, setIsPTRing] = React.useState(false)
|
||||
const {
|
||||
data,
|
||||
dataUpdatedAt,
|
||||
isFetching,
|
||||
isError,
|
||||
error,
|
||||
refetch,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
isFetchingNextPage,
|
||||
} = useMyMutedAccountsQuery()
|
||||
const isEmpty = !isFetching && !data?.pages[0]?.mutes.length
|
||||
const profiles = React.useMemo(() => {
|
||||
if (data?.pages) {
|
||||
return data.pages.flatMap(page => page.mutes)
|
||||
}
|
||||
return []
|
||||
}, [data])
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
screen('MutedAccounts')
|
||||
setMinimalShellMode(false)
|
||||
}, [screen, setMinimalShellMode]),
|
||||
)
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
screen('MutedAccounts')
|
||||
setMinimalShellMode(false)
|
||||
}, [screen, setMinimalShellMode]),
|
||||
)
|
||||
|
||||
const onRefresh = React.useCallback(async () => {
|
||||
setIsPTRing(true)
|
||||
try {
|
||||
await refetch()
|
||||
} catch (err) {
|
||||
logger.error('Failed to refresh my muted accounts', {error: err})
|
||||
}
|
||||
setIsPTRing(false)
|
||||
}, [refetch, setIsPTRing])
|
||||
const onRefresh = React.useCallback(async () => {
|
||||
setIsPTRing(true)
|
||||
try {
|
||||
await refetch()
|
||||
} catch (err) {
|
||||
logger.error('Failed to refresh my muted accounts', {error: err})
|
||||
}
|
||||
setIsPTRing(false)
|
||||
}, [refetch, setIsPTRing])
|
||||
|
||||
const onEndReached = React.useCallback(async () => {
|
||||
if (isFetching || !hasNextPage || isError) return
|
||||
const onEndReached = React.useCallback(async () => {
|
||||
if (isFetching || !hasNextPage || isError) return
|
||||
|
||||
try {
|
||||
await fetchNextPage()
|
||||
} catch (err) {
|
||||
logger.error('Failed to load more of my muted accounts', {error: err})
|
||||
}
|
||||
}, [isFetching, hasNextPage, isError, fetchNextPage])
|
||||
try {
|
||||
await fetchNextPage()
|
||||
} catch (err) {
|
||||
logger.error('Failed to load more of my muted accounts', {error: err})
|
||||
}
|
||||
}, [isFetching, hasNextPage, isError, fetchNextPage])
|
||||
|
||||
const renderItem = ({
|
||||
item,
|
||||
index,
|
||||
}: {
|
||||
item: ActorDefs.ProfileView
|
||||
index: number
|
||||
}) => (
|
||||
<ProfileCard
|
||||
testID={`mutedAccount-${index}`}
|
||||
key={item.did}
|
||||
profile={item}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<CenteredView
|
||||
style={[
|
||||
styles.container,
|
||||
isTabletOrDesktop && styles.containerDesktop,
|
||||
pal.view,
|
||||
pal.border,
|
||||
]}
|
||||
testID="mutedAccountsScreen">
|
||||
<ViewHeader title={_(msg`Muted Accounts`)} showOnDesktop />
|
||||
<Text
|
||||
type="sm"
|
||||
const renderItem = ({
|
||||
item,
|
||||
index,
|
||||
}: {
|
||||
item: ActorDefs.ProfileView
|
||||
index: number
|
||||
}) => (
|
||||
<ProfileCard
|
||||
testID={`mutedAccount-${index}`}
|
||||
key={item.did}
|
||||
profile={item}
|
||||
dataUpdatedAt={dataUpdatedAt}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<CenteredView
|
||||
style={[
|
||||
styles.description,
|
||||
pal.text,
|
||||
isTabletOrDesktop && styles.descriptionDesktop,
|
||||
]}>
|
||||
<Trans>
|
||||
styles.container,
|
||||
isTabletOrDesktop && styles.containerDesktop,
|
||||
pal.view,
|
||||
pal.border,
|
||||
]}
|
||||
testID="mutedAccountsScreen">
|
||||
<ViewHeader title="Muted Accounts" showOnDesktop />
|
||||
<Text
|
||||
type="sm"
|
||||
style={[
|
||||
styles.description,
|
||||
pal.text,
|
||||
isTabletOrDesktop && styles.descriptionDesktop,
|
||||
]}>
|
||||
Muted accounts have their posts removed from your feed and from your
|
||||
notifications. Mutes are completely private.
|
||||
</Trans>
|
||||
</Text>
|
||||
{isEmpty ? (
|
||||
<View style={[pal.border, !isTabletOrDesktop && styles.flex1]}>
|
||||
{isError ? (
|
||||
<ErrorScreen
|
||||
title="Oops!"
|
||||
message={cleanError(error)}
|
||||
onPressTryAgain={refetch}
|
||||
/>
|
||||
) : (
|
||||
<View style={[styles.empty, pal.viewLight]}>
|
||||
<Text type="lg" style={[pal.text, styles.emptyText]}>
|
||||
<Trans>
|
||||
</Text>
|
||||
{isEmpty ? (
|
||||
<View style={[pal.border, !isTabletOrDesktop && styles.flex1]}>
|
||||
{isError ? (
|
||||
<ErrorScreen
|
||||
title="Oops!"
|
||||
message={cleanError(error)}
|
||||
onPressTryAgain={refetch}
|
||||
/>
|
||||
) : (
|
||||
<View style={[styles.empty, pal.viewLight]}>
|
||||
<Text type="lg" style={[pal.text, styles.emptyText]}>
|
||||
You have not muted any accounts yet. To mute an account, go to
|
||||
their profile and selected "Mute account" from the menu on
|
||||
their account.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
style={[!isTabletOrDesktop && styles.flex1]}
|
||||
data={profiles}
|
||||
keyExtractor={item => item.did}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={isPTRing}
|
||||
onRefresh={onRefresh}
|
||||
tintColor={pal.colors.text}
|
||||
titleColor={pal.colors.text}
|
||||
/>
|
||||
}
|
||||
onEndReached={onEndReached}
|
||||
renderItem={renderItem}
|
||||
initialNumToRender={15}
|
||||
// FIXME(dan)
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
style={[!isTabletOrDesktop && styles.flex1]}
|
||||
data={profiles}
|
||||
keyExtractor={item => item.did}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={isPTRing}
|
||||
onRefresh={onRefresh}
|
||||
tintColor={pal.colors.text}
|
||||
titleColor={pal.colors.text}
|
||||
/>
|
||||
}
|
||||
onEndReached={onEndReached}
|
||||
renderItem={renderItem}
|
||||
initialNumToRender={15}
|
||||
// FIXME(dan)
|
||||
|
||||
ListFooterComponent={() => (
|
||||
<View style={styles.footer}>
|
||||
{(isFetching || isFetchingNextPage) && <ActivityIndicator />}
|
||||
</View>
|
||||
)}
|
||||
// @ts-ignore our .web version only -prf
|
||||
desktopFixedHeight
|
||||
/>
|
||||
)}
|
||||
</CenteredView>
|
||||
)
|
||||
}
|
||||
ListFooterComponent={() => (
|
||||
<View style={styles.footer}>
|
||||
{(isFetching || isFetchingNextPage) && <ActivityIndicator />}
|
||||
</View>
|
||||
)}
|
||||
// @ts-ignore our .web version only -prf
|
||||
desktopFixedHeight
|
||||
/>
|
||||
)}
|
||||
</CenteredView>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
|
||||
@@ -12,12 +12,9 @@ import {NavigationProp} from 'lib/routes/types'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {s} from 'lib/styles'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
export const NotFoundScreen = () => {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
|
||||
@@ -39,15 +36,13 @@ export const NotFoundScreen = () => {
|
||||
|
||||
return (
|
||||
<View testID="notFoundView" style={pal.view}>
|
||||
<ViewHeader title={_(msg`Page not found`)} />
|
||||
<ViewHeader title="Page not found" />
|
||||
<View style={styles.container}>
|
||||
<Text type="title-2xl" style={[pal.text, s.mb10]}>
|
||||
<Trans>Page not found</Trans>
|
||||
Page not found
|
||||
</Text>
|
||||
<Text type="md" style={[pal.text, s.mb10]}>
|
||||
<Trans>
|
||||
We're sorry! We can't find the page you were looking for.
|
||||
</Trans>
|
||||
We're sorry! We can't find the page you were looking for.
|
||||
</Text>
|
||||
<Button
|
||||
type="primary"
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
NativeStackScreenProps,
|
||||
NotificationsTabNavigatorParams,
|
||||
} from 'lib/routes/types'
|
||||
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
|
||||
import {ViewHeader} from '../com/util/ViewHeader'
|
||||
import {Feed} from '../com/notifications/Feed'
|
||||
import {TextLink} from 'view/com/util/Link'
|
||||
@@ -17,8 +18,6 @@ import {s, colors} from 'lib/styles'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {logger} from '#/logger'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useUnreadNotifications} from '#/state/queries/notifications/unread'
|
||||
import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed'
|
||||
import {listenSoftReset, emitSoftReset} from '#/state/events'
|
||||
@@ -27,100 +26,101 @@ type Props = NativeStackScreenProps<
|
||||
NotificationsTabNavigatorParams,
|
||||
'Notifications'
|
||||
>
|
||||
export function NotificationsScreen({}: Props) {
|
||||
const {_} = useLingui()
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const [onMainScroll, isScrolledDown, resetMainScroll] = useOnMainScroll()
|
||||
const scrollElRef = React.useRef<FlatList>(null)
|
||||
const {screen} = useAnalytics()
|
||||
const pal = usePalette('default')
|
||||
const {isDesktop} = useWebMediaQueries()
|
||||
const unreadNotifs = useUnreadNotifications()
|
||||
const queryClient = useQueryClient()
|
||||
const hasNew = !!unreadNotifs
|
||||
export const NotificationsScreen = withAuthRequired(
|
||||
function NotificationsScreenImpl({}: Props) {
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const [onMainScroll, isScrolledDown, resetMainScroll] = useOnMainScroll()
|
||||
const scrollElRef = React.useRef<FlatList>(null)
|
||||
const {screen} = useAnalytics()
|
||||
const pal = usePalette('default')
|
||||
const {isDesktop} = useWebMediaQueries()
|
||||
const unreadNotifs = useUnreadNotifications()
|
||||
const queryClient = useQueryClient()
|
||||
const hasNew = !!unreadNotifs
|
||||
|
||||
// event handlers
|
||||
// =
|
||||
const scrollToTop = React.useCallback(() => {
|
||||
scrollElRef.current?.scrollToOffset({offset: 0})
|
||||
resetMainScroll()
|
||||
}, [scrollElRef, resetMainScroll])
|
||||
// event handlers
|
||||
// =
|
||||
const scrollToTop = React.useCallback(() => {
|
||||
scrollElRef.current?.scrollToOffset({offset: 0})
|
||||
resetMainScroll()
|
||||
}, [scrollElRef, resetMainScroll])
|
||||
|
||||
const onPressLoadLatest = React.useCallback(() => {
|
||||
scrollToTop()
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: NOTIFS_RQKEY(),
|
||||
})
|
||||
}, [scrollToTop, queryClient])
|
||||
const onPressLoadLatest = React.useCallback(() => {
|
||||
scrollToTop()
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: NOTIFS_RQKEY(),
|
||||
})
|
||||
}, [scrollToTop, queryClient])
|
||||
|
||||
// on-visible setup
|
||||
// =
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
setMinimalShellMode(false)
|
||||
logger.debug('NotificationsScreen: Updating feed')
|
||||
screen('Notifications')
|
||||
return listenSoftReset(onPressLoadLatest)
|
||||
}, [screen, onPressLoadLatest, setMinimalShellMode]),
|
||||
)
|
||||
// on-visible setup
|
||||
// =
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
setMinimalShellMode(false)
|
||||
logger.debug('NotificationsScreen: Updating feed')
|
||||
screen('Notifications')
|
||||
return listenSoftReset(onPressLoadLatest)
|
||||
}, [screen, onPressLoadLatest, setMinimalShellMode]),
|
||||
)
|
||||
|
||||
const ListHeaderComponent = React.useCallback(() => {
|
||||
if (isDesktop) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
pal.view,
|
||||
{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 18,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
]}>
|
||||
<TextLink
|
||||
type="title-lg"
|
||||
href="/notifications"
|
||||
style={[pal.text, {fontWeight: 'bold'}]}
|
||||
text={
|
||||
<>
|
||||
<Trans>Notifications</Trans>{' '}
|
||||
{hasNew && (
|
||||
<View
|
||||
style={{
|
||||
top: -8,
|
||||
backgroundColor: colors.blue3,
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
onPress={emitSoftReset}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
return <></>
|
||||
}, [isDesktop, pal, hasNew])
|
||||
const ListHeaderComponent = React.useCallback(() => {
|
||||
if (isDesktop) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
pal.view,
|
||||
{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 18,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
]}>
|
||||
<TextLink
|
||||
type="title-lg"
|
||||
href="/notifications"
|
||||
style={[pal.text, {fontWeight: 'bold'}]}
|
||||
text={
|
||||
<>
|
||||
Notifications{' '}
|
||||
{hasNew && (
|
||||
<View
|
||||
style={{
|
||||
top: -8,
|
||||
backgroundColor: colors.blue3,
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
onPress={emitSoftReset}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
return <></>
|
||||
}, [isDesktop, pal, hasNew])
|
||||
|
||||
return (
|
||||
<View testID="notificationsScreen" style={s.hContentRegion}>
|
||||
<ViewHeader title={_(msg`Notifications`)} canGoBack={false} />
|
||||
<Feed
|
||||
onScroll={onMainScroll}
|
||||
scrollElRef={scrollElRef}
|
||||
ListHeaderComponent={ListHeaderComponent}
|
||||
/>
|
||||
{(isScrolledDown || hasNew) && (
|
||||
<LoadLatestBtn
|
||||
onPress={onPressLoadLatest}
|
||||
label={_(msg`Load new notifications`)}
|
||||
showIndicator={hasNew}
|
||||
return (
|
||||
<View testID="notificationsScreen" style={s.hContentRegion}>
|
||||
<ViewHeader title="Notifications" canGoBack={false} />
|
||||
<Feed
|
||||
onScroll={onMainScroll}
|
||||
scrollElRef={scrollElRef}
|
||||
ListHeaderComponent={ListHeaderComponent}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
{(isScrolledDown || hasNew) && (
|
||||
<LoadLatestBtn
|
||||
onPress={onPressLoadLatest}
|
||||
label="Load new notifications"
|
||||
showIndicator={hasNew}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -2,19 +2,17 @@ import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
|
||||
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
|
||||
import {ViewHeader} from '../com/util/ViewHeader'
|
||||
import {PostLikedBy as PostLikedByComponent} from '../com/post-thread/PostLikedBy'
|
||||
import {makeRecordUri} from 'lib/strings/url-helpers'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostLikedBy'>
|
||||
export const PostLikedByScreen = ({route}: Props) => {
|
||||
export const PostLikedByScreen = withAuthRequired(({route}: Props) => {
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {name, rkey} = route.params
|
||||
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
|
||||
const {_} = useLingui()
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
@@ -24,8 +22,8 @@ export const PostLikedByScreen = ({route}: Props) => {
|
||||
|
||||
return (
|
||||
<View>
|
||||
<ViewHeader title={_(msg`Liked by`)} />
|
||||
<ViewHeader title="Liked by" />
|
||||
<PostLikedByComponent uri={uri} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
|
||||
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
|
||||
import {ViewHeader} from '../com/util/ViewHeader'
|
||||
import {PostRepostedBy as PostRepostedByComponent} from '../com/post-thread/PostRepostedBy'
|
||||
import {makeRecordUri} from 'lib/strings/url-helpers'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg} from '@lingui/macro'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostRepostedBy'>
|
||||
export const PostRepostedByScreen = ({route}: Props) => {
|
||||
export const PostRepostedByScreen = withAuthRequired(({route}: Props) => {
|
||||
const {name, rkey} = route.params
|
||||
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {_} = useLingui()
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
@@ -24,8 +22,8 @@ export const PostRepostedByScreen = ({route}: Props) => {
|
||||
|
||||
return (
|
||||
<View>
|
||||
<ViewHeader title={_(msg`Reposted by`)} />
|
||||
<ViewHeader title="Reposted by" />
|
||||
<PostRepostedByComponent uri={uri} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -5,6 +5,7 @@ import {useFocusEffect} from '@react-navigation/native'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
|
||||
import {makeRecordUri} from 'lib/strings/url-helpers'
|
||||
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
|
||||
import {ViewHeader} from '../com/util/ViewHeader'
|
||||
import {PostThread as PostThreadComponent} from '../com/post-thread/PostThread'
|
||||
import {ComposePrompt} from 'view/com/composer/Prompt'
|
||||
@@ -18,17 +19,16 @@ import {clamp} from 'lodash'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
|
||||
import {ErrorMessage} from '../com/util/error/ErrorMessage'
|
||||
import {CenteredView} from '../com/util/Views'
|
||||
import {useComposerControls} from '#/state/shell/composer'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'>
|
||||
export function PostThreadScreen({route}: Props) {
|
||||
export const PostThreadScreen = withAuthRequired(function PostThreadScreenImpl({
|
||||
route,
|
||||
}: Props) {
|
||||
const queryClient = useQueryClient()
|
||||
const {_} = useLingui()
|
||||
const {fabMinimalShellTransform} = useMinimalShellMode()
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {openComposer} = useComposerControls()
|
||||
@@ -74,7 +74,7 @@ export function PostThreadScreen({route}: Props) {
|
||||
|
||||
return (
|
||||
<View style={s.hContentRegion}>
|
||||
{isMobile && <ViewHeader title={_(msg`Post`)} />}
|
||||
{isMobile && <ViewHeader title="Post" />}
|
||||
<View style={s.flex1}>
|
||||
{uriError ? (
|
||||
<CenteredView>
|
||||
@@ -101,7 +101,7 @@ export function PostThreadScreen({route}: Props) {
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
prompt: {
|
||||
|
||||
@@ -92,7 +92,7 @@ export function PreferencesHomeFeed({navigation}: Props) {
|
||||
styles.container,
|
||||
isTabletOrDesktop && styles.desktopContainer,
|
||||
]}>
|
||||
<ViewHeader title={_(msg`Home Feed Preferences`)} showOnDesktop />
|
||||
<ViewHeader title="Home Feed Preferences" showOnDesktop />
|
||||
<View
|
||||
style={[
|
||||
styles.titleSection,
|
||||
@@ -142,7 +142,7 @@ export function PreferencesHomeFeed({navigation}: Props) {
|
||||
</Text>
|
||||
<ToggleButton
|
||||
type="default-light"
|
||||
label={_(msg`Followed users only`)}
|
||||
label="Followed users only"
|
||||
isSelected={Boolean(
|
||||
variables?.hideRepliesByUnfollowed ??
|
||||
preferences?.feedViewPrefs?.hideRepliesByUnfollowed,
|
||||
@@ -188,8 +188,8 @@ export function PreferencesHomeFeed({navigation}: Props) {
|
||||
label={
|
||||
variables?.hideReposts ??
|
||||
preferences?.feedViewPrefs?.hideReposts
|
||||
? _(msg`No`)
|
||||
: _(msg`Yes`)
|
||||
? 'No'
|
||||
: 'Yes'
|
||||
}
|
||||
isSelected={
|
||||
!(
|
||||
@@ -223,8 +223,8 @@ export function PreferencesHomeFeed({navigation}: Props) {
|
||||
label={
|
||||
variables?.hideQuotePosts ??
|
||||
preferences?.feedViewPrefs?.hideQuotePosts
|
||||
? _(msg`No`)
|
||||
: _(msg`Yes`)
|
||||
? 'No'
|
||||
: 'Yes'
|
||||
}
|
||||
isSelected={
|
||||
!(
|
||||
@@ -259,8 +259,8 @@ export function PreferencesHomeFeed({navigation}: Props) {
|
||||
label={
|
||||
variables?.lab_mergeFeedEnabled ??
|
||||
preferences?.feedViewPrefs?.lab_mergeFeedEnabled
|
||||
? _(msg`Yes`)
|
||||
: _(msg`No`)
|
||||
? 'Yes'
|
||||
: 'No'
|
||||
}
|
||||
isSelected={
|
||||
!!(
|
||||
|
||||
@@ -50,7 +50,7 @@ export function PreferencesThreads({navigation}: Props) {
|
||||
styles.container,
|
||||
isTabletOrDesktop && styles.desktopContainer,
|
||||
]}>
|
||||
<ViewHeader title={_(msg`Thread Preferences`)} showOnDesktop />
|
||||
<ViewHeader title="Thread Preferences" showOnDesktop />
|
||||
<View
|
||||
style={[
|
||||
styles.titleSection,
|
||||
|
||||
@@ -9,13 +9,10 @@ import {ScrollView} from 'view/com/util/Views'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {s} from 'lib/styles'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PrivacyPolicy'>
|
||||
export const PrivacyPolicyScreen = (_props: Props) => {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
|
||||
useFocusEffect(
|
||||
@@ -26,18 +23,16 @@ export const PrivacyPolicyScreen = (_props: Props) => {
|
||||
|
||||
return (
|
||||
<View>
|
||||
<ViewHeader title={_(msg`Privacy Policy`)} />
|
||||
<ViewHeader title="Privacy Policy" />
|
||||
<ScrollView style={[s.hContentRegion, pal.view]}>
|
||||
<View style={[s.p20]}>
|
||||
<Text style={pal.text}>
|
||||
<Trans>
|
||||
The Privacy Policy has been moved to{' '}
|
||||
<TextLink
|
||||
style={pal.link}
|
||||
href="https://blueskyweb.xyz/support/privacy-policy"
|
||||
text="blueskyweb.xyz/support/privacy-policy"
|
||||
/>
|
||||
</Trans>
|
||||
The Privacy Policy has been moved to{' '}
|
||||
<TextLink
|
||||
style={pal.link}
|
||||
href="https://blueskyweb.xyz/support/privacy-policy"
|
||||
text="blueskyweb.xyz/support/privacy-policy"
|
||||
/>
|
||||
</Text>
|
||||
</View>
|
||||
<View style={s.footerSpacer} />
|
||||
|
||||
+42
-103
@@ -5,6 +5,8 @@ import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
|
||||
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
|
||||
import {ViewSelectorHandle} from '../com/util/ViewSelector'
|
||||
import {CenteredView, FlatList} from '../com/util/Views'
|
||||
import {ScreenHider} from 'view/com/util/moderation/ScreenHider'
|
||||
import {Feed} from 'view/com/posts/Feed'
|
||||
@@ -36,12 +38,10 @@ import {useQueryClient} from '@tanstack/react-query'
|
||||
import {useComposerControls} from '#/state/shell/composer'
|
||||
import {listenSoftReset} from '#/state/events'
|
||||
|
||||
interface SectionRef {
|
||||
scrollToTop: () => void
|
||||
}
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Profile'>
|
||||
export function ProfileScreen({route}: Props) {
|
||||
export const ProfileScreen = withAuthRequired(function ProfileScreenImpl({
|
||||
route,
|
||||
}: Props) {
|
||||
const {currentAccount} = useSession()
|
||||
const name =
|
||||
route.params.name === 'me' ? currentAccount?.did : route.params.name
|
||||
@@ -54,6 +54,7 @@ export function ProfileScreen({route}: Props) {
|
||||
} = useResolveDidQuery(name)
|
||||
const {
|
||||
data: profile,
|
||||
dataUpdatedAt,
|
||||
error: profileError,
|
||||
refetch: refetchProfile,
|
||||
isFetching: isFetchingProfile,
|
||||
@@ -96,6 +97,7 @@ export function ProfileScreen({route}: Props) {
|
||||
return (
|
||||
<ProfileScreenLoaded
|
||||
profile={profile}
|
||||
dataUpdatedAt={dataUpdatedAt}
|
||||
moderationOpts={moderationOpts}
|
||||
hideBackButton={!!route.params.hideBackButton}
|
||||
/>
|
||||
@@ -112,32 +114,29 @@ export function ProfileScreen({route}: Props) {
|
||||
/>
|
||||
</CenteredView>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
function ProfileScreenLoaded({
|
||||
profile: profileUnshadowed,
|
||||
dataUpdatedAt,
|
||||
moderationOpts,
|
||||
hideBackButton,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
dataUpdatedAt: number
|
||||
moderationOpts: ModerationOpts
|
||||
hideBackButton: boolean
|
||||
}) {
|
||||
const profile = useProfileShadow(profileUnshadowed)
|
||||
const {hasSession, currentAccount} = useSession()
|
||||
const profile = useProfileShadow(profileUnshadowed, dataUpdatedAt)
|
||||
const {currentAccount} = useSession()
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {openComposer} = useComposerControls()
|
||||
const {screen, track} = useAnalytics()
|
||||
const [currentPage, setCurrentPage] = React.useState(0)
|
||||
const {_} = useLingui()
|
||||
const viewSelectorRef = React.useRef<ViewSelectorHandle>(null)
|
||||
const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
|
||||
const extraInfoQuery = useProfileExtraInfoQuery(profile.did)
|
||||
const postsSectionRef = React.useRef<SectionRef>(null)
|
||||
const repliesSectionRef = React.useRef<SectionRef>(null)
|
||||
const mediaSectionRef = React.useRef<SectionRef>(null)
|
||||
const likesSectionRef = React.useRef<SectionRef>(null)
|
||||
const feedsSectionRef = React.useRef<SectionRef>(null)
|
||||
const listsSectionRef = React.useRef<SectionRef>(null)
|
||||
|
||||
useSetTitle(combinedDisplayName(profile))
|
||||
|
||||
@@ -147,68 +146,28 @@ function ProfileScreenLoaded({
|
||||
)
|
||||
|
||||
const isMe = profile.did === currentAccount?.did
|
||||
const showRepliesTab = hasSession
|
||||
const showLikesTab = isMe
|
||||
const showFeedsTab = isMe || extraInfoQuery.data?.hasFeedgens
|
||||
const showListsTab = hasSession && (isMe || extraInfoQuery.data?.hasLists)
|
||||
const showListsTab = isMe || extraInfoQuery.data?.hasLists
|
||||
const sectionTitles = useMemo<string[]>(() => {
|
||||
return [
|
||||
'Posts',
|
||||
showRepliesTab ? 'Posts & Replies' : undefined,
|
||||
'Posts & Replies',
|
||||
'Media',
|
||||
showLikesTab ? 'Likes' : undefined,
|
||||
showFeedsTab ? 'Feeds' : undefined,
|
||||
showListsTab ? 'Lists' : undefined,
|
||||
].filter(Boolean) as string[]
|
||||
}, [showRepliesTab, showLikesTab, showFeedsTab, showListsTab])
|
||||
|
||||
let nextIndex = 0
|
||||
const postsIndex = nextIndex++
|
||||
let repliesIndex: number | null = null
|
||||
if (showRepliesTab) {
|
||||
repliesIndex = nextIndex++
|
||||
}
|
||||
const mediaIndex = nextIndex++
|
||||
let likesIndex: number | null = null
|
||||
if (showLikesTab) {
|
||||
likesIndex = nextIndex++
|
||||
}
|
||||
let feedsIndex: number | null = null
|
||||
if (showFeedsTab) {
|
||||
feedsIndex = nextIndex++
|
||||
}
|
||||
let listsIndex: number | null = null
|
||||
if (showListsTab) {
|
||||
listsIndex = nextIndex++
|
||||
}
|
||||
|
||||
const scrollSectionToTop = React.useCallback(
|
||||
(index: number) => {
|
||||
if (index === postsIndex) {
|
||||
postsSectionRef.current?.scrollToTop()
|
||||
} else if (index === repliesIndex) {
|
||||
repliesSectionRef.current?.scrollToTop()
|
||||
} else if (index === mediaIndex) {
|
||||
mediaSectionRef.current?.scrollToTop()
|
||||
} else if (index === likesIndex) {
|
||||
likesSectionRef.current?.scrollToTop()
|
||||
} else if (index === feedsIndex) {
|
||||
feedsSectionRef.current?.scrollToTop()
|
||||
} else if (index === listsIndex) {
|
||||
listsSectionRef.current?.scrollToTop()
|
||||
}
|
||||
},
|
||||
[postsIndex, repliesIndex, mediaIndex, likesIndex, feedsIndex, listsIndex],
|
||||
)
|
||||
}, [showLikesTab, showFeedsTab, showListsTab])
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
setMinimalShellMode(false)
|
||||
screen('Profile')
|
||||
return listenSoftReset(() => {
|
||||
scrollSectionToTop(currentPage)
|
||||
viewSelectorRef.current?.scrollToTop()
|
||||
})
|
||||
}, [setMinimalShellMode, screen, currentPage, scrollSectionToTop]),
|
||||
}, [viewSelectorRef, setMinimalShellMode, screen]),
|
||||
)
|
||||
|
||||
useFocusEffect(
|
||||
@@ -240,13 +199,6 @@ function ProfileScreenLoaded({
|
||||
[setCurrentPage],
|
||||
)
|
||||
|
||||
const onCurrentPageSelected = React.useCallback(
|
||||
(index: number) => {
|
||||
scrollSectionToTop(index)
|
||||
},
|
||||
[scrollSectionToTop],
|
||||
)
|
||||
|
||||
// rendering
|
||||
// =
|
||||
|
||||
@@ -270,11 +222,10 @@ function ProfileScreenLoaded({
|
||||
isHeaderReady={true}
|
||||
items={sectionTitles}
|
||||
onPageSelected={onPageSelected}
|
||||
onCurrentPageSelected={onCurrentPageSelected}
|
||||
renderHeader={renderHeader}>
|
||||
{({onScroll, headerHeight, isFocused, isScrolledDown, scrollElRef}) => (
|
||||
<FeedSection
|
||||
ref={postsSectionRef}
|
||||
ref={null}
|
||||
feed={`author|${profile.did}|posts_no_replies`}
|
||||
onScroll={onScroll}
|
||||
headerHeight={headerHeight}
|
||||
@@ -285,30 +236,22 @@ function ProfileScreenLoaded({
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{showRepliesTab
|
||||
? ({
|
||||
onScroll,
|
||||
headerHeight,
|
||||
isFocused,
|
||||
isScrolledDown,
|
||||
scrollElRef,
|
||||
}) => (
|
||||
<FeedSection
|
||||
ref={repliesSectionRef}
|
||||
feed={`author|${profile.did}|posts_with_replies`}
|
||||
onScroll={onScroll}
|
||||
headerHeight={headerHeight}
|
||||
isFocused={isFocused}
|
||||
isScrolledDown={isScrolledDown}
|
||||
scrollElRef={
|
||||
scrollElRef as React.MutableRefObject<FlatList<any> | null>
|
||||
}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
{({onScroll, headerHeight, isFocused, isScrolledDown, scrollElRef}) => (
|
||||
<FeedSection
|
||||
ref={mediaSectionRef}
|
||||
ref={null}
|
||||
feed={`author|${profile.did}|posts_with_replies`}
|
||||
onScroll={onScroll}
|
||||
headerHeight={headerHeight}
|
||||
isFocused={isFocused}
|
||||
isScrolledDown={isScrolledDown}
|
||||
scrollElRef={
|
||||
scrollElRef as React.MutableRefObject<FlatList<any> | null>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{({onScroll, headerHeight, isFocused, isScrolledDown, scrollElRef}) => (
|
||||
<FeedSection
|
||||
ref={null}
|
||||
feed={`author|${profile.did}|posts_with_media`}
|
||||
onScroll={onScroll}
|
||||
headerHeight={headerHeight}
|
||||
@@ -328,7 +271,7 @@ function ProfileScreenLoaded({
|
||||
scrollElRef,
|
||||
}) => (
|
||||
<FeedSection
|
||||
ref={likesSectionRef}
|
||||
ref={null}
|
||||
feed={`likes|${profile.did}`}
|
||||
onScroll={onScroll}
|
||||
headerHeight={headerHeight}
|
||||
@@ -343,7 +286,6 @@ function ProfileScreenLoaded({
|
||||
{showFeedsTab
|
||||
? ({onScroll, headerHeight, isFocused, scrollElRef}) => (
|
||||
<ProfileFeedgens
|
||||
ref={feedsSectionRef}
|
||||
did={profile.did}
|
||||
scrollElRef={
|
||||
scrollElRef as React.MutableRefObject<FlatList<any> | null>
|
||||
@@ -358,7 +300,6 @@ function ProfileScreenLoaded({
|
||||
{showListsTab
|
||||
? ({onScroll, headerHeight, isFocused, scrollElRef}) => (
|
||||
<ProfileLists
|
||||
ref={listsSectionRef}
|
||||
did={profile.did}
|
||||
scrollElRef={
|
||||
scrollElRef as React.MutableRefObject<FlatList<any> | null>
|
||||
@@ -371,16 +312,14 @@ function ProfileScreenLoaded({
|
||||
)
|
||||
: null}
|
||||
</PagerWithHeader>
|
||||
{hasSession && (
|
||||
<FAB
|
||||
testID="composeFAB"
|
||||
onPress={onPressCompose}
|
||||
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`New post`)}
|
||||
accessibilityHint=""
|
||||
/>
|
||||
)}
|
||||
<FAB
|
||||
testID="composeFAB"
|
||||
onPress={onPressCompose}
|
||||
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`New post`)}
|
||||
accessibilityHint=""
|
||||
/>
|
||||
</ScreenHider>
|
||||
)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user