Merge remote-tracking branch 'upstream/main' into Improve-notification-localization
This commit is contained in:
+20
-1
@@ -31,7 +31,7 @@ import {
|
||||
} from 'lib/routes/types'
|
||||
import {RouteParams, State} from 'lib/routes/types'
|
||||
import {bskyTitle} from 'lib/strings/headings'
|
||||
import {isAndroid, isNative} from 'platform/detection'
|
||||
import {isAndroid, isNative, isWeb} from 'platform/detection'
|
||||
import {PreferencesExternalEmbeds} from '#/view/screens/PreferencesExternalEmbeds'
|
||||
import {AppPasswords} from 'view/screens/AppPasswords'
|
||||
import {ModerationBlockedAccounts} from 'view/screens/ModerationBlockedAccounts'
|
||||
@@ -39,6 +39,7 @@ import {ModerationMutedAccounts} from 'view/screens/ModerationMutedAccounts'
|
||||
import {PreferencesFollowingFeed} from 'view/screens/PreferencesFollowingFeed'
|
||||
import {PreferencesThreads} from 'view/screens/PreferencesThreads'
|
||||
import {SavedFeeds} from 'view/screens/SavedFeeds'
|
||||
import {SharedPreferencesTesterScreen} from '#/screens/E2E/SharedPreferencesTesterScreen'
|
||||
import HashtagScreen from '#/screens/Hashtag'
|
||||
import {ModerationScreen} from '#/screens/Moderation'
|
||||
import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers'
|
||||
@@ -48,6 +49,7 @@ import {
|
||||
StarterPackScreenShort,
|
||||
} from '#/screens/StarterPack/StarterPackScreen'
|
||||
import {Wizard} from '#/screens/StarterPack/Wizard'
|
||||
import {Referrer} from '../modules/expo-bluesky-swiss-army'
|
||||
import {init as initAnalytics} from './lib/analytics/analytics'
|
||||
import {useWebScrollRestoration} from './lib/hooks/useWebScrollRestoration'
|
||||
import {attachRouteToLogEvents, logEvent} from './lib/statsig/statsig'
|
||||
@@ -233,6 +235,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
||||
getComponent={() => DebugModScreen}
|
||||
options={{title: title(msg`Moderation states`), requireAuth: true}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="SharedPreferencesTester"
|
||||
getComponent={() => SharedPreferencesTesterScreen}
|
||||
options={{title: title(msg`Shared Preferences Tester`)}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Log"
|
||||
getComponent={() => LogScreen}
|
||||
@@ -763,6 +770,18 @@ function logModuleInitTime() {
|
||||
initMs,
|
||||
})
|
||||
|
||||
if (isWeb) {
|
||||
Referrer.getReferrerInfoAsync().then(info => {
|
||||
if (info && info.hostname !== 'bsky.app') {
|
||||
logEvent('deepLink:referrerReceived', {
|
||||
to: window.location.href,
|
||||
referrer: info?.referrer,
|
||||
hostname: info?.hostname,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
// This log is noisy, so keep false committed
|
||||
const shouldLog = false
|
||||
|
||||
+16
-5
@@ -1,7 +1,9 @@
|
||||
import React from 'react'
|
||||
import {Dimensions} from 'react-native'
|
||||
|
||||
import * as themes from '#/alf/themes'
|
||||
import {createThemes, defaultTheme} from '#/alf/themes'
|
||||
import {Theme, ThemeName} from '#/alf/types'
|
||||
import {BLUE_HUE, GREEN_HUE, RED_HUE} from '#/alf/util/colorGeneration'
|
||||
|
||||
export {atoms} from '#/alf/atoms'
|
||||
export * as tokens from '#/alf/tokens'
|
||||
@@ -39,8 +41,8 @@ function getActiveBreakpoints({width}: {width: number}) {
|
||||
* Context
|
||||
*/
|
||||
export const Context = React.createContext<{
|
||||
themeName: themes.ThemeName
|
||||
theme: themes.Theme
|
||||
themeName: ThemeName
|
||||
theme: Theme
|
||||
breakpoints: {
|
||||
active: BreakpointName | undefined
|
||||
gtPhone: boolean
|
||||
@@ -49,7 +51,7 @@ export const Context = React.createContext<{
|
||||
}
|
||||
}>({
|
||||
themeName: 'light',
|
||||
theme: themes.light,
|
||||
theme: defaultTheme,
|
||||
breakpoints: {
|
||||
active: undefined,
|
||||
gtPhone: false,
|
||||
@@ -61,7 +63,16 @@ export const Context = React.createContext<{
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
theme: themeName,
|
||||
}: React.PropsWithChildren<{theme: themes.ThemeName}>) {
|
||||
}: React.PropsWithChildren<{theme: ThemeName}>) {
|
||||
const themes = React.useMemo(() => {
|
||||
return createThemes({
|
||||
hues: {
|
||||
primary: BLUE_HUE,
|
||||
negative: RED_HUE,
|
||||
positive: GREEN_HUE,
|
||||
},
|
||||
})
|
||||
}, [])
|
||||
const theme = themes[themeName]
|
||||
const [breakpoints, setBreakpoints] = React.useState(() =>
|
||||
getActiveBreakpoints({width: Dimensions.get('window').width}),
|
||||
|
||||
+569
-447
File diff suppressed because it is too large
Load Diff
@@ -1,77 +1,6 @@
|
||||
import {
|
||||
BLUE_HUE,
|
||||
generateScale,
|
||||
GREEN_HUE,
|
||||
RED_HUE,
|
||||
} from '#/alf/util/colorGeneration'
|
||||
|
||||
export const scale = generateScale(6, 100)
|
||||
// dim shifted 6% lighter
|
||||
export const dimScale = generateScale(12, 100)
|
||||
|
||||
export const color = {
|
||||
trueBlack: '#000000',
|
||||
|
||||
temp_purple: 'rgb(105 0 255)',
|
||||
temp_purple_dark: 'rgb(83 0 202)',
|
||||
|
||||
gray_0: `hsl(${BLUE_HUE}, 20%, ${scale[14]}%)`,
|
||||
gray_25: `hsl(${BLUE_HUE}, 20%, ${scale[13]}%)`,
|
||||
gray_50: `hsl(${BLUE_HUE}, 20%, ${scale[12]}%)`,
|
||||
gray_100: `hsl(${BLUE_HUE}, 20%, ${scale[11]}%)`,
|
||||
gray_200: `hsl(${BLUE_HUE}, 20%, ${scale[10]}%)`,
|
||||
gray_300: `hsl(${BLUE_HUE}, 20%, ${scale[9]}%)`,
|
||||
gray_400: `hsl(${BLUE_HUE}, 20%, ${scale[8]}%)`,
|
||||
gray_500: `hsl(${BLUE_HUE}, 20%, ${scale[7]}%)`,
|
||||
gray_600: `hsl(${BLUE_HUE}, 24%, ${scale[6]}%)`,
|
||||
gray_700: `hsl(${BLUE_HUE}, 24%, ${scale[5]}%)`,
|
||||
gray_800: `hsl(${BLUE_HUE}, 28%, ${scale[4]}%)`,
|
||||
gray_900: `hsl(${BLUE_HUE}, 28%, ${scale[3]}%)`,
|
||||
gray_950: `hsl(${BLUE_HUE}, 28%, ${scale[2]}%)`,
|
||||
gray_975: `hsl(${BLUE_HUE}, 28%, ${scale[1]}%)`,
|
||||
gray_1000: `hsl(${BLUE_HUE}, 28%, ${scale[0]}%)`,
|
||||
|
||||
blue_25: `hsl(${BLUE_HUE}, 99%, 97%)`,
|
||||
blue_50: `hsl(${BLUE_HUE}, 99%, 95%)`,
|
||||
blue_100: `hsl(${BLUE_HUE}, 99%, 90%)`,
|
||||
blue_200: `hsl(${BLUE_HUE}, 99%, 80%)`,
|
||||
blue_300: `hsl(${BLUE_HUE}, 99%, 70%)`,
|
||||
blue_400: `hsl(${BLUE_HUE}, 99%, 60%)`,
|
||||
blue_500: `hsl(${BLUE_HUE}, 99%, 53%)`,
|
||||
blue_600: `hsl(${BLUE_HUE}, 99%, 42%)`,
|
||||
blue_700: `hsl(${BLUE_HUE}, 99%, 34%)`,
|
||||
blue_800: `hsl(${BLUE_HUE}, 99%, 26%)`,
|
||||
blue_900: `hsl(${BLUE_HUE}, 99%, 18%)`,
|
||||
blue_950: `hsl(${BLUE_HUE}, 99%, 10%)`,
|
||||
blue_975: `hsl(${BLUE_HUE}, 99%, 7%)`,
|
||||
|
||||
green_25: `hsl(${GREEN_HUE}, 82%, 97%)`,
|
||||
green_50: `hsl(${GREEN_HUE}, 82%, 95%)`,
|
||||
green_100: `hsl(${GREEN_HUE}, 82%, 90%)`,
|
||||
green_200: `hsl(${GREEN_HUE}, 82%, 80%)`,
|
||||
green_300: `hsl(${GREEN_HUE}, 82%, 70%)`,
|
||||
green_400: `hsl(${GREEN_HUE}, 82%, 60%)`,
|
||||
green_500: `hsl(${GREEN_HUE}, 82%, 50%)`,
|
||||
green_600: `hsl(${GREEN_HUE}, 82%, 42%)`,
|
||||
green_700: `hsl(${GREEN_HUE}, 82%, 34%)`,
|
||||
green_800: `hsl(${GREEN_HUE}, 82%, 26%)`,
|
||||
green_900: `hsl(${GREEN_HUE}, 82%, 18%)`,
|
||||
green_950: `hsl(${GREEN_HUE}, 82%, 10%)`,
|
||||
green_975: `hsl(${GREEN_HUE}, 82%, 7%)`,
|
||||
|
||||
red_25: `hsl(${RED_HUE}, 91%, 97%)`,
|
||||
red_50: `hsl(${RED_HUE}, 91%, 95%)`,
|
||||
red_100: `hsl(${RED_HUE}, 91%, 90%)`,
|
||||
red_200: `hsl(${RED_HUE}, 91%, 80%)`,
|
||||
red_300: `hsl(${RED_HUE}, 91%, 70%)`,
|
||||
red_400: `hsl(${RED_HUE}, 91%, 60%)`,
|
||||
red_500: `hsl(${RED_HUE}, 91%, 50%)`,
|
||||
red_600: `hsl(${RED_HUE}, 91%, 42%)`,
|
||||
red_700: `hsl(${RED_HUE}, 91%, 34%)`,
|
||||
red_800: `hsl(${RED_HUE}, 91%, 26%)`,
|
||||
red_900: `hsl(${RED_HUE}, 91%, 18%)`,
|
||||
red_950: `hsl(${RED_HUE}, 91%, 10%)`,
|
||||
red_975: `hsl(${RED_HUE}, 91%, 7%)`,
|
||||
} as const
|
||||
|
||||
export const space = {
|
||||
@@ -178,10 +107,3 @@ export const gradients = {
|
||||
hover_value: '#755B62',
|
||||
},
|
||||
} as const
|
||||
|
||||
export type Color = keyof typeof color
|
||||
export type Space = keyof typeof space
|
||||
export type FontSize = keyof typeof fontSize
|
||||
export type LineHeight = keyof typeof lineHeight
|
||||
export type BorderRadius = keyof typeof borderRadius
|
||||
export type FontWeight = keyof typeof fontWeight
|
||||
|
||||
+154
-18
@@ -1,21 +1,4 @@
|
||||
import {StyleProp, ViewStyle, TextStyle} from 'react-native'
|
||||
|
||||
type LiteralToCommon<T extends PropertyKey> = T extends number
|
||||
? number
|
||||
: T extends string
|
||||
? string
|
||||
: T extends symbol
|
||||
? symbol
|
||||
: never
|
||||
|
||||
/**
|
||||
* @see https://stackoverflow.com/questions/68249999/use-as-const-in-typescript-without-adding-readonly-modifiers
|
||||
*/
|
||||
export type Mutable<T> = {
|
||||
-readonly [K in keyof T]: T[K] extends PropertyKey
|
||||
? LiteralToCommon<T[K]>
|
||||
: Mutable<T[K]>
|
||||
}
|
||||
import {StyleProp, TextStyle, ViewStyle} from 'react-native'
|
||||
|
||||
export type TextStyleProp = {
|
||||
style?: StyleProp<TextStyle>
|
||||
@@ -24,3 +7,156 @@ export type TextStyleProp = {
|
||||
export type ViewStyleProp = {
|
||||
style?: StyleProp<ViewStyle>
|
||||
}
|
||||
|
||||
export type ThemeName = 'light' | 'dim' | 'dark'
|
||||
export type Palette = {
|
||||
white: string
|
||||
black: string
|
||||
|
||||
contrast_25: string
|
||||
contrast_50: string
|
||||
contrast_100: string
|
||||
contrast_200: string
|
||||
contrast_300: string
|
||||
contrast_400: string
|
||||
contrast_500: string
|
||||
contrast_600: string
|
||||
contrast_700: string
|
||||
contrast_800: string
|
||||
contrast_900: string
|
||||
contrast_950: string
|
||||
contrast_975: string
|
||||
|
||||
primary_25: string
|
||||
primary_50: string
|
||||
primary_100: string
|
||||
primary_200: string
|
||||
primary_300: string
|
||||
primary_400: string
|
||||
primary_500: string
|
||||
primary_600: string
|
||||
primary_700: string
|
||||
primary_800: string
|
||||
primary_900: string
|
||||
primary_950: string
|
||||
primary_975: string
|
||||
|
||||
positive_25: string
|
||||
positive_50: string
|
||||
positive_100: string
|
||||
positive_200: string
|
||||
positive_300: string
|
||||
positive_400: string
|
||||
positive_500: string
|
||||
positive_600: string
|
||||
positive_700: string
|
||||
positive_800: string
|
||||
positive_900: string
|
||||
positive_950: string
|
||||
positive_975: string
|
||||
|
||||
negative_25: string
|
||||
negative_50: string
|
||||
negative_100: string
|
||||
negative_200: string
|
||||
negative_300: string
|
||||
negative_400: string
|
||||
negative_500: string
|
||||
negative_600: string
|
||||
negative_700: string
|
||||
negative_800: string
|
||||
negative_900: string
|
||||
negative_950: string
|
||||
negative_975: string
|
||||
}
|
||||
export type ThemedAtoms = {
|
||||
text: {
|
||||
color: string
|
||||
}
|
||||
text_contrast_low: {
|
||||
color: string
|
||||
}
|
||||
text_contrast_medium: {
|
||||
color: string
|
||||
}
|
||||
text_contrast_high: {
|
||||
color: string
|
||||
}
|
||||
text_inverted: {
|
||||
color: string
|
||||
}
|
||||
bg: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_25: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_50: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_100: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_200: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_300: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_400: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_500: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_600: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_700: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_800: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_900: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_950: {
|
||||
backgroundColor: string
|
||||
}
|
||||
bg_contrast_975: {
|
||||
backgroundColor: string
|
||||
}
|
||||
border_contrast_low: {
|
||||
borderColor: string
|
||||
}
|
||||
border_contrast_medium: {
|
||||
borderColor: string
|
||||
}
|
||||
border_contrast_high: {
|
||||
borderColor: string
|
||||
}
|
||||
shadow_sm: {
|
||||
shadowRadius: number
|
||||
shadowOpacity: number
|
||||
elevation: number
|
||||
shadowColor: string
|
||||
}
|
||||
shadow_md: {
|
||||
shadowRadius: number
|
||||
shadowOpacity: number
|
||||
elevation: number
|
||||
shadowColor: string
|
||||
}
|
||||
shadow_lg: {
|
||||
shadowRadius: number
|
||||
shadowOpacity: number
|
||||
elevation: number
|
||||
shadowColor: string
|
||||
}
|
||||
}
|
||||
export type Theme = {
|
||||
name: ThemeName
|
||||
palette: Palette
|
||||
atoms: ThemedAtoms
|
||||
}
|
||||
|
||||
@@ -15,3 +15,7 @@ export function generateScale(start: number, end: number) {
|
||||
return start + range * stop
|
||||
})
|
||||
}
|
||||
|
||||
export const defaultScale = generateScale(6, 100)
|
||||
// dim shifted 6% lighter
|
||||
export const dimScale = generateScale(12, 100)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ThemeName} from '#/alf/themes'
|
||||
import {ThemeName} from '#/alf/types'
|
||||
|
||||
export function select<T>(name: ThemeName, options: Record<ThemeName, T>) {
|
||||
switch (name) {
|
||||
|
||||
@@ -4,7 +4,8 @@ import * as SystemUI from 'expo-system-ui'
|
||||
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {useThemePrefs} from 'state/shell'
|
||||
import {dark, dim, light, ThemeName} from '#/alf/themes'
|
||||
import {dark, dim, light} from '#/alf/themes'
|
||||
import {ThemeName} from '#/alf/types'
|
||||
|
||||
export function useColorModeTheme(): ThemeName {
|
||||
const theme = useThemeName()
|
||||
|
||||
+34
-28
@@ -13,7 +13,7 @@ import {
|
||||
} from 'react-native'
|
||||
import {LinearGradient} from 'expo-linear-gradient'
|
||||
|
||||
import {android, atoms as a, flatten, tokens, useTheme} from '#/alf'
|
||||
import {android, atoms as a, flatten, select, tokens, useTheme} from '#/alf'
|
||||
import {Props as SVGIconProps} from '#/components/icons/common'
|
||||
import {normalizeTextStyles} from '#/components/Typography'
|
||||
|
||||
@@ -152,7 +152,6 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
const {baseStyles, hoverStyles} = React.useMemo(() => {
|
||||
const baseStyles: ViewStyle[] = []
|
||||
const hoverStyles: ViewStyle[] = []
|
||||
const light = t.name === 'light'
|
||||
|
||||
if (color === 'primary') {
|
||||
if (variant === 'solid') {
|
||||
@@ -165,7 +164,11 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({
|
||||
backgroundColor: t.palette.primary_700,
|
||||
backgroundColor: select(t.name, {
|
||||
light: t.palette.primary_700,
|
||||
dim: t.palette.primary_300,
|
||||
dark: t.palette.primary_300,
|
||||
}),
|
||||
})
|
||||
}
|
||||
} else if (variant === 'outline') {
|
||||
@@ -178,24 +181,18 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
borderColor: t.palette.primary_500,
|
||||
})
|
||||
hoverStyles.push(a.border, {
|
||||
backgroundColor: light
|
||||
? t.palette.primary_50
|
||||
: t.palette.primary_950,
|
||||
backgroundColor: t.palette.primary_50,
|
||||
})
|
||||
} else {
|
||||
baseStyles.push(a.border, {
|
||||
borderColor: light
|
||||
? t.palette.primary_200
|
||||
: t.palette.primary_900,
|
||||
borderColor: t.palette.primary_200,
|
||||
})
|
||||
}
|
||||
} else if (variant === 'ghost') {
|
||||
if (!disabled) {
|
||||
baseStyles.push(t.atoms.bg)
|
||||
hoverStyles.push({
|
||||
backgroundColor: light
|
||||
? t.palette.primary_100
|
||||
: t.palette.primary_900,
|
||||
backgroundColor: t.palette.primary_100,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -203,14 +200,26 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
if (variant === 'solid') {
|
||||
if (!disabled) {
|
||||
baseStyles.push({
|
||||
backgroundColor: t.palette.contrast_25,
|
||||
backgroundColor: select(t.name, {
|
||||
light: t.palette.contrast_25,
|
||||
dim: t.palette.contrast_100,
|
||||
dark: t.palette.contrast_100,
|
||||
}),
|
||||
})
|
||||
hoverStyles.push({
|
||||
backgroundColor: t.palette.contrast_50,
|
||||
backgroundColor: select(t.name, {
|
||||
light: t.palette.contrast_50,
|
||||
dim: t.palette.contrast_200,
|
||||
dark: t.palette.contrast_200,
|
||||
}),
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({
|
||||
backgroundColor: t.palette.contrast_100,
|
||||
backgroundColor: select(t.name, {
|
||||
light: t.palette.contrast_100,
|
||||
dim: t.palette.contrast_25,
|
||||
dark: t.palette.contrast_25,
|
||||
}),
|
||||
})
|
||||
}
|
||||
} else if (variant === 'outline') {
|
||||
@@ -247,7 +256,7 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({
|
||||
backgroundColor: t.palette.contrast_700,
|
||||
backgroundColor: t.palette.contrast_600,
|
||||
})
|
||||
}
|
||||
} else if (variant === 'outline') {
|
||||
@@ -284,7 +293,11 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({
|
||||
backgroundColor: t.palette.negative_700,
|
||||
backgroundColor: select(t.name, {
|
||||
light: t.palette.negative_700,
|
||||
dim: t.palette.negative_300,
|
||||
dark: t.palette.negative_300,
|
||||
}),
|
||||
})
|
||||
}
|
||||
} else if (variant === 'outline') {
|
||||
@@ -297,24 +310,18 @@ export const Button = React.forwardRef<View, ButtonProps>(
|
||||
borderColor: t.palette.negative_500,
|
||||
})
|
||||
hoverStyles.push(a.border, {
|
||||
backgroundColor: light
|
||||
? t.palette.negative_50
|
||||
: t.palette.negative_975,
|
||||
backgroundColor: t.palette.negative_50,
|
||||
})
|
||||
} else {
|
||||
baseStyles.push(a.border, {
|
||||
borderColor: light
|
||||
? t.palette.negative_200
|
||||
: t.palette.negative_900,
|
||||
borderColor: t.palette.negative_200,
|
||||
})
|
||||
}
|
||||
} else if (variant === 'ghost') {
|
||||
if (!disabled) {
|
||||
baseStyles.push(t.atoms.bg)
|
||||
hoverStyles.push({
|
||||
backgroundColor: light
|
||||
? t.palette.negative_100
|
||||
: t.palette.negative_975,
|
||||
backgroundColor: t.palette.negative_100,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -482,7 +489,6 @@ export function useSharedButtonTextStyles() {
|
||||
const {color, variant, disabled, size} = useButtonContext()
|
||||
return React.useMemo(() => {
|
||||
const baseStyles: TextStyle[] = []
|
||||
const light = t.name === 'light'
|
||||
|
||||
if (color === 'primary') {
|
||||
if (variant === 'solid') {
|
||||
@@ -494,7 +500,7 @@ export function useSharedButtonTextStyles() {
|
||||
} else if (variant === 'outline') {
|
||||
if (!disabled) {
|
||||
baseStyles.push({
|
||||
color: light ? t.palette.primary_600 : t.palette.primary_500,
|
||||
color: t.palette.primary_600,
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({color: t.palette.primary_600, opacity: 0.5})
|
||||
|
||||
@@ -13,6 +13,7 @@ import {logger} from '#/logger'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useGetPopularFeedsQuery} from '#/state/queries/feed'
|
||||
import {useProfilesQuery} from '#/state/queries/profile'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useProgressGuide} from '#/state/shell/progress-guide'
|
||||
import * as userActionHistory from '#/state/userActionHistory'
|
||||
import {SeenPost} from '#/state/userActionHistory'
|
||||
@@ -127,6 +128,7 @@ function sortSeenPosts(postA: SeenPost, postB: SeenPost): 0 | 1 | -1 {
|
||||
}
|
||||
|
||||
function useExperimentalSuggestedUsersQuery() {
|
||||
const {currentAccount} = useSession()
|
||||
const userActionSnapshot = userActionHistory.useActionHistorySnapshot()
|
||||
const dids = React.useMemo(() => {
|
||||
const {likes, follows, seen} = userActionSnapshot
|
||||
@@ -138,8 +140,10 @@ function useExperimentalSuggestedUsersQuery() {
|
||||
.sort(sortSeenPosts)
|
||||
.map(l => new AtUri(l.uri))
|
||||
.map(uri => uri.host)
|
||||
return [...new Set([...likeDids, ...seenDids])]
|
||||
}, [userActionSnapshot])
|
||||
return [...new Set([...likeDids, ...seenDids])].filter(
|
||||
did => did !== currentAccount?.did,
|
||||
)
|
||||
}, [userActionSnapshot, currentAccount])
|
||||
const {data, isLoading, error} = useProfilesQuery({
|
||||
handles: dids.slice(0, 16),
|
||||
})
|
||||
|
||||
@@ -2,14 +2,14 @@ import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
|
||||
import {
|
||||
useTheme,
|
||||
atoms as a,
|
||||
ViewStyleProp,
|
||||
TextStyleProp,
|
||||
flatten,
|
||||
TextStyleProp,
|
||||
useTheme,
|
||||
ViewStyleProp,
|
||||
} from '#/alf'
|
||||
import {Growth_Stroke2_Corner0_Rounded as Growth} from '#/components/icons/Growth'
|
||||
import {Props} from '#/components/icons/common'
|
||||
import {Growth_Stroke2_Corner0_Rounded as Growth} from '#/components/icons/Growth'
|
||||
|
||||
export function IconCircle({
|
||||
icon: Icon,
|
||||
@@ -32,8 +32,7 @@ export function IconCircle({
|
||||
{
|
||||
width: size === 'lg' ? 52 : 64,
|
||||
height: size === 'lg' ? 52 : 64,
|
||||
backgroundColor:
|
||||
t.name === 'light' ? t.palette.primary_50 : t.palette.primary_950,
|
||||
backgroundColor: t.palette.primary_50,
|
||||
},
|
||||
flatten(style),
|
||||
]}>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {Link, LinkProps} from '#/components/Link'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
const AVI_SIZE = 30
|
||||
const AVI_SIZE_SMALL = 20
|
||||
const AVI_BORDER = 1
|
||||
|
||||
/**
|
||||
@@ -30,10 +31,12 @@ export function KnownFollowers({
|
||||
profile,
|
||||
moderationOpts,
|
||||
onLinkPress,
|
||||
minimal,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
moderationOpts: ModerationOpts
|
||||
onLinkPress?: LinkProps['onPress']
|
||||
minimal?: boolean
|
||||
}) {
|
||||
const cache = React.useRef<Map<string, AppBskyActorDefs.KnownFollowers>>(
|
||||
new Map(),
|
||||
@@ -59,6 +62,7 @@ export function KnownFollowers({
|
||||
cachedKnownFollowers={cachedKnownFollowers}
|
||||
moderationOpts={moderationOpts}
|
||||
onLinkPress={onLinkPress}
|
||||
minimal={minimal}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -71,11 +75,13 @@ function KnownFollowersInner({
|
||||
moderationOpts,
|
||||
cachedKnownFollowers,
|
||||
onLinkPress,
|
||||
minimal,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
moderationOpts: ModerationOpts
|
||||
cachedKnownFollowers: AppBskyActorDefs.KnownFollowers
|
||||
onLinkPress?: LinkProps['onPress']
|
||||
minimal?: boolean
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
@@ -110,6 +116,8 @@ function KnownFollowersInner({
|
||||
*/
|
||||
if (slice.length === 0) return null
|
||||
|
||||
const SIZE = minimal ? AVI_SIZE_SMALL : AVI_SIZE
|
||||
|
||||
return (
|
||||
<Link
|
||||
label={_(
|
||||
@@ -120,7 +128,7 @@ function KnownFollowersInner({
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.flex_row,
|
||||
a.gap_md,
|
||||
minimal ? a.gap_sm : a.gap_md,
|
||||
a.align_center,
|
||||
{marginLeft: -AVI_BORDER},
|
||||
]}>
|
||||
@@ -129,8 +137,8 @@ function KnownFollowersInner({
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
height: AVI_SIZE,
|
||||
width: AVI_SIZE + (slice.length - 1) * a.gap_md.gap,
|
||||
height: SIZE,
|
||||
width: SIZE + (slice.length - 1) * a.gap_md.gap,
|
||||
},
|
||||
pressed && {
|
||||
opacity: 0.5,
|
||||
@@ -145,14 +153,14 @@ function KnownFollowersInner({
|
||||
{
|
||||
borderWidth: AVI_BORDER,
|
||||
borderColor: t.atoms.bg.backgroundColor,
|
||||
width: AVI_SIZE + AVI_BORDER * 2,
|
||||
height: AVI_SIZE + AVI_BORDER * 2,
|
||||
width: SIZE + AVI_BORDER * 2,
|
||||
height: SIZE + AVI_BORDER * 2,
|
||||
left: i * a.gap_md.gap,
|
||||
zIndex: AVI_BORDER - i,
|
||||
},
|
||||
]}>
|
||||
<UserAvatar
|
||||
size={AVI_SIZE}
|
||||
size={SIZE}
|
||||
avatar={prof.avatar}
|
||||
moderation={moderation.ui('avatar')}
|
||||
/>
|
||||
|
||||
@@ -166,7 +166,9 @@ export function NameAndHandle({
|
||||
|
||||
return (
|
||||
<View style={[a.flex_1]}>
|
||||
<Text style={[a.text_md, a.font_bold, a.leading_snug]} numberOfLines={1}>
|
||||
<Text
|
||||
style={[a.text_md, a.font_bold, a.leading_snug, a.self_start]}
|
||||
numberOfLines={1}>
|
||||
{name}
|
||||
</Text>
|
||||
<Text
|
||||
|
||||
@@ -462,7 +462,8 @@ function Inner({
|
||||
|
||||
<Link to={profileURL} label={_(msg`View profile`)} onPress={hide}>
|
||||
<View style={[a.pb_sm, a.flex_1]}>
|
||||
<Text style={[a.pt_md, a.pb_xs, a.text_lg, a.font_bold]}>
|
||||
<Text
|
||||
style={[a.pt_md, a.pb_xs, a.text_lg, a.font_bold, a.self_start]}>
|
||||
{sanitizeDisplayName(
|
||||
profile.displayName || sanitizeHandle(profile.handle),
|
||||
moderation.ui('displayName'),
|
||||
|
||||
@@ -6,6 +6,7 @@ import {useLingui} from '@lingui/react'
|
||||
|
||||
import {getLabelingServiceTitle} from '#/lib/moderation'
|
||||
import {ReportOption} from '#/lib/moderation/useReportOptions'
|
||||
import {useGate} from '#/lib/statsig/statsig'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
@@ -36,6 +37,7 @@ export function SubmitView({
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const agent = useAgent()
|
||||
const gate = useGate()
|
||||
const [details, setDetails] = React.useState<string>('')
|
||||
const [submitting, setSubmitting] = React.useState<boolean>(false)
|
||||
const [selectedServices, setSelectedServices] = React.useState<string[]>([
|
||||
@@ -60,15 +62,29 @@ export function SubmitView({
|
||||
reason: details,
|
||||
}
|
||||
const results = await Promise.all(
|
||||
selectedServices.map(did =>
|
||||
agent
|
||||
.withProxy('atproto_labeler', did)
|
||||
.createModerationReport(report)
|
||||
.then(
|
||||
_ => true,
|
||||
_ => false,
|
||||
),
|
||||
),
|
||||
selectedServices.map(did => {
|
||||
if (gate('session_withproxy_fix')) {
|
||||
return agent
|
||||
.createModerationReport(report, {
|
||||
encoding: 'application/json',
|
||||
headers: {
|
||||
'atproto-proxy': `${did}#atproto_labeler`,
|
||||
},
|
||||
})
|
||||
.then(
|
||||
_ => true,
|
||||
_ => false,
|
||||
)
|
||||
} else {
|
||||
return agent
|
||||
.withProxy('atproto_labeler', did)
|
||||
.createModerationReport(report)
|
||||
.then(
|
||||
_ => true,
|
||||
_ => false,
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
setSubmitting(false)
|
||||
@@ -92,6 +108,7 @@ export function SubmitView({
|
||||
onSubmitComplete,
|
||||
setError,
|
||||
agent,
|
||||
gate,
|
||||
])
|
||||
|
||||
return (
|
||||
|
||||
@@ -110,7 +110,7 @@ export function QrCodeInner({link}: {link: string}) {
|
||||
innerEyesOptions={{borderRadius: 3}}
|
||||
logo={{
|
||||
href: require('../../../assets/logo.png'),
|
||||
scale: 1.2,
|
||||
scale: 0.95,
|
||||
padding: 2,
|
||||
hidePieces: true,
|
||||
}}
|
||||
|
||||
@@ -78,7 +78,13 @@ function WizardListCard({
|
||||
/>
|
||||
<View style={[a.flex_1, a.gap_2xs]}>
|
||||
<Text
|
||||
style={[a.flex_1, a.font_bold, a.text_md, a.leading_tight]}
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.font_bold,
|
||||
a.text_md,
|
||||
a.leading_tight,
|
||||
a.self_start,
|
||||
]}
|
||||
numberOfLines={1}>
|
||||
{displayName}
|
||||
</Text>
|
||||
|
||||
@@ -357,12 +357,11 @@ function TargetToggle({children}: React.PropsWithChildren<{}>) {
|
||||
a.px_sm,
|
||||
gtMobile && a.px_md,
|
||||
a.rounded_sm,
|
||||
t.atoms.bg_contrast_50,
|
||||
(ctx.hovered || ctx.focused) && t.atoms.bg_contrast_100,
|
||||
t.atoms.bg_contrast_25,
|
||||
(ctx.hovered || ctx.focused) && t.atoms.bg_contrast_50,
|
||||
ctx.selected && [
|
||||
{
|
||||
backgroundColor:
|
||||
t.name === 'light' ? t.palette.primary_50 : t.palette.primary_975,
|
||||
backgroundColor: t.palette.primary_50,
|
||||
},
|
||||
],
|
||||
ctx.disabled && {
|
||||
|
||||
@@ -196,10 +196,7 @@ function Selectable({
|
||||
t.atoms.bg_contrast_50,
|
||||
(hovered || focused) && t.atoms.bg_contrast_100,
|
||||
isSelected && {
|
||||
backgroundColor:
|
||||
t.name === 'light'
|
||||
? t.palette.primary_50
|
||||
: t.palette.primary_975,
|
||||
backgroundColor: t.palette.primary_100,
|
||||
},
|
||||
style,
|
||||
]}>
|
||||
|
||||
@@ -72,8 +72,7 @@ let MessageItem = ({
|
||||
lastInGroupRef.current = isLastInGroup
|
||||
}
|
||||
|
||||
const pendingColor =
|
||||
t.name === 'light' ? t.palette.primary_200 : t.palette.primary_800
|
||||
const pendingColor = t.palette.primary_200
|
||||
|
||||
const rt = useMemo(() => {
|
||||
return new RichTextAPI({text: message.text, facets: message.facets})
|
||||
@@ -110,12 +109,7 @@ let MessageItem = ({
|
||||
}>
|
||||
<RichText
|
||||
value={rt}
|
||||
style={[
|
||||
a.text_md,
|
||||
isFromSelf && {color: t.palette.white},
|
||||
isPending &&
|
||||
t.name !== 'light' && {color: t.palette.primary_300},
|
||||
]}
|
||||
style={[a.text_md, isFromSelf && {color: t.palette.white}]}
|
||||
interactiveStyle={a.underline}
|
||||
enableTags
|
||||
emojiMultiplier={3}
|
||||
|
||||
@@ -168,7 +168,12 @@ function HeaderReady({
|
||||
</View>
|
||||
<View style={a.flex_1}>
|
||||
<Text
|
||||
style={[a.text_md, a.font_bold, web(a.leading_normal)]}
|
||||
style={[
|
||||
a.text_md,
|
||||
a.font_bold,
|
||||
a.self_start,
|
||||
web(a.leading_normal),
|
||||
]}
|
||||
numberOfLines={1}>
|
||||
{displayName}
|
||||
</Text>
|
||||
|
||||
@@ -395,7 +395,7 @@ function ProfileCard({
|
||||
/>
|
||||
<View style={[a.flex_1, a.gap_2xs]}>
|
||||
<Text
|
||||
style={[t.atoms.text, a.font_bold, a.leading_tight]}
|
||||
style={[t.atoms.text, a.font_bold, a.leading_tight, a.self_start]}
|
||||
numberOfLines={1}>
|
||||
{displayName}
|
||||
</Text>
|
||||
|
||||
@@ -101,16 +101,13 @@ export function useSharedInputStyles() {
|
||||
]
|
||||
const error: ViewStyle[] = [
|
||||
{
|
||||
backgroundColor:
|
||||
t.name === 'light' ? t.palette.negative_25 : t.palette.negative_900,
|
||||
borderColor:
|
||||
t.name === 'light' ? t.palette.negative_300 : t.palette.negative_800,
|
||||
backgroundColor: t.palette.negative_25,
|
||||
borderColor: t.palette.negative_300,
|
||||
},
|
||||
]
|
||||
const errorHover: ViewStyle[] = [
|
||||
{
|
||||
backgroundColor:
|
||||
t.name === 'light' ? t.palette.negative_25 : t.palette.negative_900,
|
||||
backgroundColor: t.palette.negative_25,
|
||||
borderColor: t.palette.negative_500,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -281,24 +281,20 @@ export function createSharedToggleStyles({
|
||||
|
||||
if (selected) {
|
||||
base.push({
|
||||
backgroundColor:
|
||||
t.name === 'light' ? t.palette.primary_25 : t.palette.primary_900,
|
||||
backgroundColor: t.palette.primary_25,
|
||||
borderColor: t.palette.primary_500,
|
||||
})
|
||||
|
||||
if (hovered) {
|
||||
baseHover.push({
|
||||
backgroundColor:
|
||||
t.name === 'light' ? t.palette.primary_100 : t.palette.primary_800,
|
||||
borderColor:
|
||||
t.name === 'light' ? t.palette.primary_600 : t.palette.primary_400,
|
||||
backgroundColor: t.palette.primary_100,
|
||||
borderColor: t.palette.primary_600,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if (hovered) {
|
||||
baseHover.push({
|
||||
backgroundColor:
|
||||
t.name === 'light' ? t.palette.contrast_50 : t.palette.contrast_100,
|
||||
backgroundColor: t.palette.contrast_50,
|
||||
borderColor: t.palette.contrast_500,
|
||||
})
|
||||
}
|
||||
@@ -306,16 +302,13 @@ export function createSharedToggleStyles({
|
||||
|
||||
if (isInvalid) {
|
||||
base.push({
|
||||
backgroundColor:
|
||||
t.name === 'light' ? t.palette.negative_25 : t.palette.negative_975,
|
||||
borderColor:
|
||||
t.name === 'light' ? t.palette.negative_300 : t.palette.negative_800,
|
||||
backgroundColor: t.palette.negative_25,
|
||||
borderColor: t.palette.negative_300,
|
||||
})
|
||||
|
||||
if (hovered) {
|
||||
baseHover.push({
|
||||
backgroundColor:
|
||||
t.name === 'light' ? t.palette.negative_25 : t.palette.negative_900,
|
||||
backgroundColor: t.palette.negative_25,
|
||||
borderColor: t.palette.negative_600,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import {isAndroid} from 'platform/detection'
|
||||
import {useHasCheckedForStarterPack} from 'state/preferences/used-starter-packs'
|
||||
import {useSetActiveStarterPack} from 'state/shell/starter-pack'
|
||||
import {DevicePrefs, Referrer} from '../../../modules/expo-bluesky-swiss-army'
|
||||
import {Referrer, SharedPrefs} from '../../../modules/expo-bluesky-swiss-army'
|
||||
|
||||
export function useStarterPackEntry() {
|
||||
const [ready, setReady] = React.useState(false)
|
||||
@@ -39,14 +39,10 @@ export function useStarterPackEntry() {
|
||||
uri = createStarterPackLinkFromAndroidReferrer(res.installReferrer)
|
||||
}
|
||||
} else {
|
||||
const res = await DevicePrefs.getStringValueAsync(
|
||||
'starterPackUri',
|
||||
true,
|
||||
)
|
||||
|
||||
if (res) {
|
||||
uri = httpStarterPackUriToAtUri(res)
|
||||
DevicePrefs.setStringValueAsync('starterPackUri', null, true)
|
||||
const starterPackUri = SharedPrefs.getString('starterPackUri')
|
||||
if (starterPackUri) {
|
||||
uri = httpStarterPackUriToAtUri(starterPackUri)
|
||||
SharedPrefs.setValue('starterPackUri', null)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import type {PathProps, SvgProps} from 'react-native-svg'
|
||||
import {Defs, LinearGradient, Stop} from 'react-native-svg'
|
||||
import {nanoid} from 'nanoid/non-secure'
|
||||
|
||||
import {tokens} from '#/alf'
|
||||
import {tokens, useTheme} from '#/alf'
|
||||
|
||||
export type Props = {
|
||||
fill?: PathProps['fill']
|
||||
@@ -22,10 +22,11 @@ export const sizes = {
|
||||
}
|
||||
|
||||
export function useCommonSVGProps(props: Props) {
|
||||
const t = useTheme()
|
||||
const {fill, size, gradient, ...rest} = props
|
||||
const style = StyleSheet.flatten(rest.style)
|
||||
const _size = Number(size ? sizes[size] : rest.width || sizes.md)
|
||||
let _fill = fill || style?.color || tokens.color.blue_500
|
||||
let _fill = fill || style?.color || t.palette.primary_500
|
||||
let gradientDef = null
|
||||
|
||||
if (gradient && tokens.gradients[gradient]) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {useMutation} from '@tanstack/react-query'
|
||||
|
||||
import {useLabelInfo} from '#/lib/moderation/useLabelInfo'
|
||||
import {makeProfileLink} from '#/lib/routes/links'
|
||||
import {useGate} from '#/lib/statsig/statsig'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {logger} from '#/logger'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
@@ -201,22 +202,42 @@ function AppealForm({
|
||||
const [details, setDetails] = React.useState('')
|
||||
const isAccountReport = 'did' in subject
|
||||
const agent = useAgent()
|
||||
const gate = useGate()
|
||||
|
||||
const {mutate, isPending} = useMutation({
|
||||
mutationFn: async () => {
|
||||
const $type = !isAccountReport
|
||||
? 'com.atproto.repo.strongRef'
|
||||
: 'com.atproto.admin.defs#repoRef'
|
||||
await agent
|
||||
.withProxy('atproto_labeler', label.src)
|
||||
.createModerationReport({
|
||||
reasonType: ComAtprotoModerationDefs.REASONAPPEAL,
|
||||
subject: {
|
||||
$type,
|
||||
...subject,
|
||||
if (gate('session_withproxy_fix')) {
|
||||
await agent.createModerationReport(
|
||||
{
|
||||
reasonType: ComAtprotoModerationDefs.REASONAPPEAL,
|
||||
subject: {
|
||||
$type,
|
||||
...subject,
|
||||
},
|
||||
reason: details,
|
||||
},
|
||||
reason: details,
|
||||
})
|
||||
{
|
||||
encoding: 'application/json',
|
||||
headers: {
|
||||
'atproto-proxy': `${label.src}#atproto_labeler`,
|
||||
},
|
||||
},
|
||||
)
|
||||
} else {
|
||||
await agent
|
||||
.withProxy('atproto_labeler', label.src)
|
||||
.createModerationReport({
|
||||
reasonType: ComAtprotoModerationDefs.REASONAPPEAL,
|
||||
subject: {
|
||||
$type,
|
||||
...subject,
|
||||
},
|
||||
reason: details,
|
||||
})
|
||||
}
|
||||
},
|
||||
onError: err => {
|
||||
logger.error('Failed to submit label appeal', {message: err})
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, {ReactNode, createContext, useContext} from 'react'
|
||||
import React, {createContext, ReactNode, useContext} from 'react'
|
||||
import {TextStyle, ViewStyle} from 'react-native'
|
||||
|
||||
import {ThemeName} from '#/alf/types'
|
||||
import {darkTheme, defaultTheme, dimTheme} from './themes'
|
||||
import {ThemeName} from '#/alf/themes'
|
||||
|
||||
export type ColorScheme = 'light' | 'dark'
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import React from 'react'
|
||||
import * as Linking from 'expo-linking'
|
||||
|
||||
import {logEvent} from 'lib/statsig/statsig'
|
||||
import {isNative} from 'platform/detection'
|
||||
import {useComposerControls} from 'state/shell'
|
||||
import {useSession} from 'state/session'
|
||||
import {useComposerControls} from 'state/shell'
|
||||
import {useCloseAllActiveElements} from 'state/util'
|
||||
import {Referrer} from '../../../modules/expo-bluesky-swiss-army'
|
||||
|
||||
type IntentType = 'compose'
|
||||
|
||||
@@ -15,6 +18,16 @@ export function useIntentHandler() {
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleIncomingURL = (url: string) => {
|
||||
Referrer.getReferrerInfoAsync().then(info => {
|
||||
if (info && info.hostname !== 'bsky.app') {
|
||||
logEvent('deepLink:referrerReceived', {
|
||||
to: url,
|
||||
referrer: info?.referrer,
|
||||
hostname: info?.hostname,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// We want to be able to support bluesky:// deeplinks. It's unnatural for someone to use a deeplink with three
|
||||
// slashes, like bluesky:///intent/follow. However, supporting just two slashes causes us to have to take care
|
||||
// of two cases when parsing the url. If we ensure there is a third slash, we can always ensure the first
|
||||
|
||||
@@ -25,6 +25,7 @@ export type CommonNavigatorParams = {
|
||||
ProfileLabelerLikedBy: {name: string}
|
||||
Debug: undefined
|
||||
DebugMod: undefined
|
||||
SharedPreferencesTester: undefined
|
||||
Log: undefined
|
||||
Support: undefined
|
||||
PrivacyPolicy: undefined
|
||||
|
||||
@@ -25,6 +25,11 @@ export type LogEvents = {
|
||||
}
|
||||
'state:foreground:sampled': {}
|
||||
'router:navigate:sampled': {}
|
||||
'deepLink:referrerReceived': {
|
||||
to: string
|
||||
referrer: string
|
||||
hostname: string
|
||||
}
|
||||
|
||||
// Screen events
|
||||
'splash:signInPressed': {}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
export type Gate =
|
||||
// Keep this alphabetic please.
|
||||
| 'debug_show_feedcontext'
|
||||
| 'explore_page_profile_card_social_proof'
|
||||
| 'native_pwi_disabled'
|
||||
| 'new_user_guided_tour'
|
||||
| 'new_user_progress_guide'
|
||||
| 'onboarding_minimum_interests'
|
||||
| 'request_notifications_permission_after_onboarding_v2'
|
||||
| 'session_withproxy_fix'
|
||||
| 'show_avi_follow_button'
|
||||
| 'show_follow_back_label_v2'
|
||||
| 'suggested_feeds_interstitial'
|
||||
|
||||
@@ -28,8 +28,6 @@ type StatsigUser = {
|
||||
bundleDate: number
|
||||
refSrc: string
|
||||
refUrl: string
|
||||
referrer: string
|
||||
referrerHostname: string
|
||||
appLanguage: string
|
||||
contentLanguages: string[]
|
||||
}
|
||||
@@ -37,29 +35,12 @@ type StatsigUser = {
|
||||
|
||||
let refSrc = ''
|
||||
let refUrl = ''
|
||||
let referrer = ''
|
||||
let referrerHostname = ''
|
||||
if (isWeb && typeof window !== 'undefined') {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
refSrc = params.get('ref_src') ?? ''
|
||||
refUrl = decodeURIComponent(params.get('ref_url') ?? '')
|
||||
}
|
||||
|
||||
if (
|
||||
isWeb &&
|
||||
typeof document !== 'undefined' &&
|
||||
document != null &&
|
||||
document.referrer
|
||||
) {
|
||||
try {
|
||||
const url = new URL(document.referrer)
|
||||
if (url.hostname !== 'bsky.app') {
|
||||
referrer = document.referrer
|
||||
referrerHostname = url.hostname
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
export type {LogEvents}
|
||||
|
||||
function createStatsigOptions(prefetchUsers: StatsigUser[]) {
|
||||
@@ -222,8 +203,6 @@ function toStatsigUser(did: string | undefined): StatsigUser {
|
||||
custom: {
|
||||
refSrc,
|
||||
refUrl,
|
||||
referrer,
|
||||
referrerHostname,
|
||||
platform: Platform.OS as 'ios' | 'android' | 'web',
|
||||
bundleIdentifier: BUNDLE_IDENTIFIER,
|
||||
bundleDate: BUNDLE_DATE,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const NON_BREAKING_SPACE = '\u00A0'
|
||||
+7
-7
@@ -1,8 +1,8 @@
|
||||
import {Platform} from 'react-native'
|
||||
import type {Theme} from './ThemeContext'
|
||||
import {colors} from './styles'
|
||||
|
||||
import {darkPalette, lightPalette, dimPalette} from '#/alf/themes'
|
||||
import {darkPalette, dimPalette, lightPalette} from '#/alf/themes'
|
||||
import {colors} from './styles'
|
||||
import type {Theme} from './ThemeContext'
|
||||
|
||||
export const defaultTheme: Theme = {
|
||||
colorScheme: 'light',
|
||||
@@ -308,8 +308,8 @@ export const darkTheme: Theme = {
|
||||
textVeryLight: darkPalette.contrast_400,
|
||||
replyLine: darkPalette.contrast_200,
|
||||
replyLineDot: darkPalette.contrast_200,
|
||||
unreadNotifBg: darkPalette.primary_975,
|
||||
unreadNotifBorder: darkPalette.primary_900,
|
||||
unreadNotifBg: darkPalette.primary_25,
|
||||
unreadNotifBorder: darkPalette.primary_100,
|
||||
postCtrl: darkPalette.contrast_500,
|
||||
brandText: darkPalette.primary_500,
|
||||
emptyStateIcon: darkPalette.contrast_300,
|
||||
@@ -357,8 +357,8 @@ export const dimTheme: Theme = {
|
||||
textVeryLight: dimPalette.contrast_400,
|
||||
replyLine: dimPalette.contrast_200,
|
||||
replyLineDot: dimPalette.contrast_200,
|
||||
unreadNotifBg: dimPalette.primary_975,
|
||||
unreadNotifBorder: dimPalette.primary_900,
|
||||
unreadNotifBg: dimPalette.primary_25,
|
||||
unreadNotifBorder: dimPalette.primary_100,
|
||||
postCtrl: dimPalette.contrast_500,
|
||||
brandText: dimPalette.primary_500,
|
||||
emptyStateIcon: dimPalette.contrast_300,
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
|
||||
import {ScrollView} from 'view/com/util/Views'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {SharedPrefs} from '../../../modules/expo-bluesky-swiss-army'
|
||||
|
||||
export function SharedPreferencesTesterScreen() {
|
||||
const [currentTestOutput, setCurrentTestOutput] = React.useState<string>('')
|
||||
|
||||
return (
|
||||
<ScrollView contentContainerStyle={{backgroundColor: 'red'}}>
|
||||
<View style={[a.flex_1]}>
|
||||
<View>
|
||||
<Text testID="testOutput">{currentTestOutput}</Text>
|
||||
</View>
|
||||
<View style={[a.flex_wrap]}>
|
||||
<Button
|
||||
label="btn"
|
||||
testID="setStringBtn"
|
||||
style={[a.self_center]}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="xsmall"
|
||||
onPress={async () => {
|
||||
SharedPrefs.removeValue('testerString')
|
||||
SharedPrefs.setValue('testerString', 'Hello')
|
||||
const str = SharedPrefs.getString('testerString')
|
||||
console.log(JSON.stringify(str))
|
||||
setCurrentTestOutput(`${str}`)
|
||||
}}>
|
||||
<ButtonText>Set String</ButtonText>
|
||||
</Button>
|
||||
<Button
|
||||
label="btn"
|
||||
testID="removeStringBtn"
|
||||
style={[a.self_center]}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="xsmall"
|
||||
onPress={async () => {
|
||||
SharedPrefs.removeValue('testerString')
|
||||
const str = SharedPrefs.getString('testerString')
|
||||
setCurrentTestOutput(`${str}`)
|
||||
}}>
|
||||
<ButtonText>Remove String</ButtonText>
|
||||
</Button>
|
||||
<Button
|
||||
label="btn"
|
||||
testID="setBoolBtn"
|
||||
style={[a.self_center]}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="xsmall"
|
||||
onPress={async () => {
|
||||
SharedPrefs.removeValue('testerBool')
|
||||
SharedPrefs.setValue('testerBool', true)
|
||||
const bool = SharedPrefs.getBool('testerBool')
|
||||
setCurrentTestOutput(`${bool}`)
|
||||
}}>
|
||||
<ButtonText>Set Bool</ButtonText>
|
||||
</Button>
|
||||
<Button
|
||||
label="btn"
|
||||
testID="setNumberBtn"
|
||||
style={[a.self_center]}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="xsmall"
|
||||
onPress={async () => {
|
||||
SharedPrefs.removeValue('testerNumber')
|
||||
SharedPrefs.setValue('testerNumber', 123)
|
||||
const num = SharedPrefs.getNumber('testerNumber')
|
||||
setCurrentTestOutput(`${num}`)
|
||||
}}>
|
||||
<ButtonText>Set Number</ButtonText>
|
||||
</Button>
|
||||
<Button
|
||||
label="btn"
|
||||
testID="addToSetBtn"
|
||||
style={[a.self_center]}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="xsmall"
|
||||
onPress={async () => {
|
||||
SharedPrefs.removeFromSet('testerSet', 'Hello!')
|
||||
SharedPrefs.addToSet('testerSet', 'Hello!')
|
||||
const contains = SharedPrefs.setContains('testerSet', 'Hello!')
|
||||
setCurrentTestOutput(`${contains}`)
|
||||
}}>
|
||||
<ButtonText>Add to Set</ButtonText>
|
||||
</Button>
|
||||
<Button
|
||||
label="btn"
|
||||
testID="removeFromSetBtn"
|
||||
style={[a.self_center]}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="xsmall"
|
||||
onPress={async () => {
|
||||
SharedPrefs.removeFromSet('testerSet', 'Hello!')
|
||||
const contains = SharedPrefs.setContains('testerSet', 'Hello!')
|
||||
setCurrentTestOutput(`${contains}`)
|
||||
}}>
|
||||
<ButtonText>Remove from Set</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
@@ -169,7 +169,7 @@ function HashtagScreenTab({
|
||||
|
||||
const queryParam = React.useMemo(() => {
|
||||
if (!author) return fullTag
|
||||
return `${fullTag} from:${sanitizeHandle(author)}`
|
||||
return `${fullTag} from:${author}`
|
||||
}, [fullTag, author])
|
||||
|
||||
const {
|
||||
|
||||
@@ -386,10 +386,10 @@ export function MessagesList({
|
||||
data={convoState.items}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={keyExtractor}
|
||||
containWeb={true}
|
||||
disableFullWindowScroll={true}
|
||||
// Prevents wrong position in Firefox when sending a message
|
||||
// as well as scroll getting stuck on Chome when scrolling upwards.
|
||||
disableContentVisibility={true}
|
||||
disableContainStyle={true}
|
||||
disableVirtualization={true}
|
||||
style={animatedListStyle}
|
||||
// The extra two items account for the header and the footer components
|
||||
|
||||
@@ -20,7 +20,7 @@ export function ProfileHeaderDisplayName({
|
||||
<View pointerEvents="none">
|
||||
<Text
|
||||
testID="profileHeaderDisplayName"
|
||||
style={[t.atoms.text, a.text_4xl, {fontWeight: '500'}]}>
|
||||
style={[t.atoms.text, a.text_4xl, a.self_start, {fontWeight: '500'}]}>
|
||||
{sanitizeDisplayName(
|
||||
profile.displayName || sanitizeHandle(profile.handle),
|
||||
moderation.ui('displayName'),
|
||||
|
||||
@@ -101,7 +101,7 @@ export function StepFeeds({moderationOpts}: {moderationOpts: ModerationOpts}) {
|
||||
onEndReachedThreshold={2}
|
||||
renderScrollComponent={props => <KeyboardAwareScrollView {...props} />}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
containWeb={true}
|
||||
disableFullWindowScroll={true}
|
||||
sideBorders={false}
|
||||
style={{flex: 1}}
|
||||
ListEmptyComponent={
|
||||
|
||||
@@ -80,7 +80,7 @@ export function StepProfiles({
|
||||
keyExtractor={keyExtractor}
|
||||
renderScrollComponent={props => <KeyboardAwareScrollView {...props} />}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
containWeb={true}
|
||||
disableFullWindowScroll={true}
|
||||
sideBorders={false}
|
||||
style={[a.flex_1]}
|
||||
onEndReached={
|
||||
|
||||
+31
-13
@@ -5,6 +5,7 @@ import throttle from 'lodash.throttle'
|
||||
|
||||
import {PROD_DEFAULT_FEED} from '#/lib/constants'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {useGate} from '#/lib/statsig/statsig'
|
||||
import {logger} from '#/logger'
|
||||
import {FeedDescriptor, FeedPostSliceItem} from '#/state/queries/post-feed'
|
||||
import {getFeedPostSlice} from '#/view/com/posts/Feed'
|
||||
@@ -24,6 +25,7 @@ const stateContext = React.createContext<StateContext>({
|
||||
|
||||
export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
|
||||
const agent = useAgent()
|
||||
const gate = useGate()
|
||||
const enabled = isDiscoverFeed(feed) && hasSession
|
||||
const queue = React.useRef<Set<string>>(new Set())
|
||||
const history = React.useRef<
|
||||
@@ -43,22 +45,38 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
|
||||
)
|
||||
|
||||
const sendToFeedNoDelay = React.useCallback(() => {
|
||||
const proxyAgent = agent.withProxy(
|
||||
// @ts-ignore TODO need to update withProxy() to support this key -prf
|
||||
'bsky_fg',
|
||||
// TODO when we start sending to other feeds, we need to grab their DID -prf
|
||||
'did:web:discover.bsky.app',
|
||||
) as BskyAgent
|
||||
|
||||
const interactions = Array.from(queue.current).map(toInteraction)
|
||||
queue.current.clear()
|
||||
|
||||
// Send to the feed
|
||||
proxyAgent.app.bsky.feed
|
||||
.sendInteractions({interactions})
|
||||
.catch((e: any) => {
|
||||
logger.warn('Failed to send feed interactions', {error: e})
|
||||
})
|
||||
if (gate('session_withproxy_fix')) {
|
||||
agent.app.bsky.feed
|
||||
.sendInteractions(
|
||||
{interactions},
|
||||
{
|
||||
encoding: 'application/json',
|
||||
headers: {
|
||||
// TODO when we start sending to other feeds, we need to grab their DID -prf
|
||||
'atproto-proxy': 'did:web:discover.bsky.app#bsky_fg',
|
||||
},
|
||||
},
|
||||
)
|
||||
.catch((e: any) => {
|
||||
logger.warn('Failed to send feed interactions', {error: e})
|
||||
})
|
||||
} else {
|
||||
const proxyAgent = agent.withProxy(
|
||||
// @ts-ignore TODO need to update withProxy() to support this key -prf
|
||||
'bsky_fg',
|
||||
// TODO when we start sending to other feeds, we need to grab their DID -prf
|
||||
'did:web:discover.bsky.app',
|
||||
) as BskyAgent
|
||||
proxyAgent.app.bsky.feed
|
||||
.sendInteractions({interactions})
|
||||
.catch((e: any) => {
|
||||
logger.warn('Failed to send feed interactions', {error: e})
|
||||
})
|
||||
}
|
||||
|
||||
// Send to Statsig
|
||||
if (aggregatedStats.current === null) {
|
||||
@@ -66,7 +84,7 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
|
||||
}
|
||||
sendOrAggregateInteractionsForStats(aggregatedStats.current, interactions)
|
||||
throttledFlushAggregatedStats()
|
||||
}, [agent, throttledFlushAggregatedStats])
|
||||
}, [agent, gate, throttledFlushAggregatedStats])
|
||||
|
||||
const sendToFeed = React.useMemo(
|
||||
() =>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {tryFetchGates} from '#/lib/statsig/statsig'
|
||||
import {getAge} from '#/lib/strings/time'
|
||||
import {logger} from '#/logger'
|
||||
import {snoozeEmailConfirmationPrompt} from '#/state/shell/reminders'
|
||||
import {addSessionErrorLog} from './logging'
|
||||
import {
|
||||
configureModerationForAccount,
|
||||
configureModerationForGuest,
|
||||
@@ -194,6 +195,9 @@ async function prepareAgent(
|
||||
const account = agentToSessionAccountOrThrow(agent)
|
||||
agent.setPersistSessionHandler(event => {
|
||||
onSessionChange(agent, account.did, event)
|
||||
if (event !== 'create' && event !== 'update') {
|
||||
addSessionErrorLog(account.did, event)
|
||||
}
|
||||
})
|
||||
return {agent, account}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {AtpSessionData} from '@atproto/api'
|
||||
import {AtpSessionData, AtpSessionEvent} from '@atproto/api'
|
||||
import {sha256} from 'js-sha256'
|
||||
import {Statsig} from 'statsig-react-native-expo'
|
||||
|
||||
@@ -70,6 +70,23 @@ export function wrapSessionReducerForLogging(reducer: Reducer): Reducer {
|
||||
let nextMessageIndex = 0
|
||||
const MAX_SLICE_LENGTH = 1000
|
||||
|
||||
// Not gated.
|
||||
export function addSessionErrorLog(did: string, event: AtpSessionEvent) {
|
||||
try {
|
||||
if (!Statsig.initializeCalled() || !Statsig.getStableID()) {
|
||||
return
|
||||
}
|
||||
const stack = (new Error().stack ?? '').slice(0, MAX_SLICE_LENGTH)
|
||||
Statsig.logEvent('session:error', null, {
|
||||
did,
|
||||
event,
|
||||
stack,
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
export function addSessionDebugLog(log: Log) {
|
||||
try {
|
||||
if (!Statsig.initializeCalled() || !Statsig.getStableID()) {
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
KeyboardAvoidingView,
|
||||
LayoutChangeEvent,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import Animated, {
|
||||
@@ -24,7 +23,6 @@ import Animated, {
|
||||
withTiming,
|
||||
} from 'react-native-reanimated'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {LinearGradient} from 'expo-linear-gradient'
|
||||
import {
|
||||
AppBskyFeedDefs,
|
||||
AppBskyFeedGetPostThread,
|
||||
@@ -61,21 +59,21 @@ import {useAgent, useSession} from '#/state/session'
|
||||
import {useComposerControls} from '#/state/shell/composer'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import * as apilib from 'lib/api/index'
|
||||
import {HITSLOP_10, MAX_GRAPHEME_LENGTH} from 'lib/constants'
|
||||
import {MAX_GRAPHEME_LENGTH} from 'lib/constants'
|
||||
import {useIsKeyboardVisible} from 'lib/hooks/useIsKeyboardVisible'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {cleanError} from 'lib/strings/errors'
|
||||
import {insertMentionAt} from 'lib/strings/mention-manip'
|
||||
import {shortenLinks} from 'lib/strings/rich-text-manip'
|
||||
import {colors, gradients, s} from 'lib/styles'
|
||||
import {colors, s} from 'lib/styles'
|
||||
import {isAndroid, isIOS, isNative, isWeb} from 'platform/detection'
|
||||
import {useDialogStateControlContext} from 'state/dialogs'
|
||||
import {GalleryModel} from 'state/models/media/gallery'
|
||||
import {ComposerOpts} from 'state/shell/composer'
|
||||
import {ComposerReplyTo} from 'view/com/composer/ComposerReplyTo'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {QuoteEmbed, QuoteX} from '../util/post-embeds/QuoteEmbed'
|
||||
@@ -458,20 +456,25 @@ export const ComposePost = observer(function ComposePost({
|
||||
<View style={[a.flex_1, viewStyles]} aria-modal accessibilityViewIsModal>
|
||||
<Animated.View style={topBarAnimatedStyle}>
|
||||
<View style={styles.topbarInner}>
|
||||
<TouchableOpacity
|
||||
testID="composerDiscardButton"
|
||||
<Button
|
||||
label={_(msg`Cancel`)}
|
||||
variant="ghost"
|
||||
color="primary"
|
||||
shape="default"
|
||||
size="small"
|
||||
style={[
|
||||
a.rounded_full,
|
||||
a.py_sm,
|
||||
{paddingLeft: 7, paddingRight: 7},
|
||||
]}
|
||||
onPress={onPressCancel}
|
||||
onAccessibilityEscape={onPressCancel}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Cancel`)}
|
||||
accessibilityHint={_(
|
||||
msg`Closes post composer and discards post draft`,
|
||||
)}
|
||||
hitSlop={HITSLOP_10}>
|
||||
<Text style={[pal.link, s.f18]}>
|
||||
)}>
|
||||
<ButtonText style={[a.text_md]}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
<View style={a.flex_1} />
|
||||
{isProcessing ? (
|
||||
<>
|
||||
@@ -488,31 +491,25 @@ export const ComposePost = observer(function ComposePost({
|
||||
hasMedia={hasMedia}
|
||||
/>
|
||||
{canPost ? (
|
||||
<TouchableOpacity
|
||||
<Button
|
||||
testID="composerPublishBtn"
|
||||
onPress={onPressPublish}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={
|
||||
label={
|
||||
replyTo ? _(msg`Publish reply`) : _(msg`Publish post`)
|
||||
}
|
||||
accessibilityHint="">
|
||||
<LinearGradient
|
||||
colors={[
|
||||
gradients.blueLight.start,
|
||||
gradients.blueLight.end,
|
||||
]}
|
||||
start={{x: 0, y: 0}}
|
||||
end={{x: 1, y: 1}}
|
||||
style={styles.postBtn}>
|
||||
<Text style={[s.white, s.f16, s.bold]}>
|
||||
{replyTo ? (
|
||||
<Trans context="action">Reply</Trans>
|
||||
) : (
|
||||
<Trans context="action">Post</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
variant="solid"
|
||||
color="primary"
|
||||
shape="default"
|
||||
size="small"
|
||||
style={[a.rounded_full, a.py_sm]}
|
||||
onPress={onPressPublish}>
|
||||
<ButtonText style={[a.text_md]}>
|
||||
{replyTo ? (
|
||||
<Trans context="action">Reply</Trans>
|
||||
) : (
|
||||
<Trans context="action">Post</Trans>
|
||||
)}
|
||||
</ButtonText>
|
||||
</Button>
|
||||
) : (
|
||||
<View style={[styles.postBtn, pal.btn]}>
|
||||
<Text style={[pal.textLight, s.f16, s.bold]}>
|
||||
@@ -828,7 +825,7 @@ const styles = StyleSheet.create({
|
||||
topbarInner: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
paddingHorizontal: 8,
|
||||
height: 54,
|
||||
gap: 4,
|
||||
},
|
||||
|
||||
@@ -174,8 +174,13 @@ function AltTextInner({
|
||||
<Text style={[a.text_2xl, a.font_bold, a.leading_tight, a.pb_sm]}>
|
||||
<Trans>Add alt text</Trans>
|
||||
</Text>
|
||||
<View style={[a.w_full, a.align_center, native({maxHeight: 200})]}>
|
||||
<GifEmbed link={link} params={params} hideAlt />
|
||||
<View style={[a.align_center]}>
|
||||
<GifEmbed
|
||||
link={link}
|
||||
params={params}
|
||||
hideAlt
|
||||
style={[native({maxHeight: 225})]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import React from 'react'
|
||||
import Picker from '@emoji-mart/react'
|
||||
import {
|
||||
StyleSheet,
|
||||
GestureResponderEvent,
|
||||
TouchableWithoutFeedback,
|
||||
useWindowDimensions,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import Picker from '@emoji-mart/react'
|
||||
|
||||
import {atoms as a} from '#/alf'
|
||||
import {textInputWebEmitter} from '../TextInput.web'
|
||||
|
||||
const HEIGHT_OFFSET = 40
|
||||
@@ -96,12 +98,29 @@ export function EmojiPicker({state, close}: IProps) {
|
||||
|
||||
if (!state.isOpen) return null
|
||||
|
||||
const onPressBackdrop = (e: GestureResponderEvent) => {
|
||||
// @ts-ignore web only
|
||||
if (e.nativeEvent?.pointerId === -1) return
|
||||
close()
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableWithoutFeedback
|
||||
accessibilityRole="button"
|
||||
onPress={close}
|
||||
onPress={onPressBackdrop}
|
||||
accessibilityViewIsModal>
|
||||
<View style={styles.mask}>
|
||||
<View
|
||||
style={[
|
||||
a.fixed,
|
||||
a.w_full,
|
||||
a.h_full,
|
||||
a.align_center,
|
||||
{
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
},
|
||||
]}>
|
||||
{/* eslint-disable-next-line react-native-a11y/has-valid-accessibility-descriptors */}
|
||||
<TouchableWithoutFeedback onPress={e => e.stopPropagation()}>
|
||||
<View style={[{position: 'absolute'}, position]}>
|
||||
@@ -118,20 +137,3 @@ export function EmojiPicker({state, close}: IProps) {
|
||||
</TouchableWithoutFeedback>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
mask: {
|
||||
// @ts-ignore web ony
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
},
|
||||
picker: {
|
||||
marginHorizontal: 'auto',
|
||||
paddingRight: 50,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -58,6 +58,7 @@ import {useNavigation} from '@react-navigation/native'
|
||||
import {parseTenorGif} from '#/lib/strings/embed-player'
|
||||
import {logger} from '#/logger'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
import {forceLTR} from 'lib/strings/bidi'
|
||||
import {DM_SERVICE_HEADERS} from 'state/queries/messages/const'
|
||||
import {useAgent} from 'state/session'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
@@ -178,8 +179,10 @@ let FeedItem = ({
|
||||
key={authors[0].href}
|
||||
style={[pal.text, s.bold]}
|
||||
href={authors[0].href}
|
||||
text={sanitizeDisplayName(
|
||||
authors[0].profile.displayName || authors[0].profile.handle,
|
||||
text={forceLTR(
|
||||
sanitizeDisplayName(
|
||||
authors[0].profile.displayName || authors[0].profile.handle,
|
||||
),
|
||||
)}
|
||||
disableMismatchWarning
|
||||
/>
|
||||
@@ -378,7 +381,7 @@ let FeedItem = ({
|
||||
showDmButton={item.type === 'starterpack-joined' || isFollowBack}
|
||||
/>
|
||||
<ExpandedAuthorsList visible={isAuthorsExpanded} authors={authors} />
|
||||
<Text style={[styles.meta, pal.text]}>
|
||||
<Text style={[styles.meta, a.self_start, pal.text]}>
|
||||
{action}
|
||||
<TimeElapsed timestamp={item.notification.indexedAt}>
|
||||
{({timeElapsed}) => (
|
||||
|
||||
@@ -281,7 +281,7 @@ let PostThreadItemLoaded = ({
|
||||
<Link style={s.flex1} href={authorHref} title={authorTitle}>
|
||||
<Text
|
||||
type="xl-bold"
|
||||
style={[pal.text]}
|
||||
style={[pal.text, a.self_start]}
|
||||
numberOfLines={1}
|
||||
lineHeight={1.2}>
|
||||
{sanitizeDisplayName(
|
||||
|
||||
+19
-11
@@ -15,6 +15,7 @@ import {useQueryClient} from '@tanstack/react-query'
|
||||
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
|
||||
import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useComposerControls} from '#/state/shell/composer'
|
||||
import {MAX_POST_LINES} from 'lib/constants'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
@@ -145,6 +146,9 @@ function PostInner({
|
||||
precacheProfile(queryClient, post.author)
|
||||
}, [queryClient, post.author])
|
||||
|
||||
const {currentAccount} = useSession()
|
||||
const isMe = replyAuthorDid === currentAccount?.did
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={itemHref}
|
||||
@@ -187,17 +191,21 @@ function PostInner({
|
||||
style={[pal.textLight, s.mr2]}
|
||||
lineHeight={1.2}
|
||||
numberOfLines={1}>
|
||||
<Trans context="description">
|
||||
Reply to{' '}
|
||||
<ProfileHoverCard inline did={replyAuthorDid}>
|
||||
<UserInfoText
|
||||
type="sm"
|
||||
did={replyAuthorDid}
|
||||
attr="displayName"
|
||||
style={[pal.textLight]}
|
||||
/>
|
||||
</ProfileHoverCard>
|
||||
</Trans>
|
||||
{isMe ? (
|
||||
<Trans context="description">Reply to you</Trans>
|
||||
) : (
|
||||
<Trans context="description">
|
||||
Reply to{' '}
|
||||
<ProfileHoverCard inline did={replyAuthorDid}>
|
||||
<UserInfoText
|
||||
type="sm"
|
||||
did={replyAuthorDid}
|
||||
attr="displayName"
|
||||
style={[pal.textLight]}
|
||||
/>
|
||||
</ProfileHoverCard>
|
||||
</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -43,6 +43,7 @@ import {Text} from '../util/text/Text'
|
||||
import {PreviewableUserAvatar} from '../util/UserAvatar'
|
||||
import {AviFollowButton} from './AviFollowButton'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
import {useSession} from '#/state/session'
|
||||
import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost'
|
||||
|
||||
interface FeedItemProps {
|
||||
@@ -200,6 +201,11 @@ let FeedItemInner = ({
|
||||
},
|
||||
]
|
||||
|
||||
const {currentAccount} = useSession()
|
||||
const isOwner =
|
||||
AppBskyFeedDefs.isReasonRepost(reason) &&
|
||||
reason.by.did === currentAccount?.did
|
||||
|
||||
return (
|
||||
<Link
|
||||
testID={`feedItem-by-${post.author.handle}`}
|
||||
@@ -250,11 +256,15 @@ let FeedItemInner = ({
|
||||
<Link
|
||||
style={styles.includeReason}
|
||||
href={makeProfileLink(reason.by)}
|
||||
title={_(
|
||||
msg`Reposted by ${sanitizeDisplayName(
|
||||
reason.by.displayName || reason.by.handle,
|
||||
)}`,
|
||||
)}
|
||||
title={
|
||||
isOwner
|
||||
? _(msg`Reposted by you`)
|
||||
: _(
|
||||
msg`Reposted by ${sanitizeDisplayName(
|
||||
reason.by.displayName || reason.by.handle,
|
||||
)}`,
|
||||
)
|
||||
}
|
||||
onBeforePress={onOpenReposter}>
|
||||
<Repost
|
||||
style={{color: pal.colors.textLight, marginRight: 3}}
|
||||
@@ -266,24 +276,28 @@ let FeedItemInner = ({
|
||||
style={pal.textLight}
|
||||
lineHeight={1.2}
|
||||
numberOfLines={1}>
|
||||
<Trans>
|
||||
Reposted by{' '}
|
||||
<ProfileHoverCard inline did={reason.by.did}>
|
||||
<TextLinkOnWebOnly
|
||||
type="sm-bold"
|
||||
style={pal.textLight}
|
||||
lineHeight={1.2}
|
||||
numberOfLines={1}
|
||||
text={sanitizeDisplayName(
|
||||
reason.by.displayName ||
|
||||
sanitizeHandle(reason.by.handle),
|
||||
moderation.ui('displayName'),
|
||||
)}
|
||||
href={makeProfileLink(reason.by)}
|
||||
onBeforePress={onOpenReposter}
|
||||
/>
|
||||
</ProfileHoverCard>
|
||||
</Trans>
|
||||
{isOwner ? (
|
||||
<Trans>Reposted by you</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
Reposted by{' '}
|
||||
<ProfileHoverCard inline did={reason.by.did}>
|
||||
<TextLinkOnWebOnly
|
||||
type="sm-bold"
|
||||
style={pal.textLight}
|
||||
lineHeight={1.2}
|
||||
numberOfLines={1}
|
||||
text={sanitizeDisplayName(
|
||||
reason.by.displayName ||
|
||||
sanitizeHandle(reason.by.handle),
|
||||
moderation.ui('displayName'),
|
||||
)}
|
||||
href={makeProfileLink(reason.by)}
|
||||
onBeforePress={onOpenReposter}
|
||||
/>
|
||||
</ProfileHoverCard>
|
||||
</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</Link>
|
||||
) : null}
|
||||
@@ -421,6 +435,9 @@ function ReplyToLabel({
|
||||
blocked?: boolean
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const {currentAccount} = useSession()
|
||||
const isMe = profile.did === currentAccount?.did
|
||||
|
||||
return (
|
||||
<View style={[s.flexRow, s.mb2, s.alignCenter]}>
|
||||
<FontAwesomeIcon
|
||||
@@ -433,7 +450,9 @@ function ReplyToLabel({
|
||||
style={[pal.textLight, s.mr2]}
|
||||
lineHeight={1.2}
|
||||
numberOfLines={1}>
|
||||
{blocked ? (
|
||||
{isMe ? (
|
||||
<Trans context="description">Reply to you</Trans>
|
||||
) : blocked ? (
|
||||
<Trans context="description">Reply to a blocked post</Trans>
|
||||
) : (
|
||||
<Trans context="description">
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
moderateProfile,
|
||||
ModerationDecision,
|
||||
} from '@atproto/api'
|
||||
import {Trans} from '@lingui/macro'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
@@ -19,12 +18,16 @@ import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||
import {sanitizeHandle} from 'lib/strings/handles'
|
||||
import {s} from 'lib/styles'
|
||||
import {precacheProfile} from 'state/queries/profile'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {
|
||||
KnownFollowers,
|
||||
shouldShowKnownFollowers,
|
||||
} from '#/components/KnownFollowers'
|
||||
import {Link} from '../util/Link'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {PreviewableUserAvatar} from '../util/UserAvatar'
|
||||
import {FollowButton} from './FollowButton'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
import {atoms as a} from '#/alf'
|
||||
import * as Pills from '#/components/Pills'
|
||||
|
||||
export function ProfileCard({
|
||||
@@ -33,22 +36,22 @@ export function ProfileCard({
|
||||
noModFilter,
|
||||
noBg,
|
||||
noBorder,
|
||||
followers,
|
||||
renderButton,
|
||||
onPress,
|
||||
style,
|
||||
showKnownFollowers,
|
||||
}: {
|
||||
testID?: string
|
||||
profile: AppBskyActorDefs.ProfileViewBasic
|
||||
noModFilter?: boolean
|
||||
noBg?: boolean
|
||||
noBorder?: boolean
|
||||
followers?: AppBskyActorDefs.ProfileView[] | undefined
|
||||
renderButton?: (
|
||||
profile: Shadow<AppBskyActorDefs.ProfileViewBasic>,
|
||||
) => React.ReactNode
|
||||
onPress?: () => void
|
||||
style?: StyleProp<ViewStyle>
|
||||
showKnownFollowers?: boolean
|
||||
}) {
|
||||
const queryClient = useQueryClient()
|
||||
const pal = usePalette('default')
|
||||
@@ -70,6 +73,11 @@ export function ProfileCard({
|
||||
return null
|
||||
}
|
||||
|
||||
const knownFollowersVisible =
|
||||
showKnownFollowers &&
|
||||
shouldShowKnownFollowers(profile.viewer?.knownFollowers) &&
|
||||
moderationOpts
|
||||
|
||||
return (
|
||||
<Link
|
||||
testID={testID}
|
||||
@@ -97,7 +105,7 @@ export function ProfileCard({
|
||||
<View style={styles.layoutContent}>
|
||||
<Text
|
||||
type="lg"
|
||||
style={[s.bold, pal.text]}
|
||||
style={[s.bold, pal.text, a.self_start]}
|
||||
numberOfLines={1}
|
||||
lineHeight={1.2}>
|
||||
{sanitizeDisplayName(
|
||||
@@ -118,14 +126,30 @@ export function ProfileCard({
|
||||
<View style={styles.layoutButton}>{renderButton(profile)}</View>
|
||||
) : undefined}
|
||||
</View>
|
||||
{profile.description ? (
|
||||
{profile.description || knownFollowersVisible ? (
|
||||
<View style={styles.details}>
|
||||
<Text style={pal.text} numberOfLines={4}>
|
||||
{profile.description as string}
|
||||
</Text>
|
||||
{profile.description ? (
|
||||
<Text style={pal.text} numberOfLines={4}>
|
||||
{profile.description as string}
|
||||
</Text>
|
||||
) : null}
|
||||
{knownFollowersVisible ? (
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.gap_sm,
|
||||
!!profile.description && a.mt_md,
|
||||
]}>
|
||||
<KnownFollowers
|
||||
minimal
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
<FollowersList followers={followers} />
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -155,73 +179,20 @@ export function ProfileCardPills({
|
||||
)
|
||||
}
|
||||
|
||||
function FollowersList({
|
||||
followers,
|
||||
}: {
|
||||
followers?: AppBskyActorDefs.ProfileView[] | undefined
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const moderationOpts = useModerationOpts()
|
||||
|
||||
const followersWithMods = React.useMemo(() => {
|
||||
if (!followers || !moderationOpts) {
|
||||
return []
|
||||
}
|
||||
|
||||
return followers
|
||||
.map(f => ({
|
||||
f,
|
||||
mod: moderateProfile(f, moderationOpts),
|
||||
}))
|
||||
.filter(({mod}) => !mod.ui('profileList').filter)
|
||||
}, [followers, moderationOpts])
|
||||
|
||||
if (!followersWithMods?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.followedBy}>
|
||||
<Text
|
||||
type="sm"
|
||||
style={[styles.followsByDesc, pal.textLight]}
|
||||
numberOfLines={2}
|
||||
lineHeight={1.2}>
|
||||
<Trans>
|
||||
Followed by{' '}
|
||||
{followersWithMods.map(({f}) => f.displayName || f.handle).join(', ')}
|
||||
</Trans>
|
||||
</Text>
|
||||
{followersWithMods.slice(0, 3).map(({f, mod}) => (
|
||||
<View key={f.did} style={styles.followedByAviContainer}>
|
||||
<View style={[styles.followedByAvi, pal.view]}>
|
||||
<PreviewableUserAvatar
|
||||
size={32}
|
||||
profile={f}
|
||||
moderation={mod.ui('avatar')}
|
||||
type={f.associated?.labeler ? 'labeler' : 'user'}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function ProfileCardWithFollowBtn({
|
||||
profile,
|
||||
noBg,
|
||||
noBorder,
|
||||
followers,
|
||||
onPress,
|
||||
logContext = 'ProfileCard',
|
||||
showKnownFollowers,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewBasic
|
||||
profile: AppBskyActorDefs.ProfileView
|
||||
noBg?: boolean
|
||||
noBorder?: boolean
|
||||
followers?: AppBskyActorDefs.ProfileView[] | undefined
|
||||
onPress?: () => void
|
||||
logContext?: 'ProfileCard' | 'StarterPackProfilesList'
|
||||
showKnownFollowers?: boolean
|
||||
}) {
|
||||
const {currentAccount} = useSession()
|
||||
const isMe = profile.did === currentAccount?.did
|
||||
@@ -231,7 +202,6 @@ export function ProfileCardWithFollowBtn({
|
||||
profile={profile}
|
||||
noBg={noBg}
|
||||
noBorder={noBorder}
|
||||
followers={followers}
|
||||
renderButton={
|
||||
isMe
|
||||
? undefined
|
||||
@@ -240,6 +210,7 @@ export function ProfileCardWithFollowBtn({
|
||||
)
|
||||
}
|
||||
onPress={onPress}
|
||||
showKnownFollowers={!isMe && showKnownFollowers}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,11 +24,12 @@ export type ListProps<ItemT> = Omit<
|
||||
refreshing?: boolean
|
||||
onRefresh?: () => void
|
||||
onItemSeen?: (item: ItemT) => void
|
||||
containWeb?: boolean
|
||||
desktopFixedHeight?: number | boolean
|
||||
// Web only prop to contain the scroll to the container rather than the window
|
||||
disableFullWindowScroll?: boolean
|
||||
sideBorders?: boolean
|
||||
// Web only prop to disable a perf optimization (which would otherwise be on).
|
||||
disableContentVisibility?: boolean
|
||||
disableContainStyle?: boolean
|
||||
}
|
||||
export type ListRef = React.MutableRefObject<FlatList_INTERNAL | null>
|
||||
|
||||
|
||||
@@ -23,9 +23,11 @@ export type ListProps<ItemT> = Omit<
|
||||
onRefresh?: () => void
|
||||
onItemSeen?: (item: ItemT) => void
|
||||
desktopFixedHeight?: number | boolean
|
||||
containWeb?: boolean
|
||||
// Web only prop to contain the scroll to the container rather than the window
|
||||
disableFullWindowScroll?: boolean
|
||||
sideBorders?: boolean
|
||||
disableContentVisibility?: boolean
|
||||
// Web only prop to disable a perf optimization (which would otherwise be on).
|
||||
disableContainStyle?: boolean
|
||||
}
|
||||
export type ListRef = React.MutableRefObject<any | null> // TODO: Better types.
|
||||
|
||||
@@ -39,7 +41,7 @@ function ListImpl<ItemT>(
|
||||
ListHeaderComponent,
|
||||
ListFooterComponent,
|
||||
ListEmptyComponent,
|
||||
containWeb,
|
||||
disableFullWindowScroll,
|
||||
contentContainerStyle,
|
||||
data,
|
||||
desktopFixedHeight,
|
||||
@@ -58,7 +60,7 @@ function ListImpl<ItemT>(
|
||||
extraData,
|
||||
style,
|
||||
sideBorders = true,
|
||||
disableContentVisibility,
|
||||
disableContainStyle,
|
||||
...props
|
||||
}: ListProps<ItemT>,
|
||||
ref: React.Ref<ListMethods>,
|
||||
@@ -112,7 +114,7 @@ function ListImpl<ItemT>(
|
||||
}
|
||||
|
||||
const getScrollableNode = React.useCallback(() => {
|
||||
if (containWeb) {
|
||||
if (disableFullWindowScroll) {
|
||||
const element = nativeRef.current as HTMLDivElement | null
|
||||
if (!element) return
|
||||
|
||||
@@ -182,7 +184,7 @@ function ListImpl<ItemT>(
|
||||
},
|
||||
}
|
||||
}
|
||||
}, [containWeb])
|
||||
}, [disableFullWindowScroll])
|
||||
|
||||
const nativeRef = React.useRef<HTMLDivElement>(null)
|
||||
React.useImperativeHandle(
|
||||
@@ -267,7 +269,12 @@ function ListImpl<ItemT>(
|
||||
return () => {
|
||||
element?.removeEventListener('scroll', handleScroll)
|
||||
}
|
||||
}, [isInsideVisibleTree, handleScroll, containWeb, getScrollableNode])
|
||||
}, [
|
||||
isInsideVisibleTree,
|
||||
handleScroll,
|
||||
disableFullWindowScroll,
|
||||
getScrollableNode,
|
||||
])
|
||||
|
||||
// --- onScrolledDownChange ---
|
||||
const isScrolledDown = useRef(false)
|
||||
@@ -308,7 +315,7 @@ function ListImpl<ItemT>(
|
||||
{...props}
|
||||
style={[
|
||||
style,
|
||||
containWeb && {
|
||||
disableFullWindowScroll && {
|
||||
flex: 1,
|
||||
// @ts-expect-error web only
|
||||
'overflow-y': 'scroll',
|
||||
@@ -332,13 +339,13 @@ function ListImpl<ItemT>(
|
||||
pal.border,
|
||||
]}>
|
||||
<Visibility
|
||||
root={containWeb ? nativeRef : null}
|
||||
root={disableFullWindowScroll ? nativeRef : null}
|
||||
onVisibleChange={handleAboveTheFoldVisibleChange}
|
||||
style={[styles.aboveTheFoldDetector, {height: headerOffset}]}
|
||||
/>
|
||||
{onStartReached && !isEmpty && (
|
||||
<Visibility
|
||||
root={containWeb ? nativeRef : null}
|
||||
root={disableFullWindowScroll ? nativeRef : null}
|
||||
onVisibleChange={onHeadVisibilityChange}
|
||||
topMargin={(onStartReachedThreshold ?? 0) * 100 + '%'}
|
||||
/>
|
||||
@@ -356,13 +363,13 @@ function ListImpl<ItemT>(
|
||||
renderItem={renderItem}
|
||||
extraData={extraData}
|
||||
onItemSeen={onItemSeen}
|
||||
disableContentVisibility={disableContentVisibility}
|
||||
disableContainStyle={disableContainStyle}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{onEndReached && !isEmpty && (
|
||||
<Visibility
|
||||
root={containWeb ? nativeRef : null}
|
||||
root={disableFullWindowScroll ? nativeRef : null}
|
||||
onVisibleChange={onTailVisibilityChange}
|
||||
bottomMargin={(onEndReachedThreshold ?? 0) * 100 + '%'}
|
||||
key={data?.length}
|
||||
@@ -406,7 +413,7 @@ let Row = function RowImpl<ItemT>({
|
||||
renderItem,
|
||||
extraData: _unused,
|
||||
onItemSeen,
|
||||
disableContentVisibility,
|
||||
disableContainStyle,
|
||||
}: {
|
||||
item: ItemT
|
||||
index: number
|
||||
@@ -416,7 +423,7 @@ let Row = function RowImpl<ItemT>({
|
||||
| ((data: {index: number; item: any; separators: any}) => React.ReactNode)
|
||||
extraData: any
|
||||
onItemSeen: ((item: any) => void) | undefined
|
||||
disableContentVisibility?: boolean
|
||||
disableContainStyle?: boolean
|
||||
}): React.ReactNode {
|
||||
const rowRef = React.useRef(null)
|
||||
const intersectionTimeout = React.useRef<NodeJS.Timer | undefined>(undefined)
|
||||
@@ -465,14 +472,10 @@ let Row = function RowImpl<ItemT>({
|
||||
return null
|
||||
}
|
||||
|
||||
const shouldDisableContentVisibility = disableContentVisibility || isSafari
|
||||
const shouldDisableContainStyle = disableContainStyle || isSafari
|
||||
return (
|
||||
<View
|
||||
style={
|
||||
shouldDisableContentVisibility
|
||||
? undefined
|
||||
: styles.contentVisibilityAuto
|
||||
}
|
||||
style={shouldDisableContainStyle ? undefined : styles.contain}
|
||||
ref={rowRef}>
|
||||
{renderItem({item, index, separators: null as any})}
|
||||
</View>
|
||||
@@ -544,9 +547,9 @@ const styles = StyleSheet.create({
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
},
|
||||
contentVisibilityAuto: {
|
||||
contain: {
|
||||
// @ts-ignore web only
|
||||
contentVisibility: 'auto',
|
||||
contain: 'layout paint',
|
||||
},
|
||||
minHeightViewport: {
|
||||
// @ts-ignore web only
|
||||
|
||||
@@ -6,6 +6,8 @@ import {useQueryClient} from '@tanstack/react-query'
|
||||
import {precacheProfile} from '#/state/queries/profile'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {makeProfileLink} from 'lib/routes/links'
|
||||
import {forceLTR} from 'lib/strings/bidi'
|
||||
import {NON_BREAKING_SPACE} from 'lib/strings/constants'
|
||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||
import {sanitizeHandle} from 'lib/strings/handles'
|
||||
import {niceDate} from 'lib/strings/time'
|
||||
@@ -32,8 +34,6 @@ interface PostMetaOpts {
|
||||
style?: StyleProp<ViewStyle>
|
||||
}
|
||||
|
||||
const NON_BREAKING_SPACE = '\u00A0'
|
||||
|
||||
let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
|
||||
const pal = usePalette('default')
|
||||
const displayName = opts.author.displayName || opts.author.handle
|
||||
@@ -70,14 +70,12 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
|
||||
style={[pal.text]}
|
||||
lineHeight={1.2}
|
||||
disableMismatchWarning
|
||||
text={
|
||||
<>
|
||||
{sanitizeDisplayName(
|
||||
displayName,
|
||||
opts.moderation?.ui('displayName'),
|
||||
)}
|
||||
</>
|
||||
}
|
||||
text={forceLTR(
|
||||
sanitizeDisplayName(
|
||||
displayName,
|
||||
opts.moderation?.ui('displayName'),
|
||||
),
|
||||
)}
|
||||
href={profileLink}
|
||||
onBeforePress={onBeforePressAuthor}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import React from 'react'
|
||||
import {Pressable, StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
import {
|
||||
Pressable,
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import {AppBskyEmbedExternal} from '@atproto/api'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
@@ -89,10 +96,12 @@ export function GifEmbed({
|
||||
params,
|
||||
link,
|
||||
hideAlt,
|
||||
style = {width: '100%'},
|
||||
}: {
|
||||
params: EmbedPlayerParams
|
||||
link: AppBskyEmbedExternal.ViewExternal
|
||||
hideAlt?: boolean
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const autoplayDisabled = useAutoplayDisabled()
|
||||
@@ -124,7 +133,7 @@ export function GifEmbed({
|
||||
)
|
||||
|
||||
return (
|
||||
<View style={[a.rounded_sm, a.overflow_hidden, a.mt_sm, {width: '100%'}]}>
|
||||
<View style={[a.rounded_sm, a.overflow_hidden, a.mt_sm, style]}>
|
||||
<View
|
||||
style={[
|
||||
a.rounded_sm,
|
||||
|
||||
@@ -131,7 +131,7 @@ export function PostEmbeds({
|
||||
const {alt, thumb, aspectRatio} = images[0]
|
||||
return (
|
||||
<ContentHider modui={moderation?.ui('contentMedia')}>
|
||||
<View style={[styles.imagesContainer, style]}>
|
||||
<View style={[styles.container, style]}>
|
||||
<AutoSizedImage
|
||||
alt={alt}
|
||||
uri={thumb}
|
||||
@@ -156,7 +156,7 @@ export function PostEmbeds({
|
||||
|
||||
return (
|
||||
<ContentHider modui={moderation?.ui('contentMedia')}>
|
||||
<View style={[styles.imagesContainer, style]}>
|
||||
<View style={[styles.container, style]}>
|
||||
<ImageLayoutGrid
|
||||
images={embed.images}
|
||||
onPress={_openLightbox}
|
||||
@@ -174,7 +174,11 @@ export function PostEmbeds({
|
||||
const link = embed.external
|
||||
return (
|
||||
<ContentHider modui={moderation?.ui('contentMedia')}>
|
||||
<ExternalLinkEmbed link={link} onOpen={onOpen} style={style} />
|
||||
<ExternalLinkEmbed
|
||||
link={link}
|
||||
onOpen={onOpen}
|
||||
style={[styles.container, style]}
|
||||
/>
|
||||
</ContentHider>
|
||||
)
|
||||
}
|
||||
@@ -183,7 +187,7 @@ export function PostEmbeds({
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
imagesContainer: {
|
||||
container: {
|
||||
marginTop: 8,
|
||||
},
|
||||
altContainer: {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useGate} from '#/lib/statsig/statsig'
|
||||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
@@ -241,7 +242,7 @@ type ExploreScreenItems =
|
||||
| {
|
||||
type: 'profile'
|
||||
key: string
|
||||
profile: AppBskyActorDefs.ProfileViewBasic
|
||||
profile: AppBskyActorDefs.ProfileView
|
||||
}
|
||||
| {
|
||||
type: 'feed'
|
||||
@@ -291,6 +292,7 @@ export function Explore() {
|
||||
error: feedsError,
|
||||
fetchNextPage: fetchNextFeedsPage,
|
||||
} = useGetPopularFeedsQuery({limit: 10})
|
||||
const gate = useGate()
|
||||
|
||||
const isLoadingMoreProfiles = isFetchingNextProfilesPage && !isLoadingProfiles
|
||||
const onLoadMoreProfiles = React.useCallback(async () => {
|
||||
@@ -492,7 +494,14 @@ export function Explore() {
|
||||
case 'profile': {
|
||||
return (
|
||||
<View style={[a.border_b, t.atoms.border_contrast_low]}>
|
||||
<ProfileCardWithFollowBtn profile={item.profile} noBg noBorder />
|
||||
<ProfileCardWithFollowBtn
|
||||
profile={item.profile}
|
||||
noBg
|
||||
noBorder
|
||||
showKnownFollowers={gate(
|
||||
'explore_page_profile_card_social_proof',
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -555,7 +564,7 @@ export function Explore() {
|
||||
}
|
||||
}
|
||||
},
|
||||
[t, moderationOpts],
|
||||
[t, moderationOpts, gate],
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
@@ -68,6 +68,7 @@ import {navigate, resetToTab} from '#/Navigation'
|
||||
import {Email2FAToggle} from './Email2FAToggle'
|
||||
import {ExportCarDialog} from './ExportCarDialog'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
import {atoms as a} from '#/alf'
|
||||
|
||||
function SettingsAccountCard({
|
||||
account,
|
||||
@@ -104,7 +105,7 @@ function SettingsAccountCard({
|
||||
/>
|
||||
</View>
|
||||
<View style={[s.flex1]}>
|
||||
<Text type="md-bold" style={pal.text} numberOfLines={1}>
|
||||
<Text type="md-bold" style={[pal.text, a.self_start]} numberOfLines={1}>
|
||||
{profile?.displayName || account.handle}
|
||||
</Text>
|
||||
<Text type="sm" style={pal.textLight} numberOfLines={1}>
|
||||
|
||||
@@ -20,29 +20,31 @@ export function Buttons() {
|
||||
<H1>Buttons</H1>
|
||||
|
||||
<View style={[a.flex_row, a.flex_wrap, a.gap_md, a.align_start]}>
|
||||
{['primary', 'secondary', 'secondary_inverted'].map(color => (
|
||||
<View key={color} style={[a.gap_md, a.align_start]}>
|
||||
{['solid', 'outline', 'ghost'].map(variant => (
|
||||
<React.Fragment key={variant}>
|
||||
<Button
|
||||
variant={variant as ButtonVariant}
|
||||
color={color as ButtonColor}
|
||||
size="large"
|
||||
label="Click here">
|
||||
<ButtonText>Button</ButtonText>
|
||||
</Button>
|
||||
<Button
|
||||
disabled
|
||||
variant={variant as ButtonVariant}
|
||||
color={color as ButtonColor}
|
||||
size="large"
|
||||
label="Click here">
|
||||
<ButtonText>Button</ButtonText>
|
||||
</Button>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</View>
|
||||
))}
|
||||
{['primary', 'secondary', 'secondary_inverted', 'negative'].map(
|
||||
color => (
|
||||
<View key={color} style={[a.gap_md, a.align_start]}>
|
||||
{['solid', 'outline', 'ghost'].map(variant => (
|
||||
<React.Fragment key={variant}>
|
||||
<Button
|
||||
variant={variant as ButtonVariant}
|
||||
color={color as ButtonColor}
|
||||
size="large"
|
||||
label="Click here">
|
||||
<ButtonText>Button</ButtonText>
|
||||
</Button>
|
||||
<Button
|
||||
disabled
|
||||
variant={variant as ButtonVariant}
|
||||
color={color as ButtonColor}
|
||||
size="large"
|
||||
label="Click here">
|
||||
<ButtonText>Button</ButtonText>
|
||||
</Button>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</View>
|
||||
),
|
||||
)}
|
||||
|
||||
<View style={[a.flex_row, a.gap_md, a.align_start]}>
|
||||
<View style={[a.gap_md, a.align_start]}>
|
||||
@@ -68,6 +70,7 @@ export function Buttons() {
|
||||
),
|
||||
)}
|
||||
</View>
|
||||
{/*
|
||||
<View style={[a.gap_md, a.align_start]}>
|
||||
{['gradient_sunset', 'gradient_nordic', 'gradient_bonfire'].map(
|
||||
name => (
|
||||
@@ -91,6 +94,7 @@ export function Buttons() {
|
||||
),
|
||||
)}
|
||||
</View>
|
||||
*/}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {useDialogStateControlContext} from '#/state/dialogs'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
@@ -18,6 +20,7 @@ export function Dialogs() {
|
||||
const [shouldRenderUnmountTest, setShouldRenderUnmountTest] =
|
||||
React.useState(false)
|
||||
const unmountTestInterval = React.useRef<number>()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
|
||||
const onUnmountTestStartPressWithClose = () => {
|
||||
setShouldRenderUnmountTest(true)
|
||||
@@ -134,6 +137,16 @@ export function Dialogs() {
|
||||
<ButtonText>End Unmount Test</ButtonText>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="small"
|
||||
onPress={() => navigation.navigate('SharedPreferencesTester')}
|
||||
label="two"
|
||||
testID="sharedPrefsTestOpenBtn">
|
||||
<ButtonText>Open Shared Prefs Tester</ButtonText>
|
||||
</Button>
|
||||
|
||||
<Prompt.Outer control={prompt}>
|
||||
<Prompt.TitleText>This is a prompt</Prompt.TitleText>
|
||||
<Prompt.DescriptionText>
|
||||
|
||||
@@ -41,6 +41,16 @@ export function Forms() {
|
||||
/>
|
||||
</TextField.Root>
|
||||
|
||||
<TextField.Root>
|
||||
<TextField.Icon icon={Globe} />
|
||||
<TextField.Input
|
||||
value={value}
|
||||
onChangeText={setValue}
|
||||
label="Text field"
|
||||
isInvalid
|
||||
/>
|
||||
</TextField.Root>
|
||||
|
||||
<View style={[a.w_full]}>
|
||||
<TextField.LabelText>Text field</TextField.LabelText>
|
||||
<TextField.Root>
|
||||
|
||||
@@ -47,7 +47,7 @@ export function ListContained() {
|
||||
)
|
||||
}}
|
||||
keyExtractor={item => item.id.toString()}
|
||||
containWeb={true}
|
||||
disableFullWindowScroll={true}
|
||||
style={{flex: 1}}
|
||||
onStartReached={() => {
|
||||
console.log('Start Reached')
|
||||
|
||||
@@ -30,6 +30,7 @@ import {precacheProfile} from 'state/queries/profile'
|
||||
import {Link} from '#/view/com/util/Link'
|
||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {atoms as a} from '#/alf'
|
||||
|
||||
let SearchLinkCard = ({
|
||||
label,
|
||||
@@ -127,7 +128,7 @@ let SearchProfileCard = ({
|
||||
<View style={{flex: 1}}>
|
||||
<Text
|
||||
type="lg"
|
||||
style={[s.bold, pal.text]}
|
||||
style={[s.bold, pal.text, a.self_start]}
|
||||
numberOfLines={1}
|
||||
lineHeight={1.2}>
|
||||
{sanitizeDisplayName(
|
||||
|
||||
Reference in New Issue
Block a user