Merge branch 'bluesky-social:main' into patch-3
commit
ab2b454be8
|
@ -180,6 +180,7 @@ func serve(cctx *cli.Context) error {
|
|||
e.GET("/", server.WebHome)
|
||||
|
||||
// generic routes
|
||||
e.GET("/hashtag/:tag", server.WebGeneric)
|
||||
e.GET("/search", server.WebGeneric)
|
||||
e.GET("/feeds", server.WebGeneric)
|
||||
e.GET("/notifications", server.WebGeneric)
|
||||
|
|
|
@ -44,6 +44,12 @@
|
|||
scrollbar-gutter: stable both-edges;
|
||||
}
|
||||
|
||||
/* Buttons and inputs have a font set by UA, so we'll have to reset that */
|
||||
button, input, textarea {
|
||||
font: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
/* Color theming */
|
||||
/* Default will always be white */
|
||||
:root {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "bsky.app",
|
||||
"version": "1.70.0",
|
||||
"version": "1.71.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
|
@ -44,7 +44,7 @@
|
|||
"update-extensions": "scripts/updateExtensions.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.10.0",
|
||||
"@atproto/api": "^0.10.4",
|
||||
"@bam.tech/react-native-image-resizer": "^3.0.4",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
|
|
|
@ -77,6 +77,7 @@ import {PreferencesExternalEmbeds} from '#/view/screens/PreferencesExternalEmbed
|
|||
import {createNativeStackNavigatorWithAuth} from './view/shell/createNativeStackNavigatorWithAuth'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {i18n, MessageDescriptor} from '@lingui/core'
|
||||
import HashtagScreen from '#/screens/Hashtag'
|
||||
|
||||
const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
|
||||
|
||||
|
@ -262,6 +263,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
|||
requireAuth: true,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Hashtag"
|
||||
getComponent={() => HashtagScreen}
|
||||
options={{title: title(msg`Hashtag`)}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -479,12 +485,19 @@ const LINKING = {
|
|||
},
|
||||
|
||||
getStateFromPath(path: string) {
|
||||
const [name, params] = router.matchPath(path)
|
||||
|
||||
// Any time we receive a url that starts with `intent/` we want to ignore it here. It will be handled in the
|
||||
// intent handler hook. We should check for the trailing slash, because if there isn't one then it isn't a valid
|
||||
// intent
|
||||
if (path.includes('intent/')) return
|
||||
// On web, there is no route state that's created by default, so we should initialize it as the home route. On
|
||||
// native, since the home tab and the home screen are defined as initial routes, we don't need to return a state
|
||||
// since it will be created by react-navigation.
|
||||
if (path.includes('intent/')) {
|
||||
if (isNative) return
|
||||
return buildStateObject('Flat', 'Home', params)
|
||||
}
|
||||
|
||||
const [name, params] = router.matchPath(path)
|
||||
if (isNative) {
|
||||
if (name === 'Search') {
|
||||
return buildStateObject('SearchTab', 'Search', params)
|
||||
|
|
|
@ -17,7 +17,7 @@ const breakpoints: {
|
|||
[key: string]: number
|
||||
} = {
|
||||
gtMobile: 800,
|
||||
gtTablet: 1200,
|
||||
gtTablet: 1300,
|
||||
}
|
||||
function getActiveBreakpoints({width}: {width: number}) {
|
||||
const active: (keyof typeof breakpoints)[] = Object.keys(breakpoints).filter(
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import * as tokens from '#/alf/tokens'
|
||||
import type {Mutable} from '#/alf/types'
|
||||
import {atoms} from '#/alf/atoms'
|
||||
import {BLUE_HUE, GREEN_HUE, RED_HUE} from '#/alf/util/colorGeneration'
|
||||
|
||||
export type ThemeName = 'light' | 'dim' | 'dark'
|
||||
export type ReadonlyTheme = typeof light
|
||||
|
@ -73,19 +74,19 @@ export const darkPalette: Palette = {
|
|||
white: tokens.color.gray_0,
|
||||
black: tokens.color.trueBlack,
|
||||
|
||||
contrast_25: `hsl(211, 28%, 8%)`,
|
||||
contrast_50: `hsl(211, 28%, 11%)`,
|
||||
contrast_100: `hsl(211, 28%, 16%)`,
|
||||
contrast_200: `hsl(211, 28%, 24%)`,
|
||||
contrast_300: `hsl(211, 24%, 31%)`,
|
||||
contrast_400: `hsl(211, 24%, 38%)`,
|
||||
contrast_500: `hsl(211, 20%, 44%)`,
|
||||
contrast_600: `hsl(211, 20%, 55%)`,
|
||||
contrast_700: `hsl(211, 20%, 63%)`,
|
||||
contrast_800: `hsl(211, 20%, 71%)`,
|
||||
contrast_900: `hsl(211, 20%, 79%)`,
|
||||
contrast_950: `hsl(211, 20%, 87%)`,
|
||||
contrast_975: `hsl(211, 20%, 95%)`,
|
||||
contrast_25: tokens.color.gray_1000,
|
||||
contrast_50: tokens.color.gray_975,
|
||||
contrast_100: tokens.color.gray_950,
|
||||
contrast_200: tokens.color.gray_900,
|
||||
contrast_300: tokens.color.gray_800,
|
||||
contrast_400: tokens.color.gray_700,
|
||||
contrast_500: tokens.color.gray_600,
|
||||
contrast_600: tokens.color.gray_500,
|
||||
contrast_700: tokens.color.gray_400,
|
||||
contrast_800: tokens.color.gray_300,
|
||||
contrast_900: tokens.color.gray_200,
|
||||
contrast_950: tokens.color.gray_100,
|
||||
contrast_975: tokens.color.gray_50,
|
||||
|
||||
primary_25: tokens.color.blue_25,
|
||||
primary_50: tokens.color.blue_50,
|
||||
|
@ -132,28 +133,63 @@ export const darkPalette: Palette = {
|
|||
|
||||
export const dimPalette: Palette = {
|
||||
...darkPalette,
|
||||
black: `hsl(211, 28%, 12%)`,
|
||||
black: `hsl(${BLUE_HUE}, 28%, ${tokens.dimScale[0]}%)`,
|
||||
|
||||
contrast_25: `hsl(211, 28%, 15%)`,
|
||||
contrast_50: `hsl(211, 28%, 18%)`,
|
||||
contrast_100: `hsl(211, 28%, 24%)`,
|
||||
contrast_200: `hsl(211, 28%, 27%)`,
|
||||
contrast_300: `hsl(211, 24%, 34%)`,
|
||||
contrast_400: `hsl(211, 24%, 41%)`,
|
||||
contrast_500: `hsl(211, 20%, 52%)`,
|
||||
contrast_600: `hsl(211, 20%, 55%)`,
|
||||
contrast_700: `hsl(211, 20%, 67%)`,
|
||||
contrast_800: `hsl(211, 20%, 71%)`,
|
||||
contrast_900: `hsl(211, 20%, 79%)`,
|
||||
contrast_950: `hsl(211, 20%, 87%)`,
|
||||
contrast_975: `hsl(211, 20%, 95%)`,
|
||||
contrast_25: `hsl(${BLUE_HUE}, 28%, ${tokens.dimScale[1]}%)`,
|
||||
contrast_50: `hsl(${BLUE_HUE}, 28%, ${tokens.dimScale[2]}%)`,
|
||||
contrast_100: `hsl(${BLUE_HUE}, 28%, ${tokens.dimScale[3]}%)`,
|
||||
contrast_200: `hsl(${BLUE_HUE}, 28%, ${tokens.dimScale[4]}%)`,
|
||||
contrast_300: `hsl(${BLUE_HUE}, 24%, ${tokens.dimScale[5]}%)`,
|
||||
contrast_400: `hsl(${BLUE_HUE}, 24%, ${tokens.dimScale[6]}%)`,
|
||||
contrast_500: `hsl(${BLUE_HUE}, 20%, ${tokens.dimScale[7]}%)`,
|
||||
contrast_600: `hsl(${BLUE_HUE}, 20%, ${tokens.dimScale[8]}%)`,
|
||||
contrast_700: `hsl(${BLUE_HUE}, 20%, ${tokens.dimScale[9]}%)`,
|
||||
contrast_800: `hsl(${BLUE_HUE}, 20%, ${tokens.dimScale[10]}%)`,
|
||||
contrast_900: `hsl(${BLUE_HUE}, 20%, ${tokens.dimScale[11]}%)`,
|
||||
contrast_950: `hsl(${BLUE_HUE}, 20%, ${tokens.dimScale[12]}%)`,
|
||||
contrast_975: `hsl(${BLUE_HUE}, 20%, ${tokens.dimScale[13]}%)`,
|
||||
|
||||
primary_600: `hsl(211, 95%, 39%)`,
|
||||
primary_700: `hsl(211, 90%, 30%)`,
|
||||
primary_800: `hsl(211, 90%, 23%)`,
|
||||
primary_900: `hsl(211, 80%, 16%)`,
|
||||
primary_950: `hsl(211, 80%, 13%)`,
|
||||
primary_975: `hsl(211, 80%, 10%)`,
|
||||
primary_25: `hsl(${BLUE_HUE}, 99%, ${tokens.dimScale[13]}%)`,
|
||||
primary_50: `hsl(${BLUE_HUE}, 99%, ${tokens.dimScale[12]}%)`,
|
||||
primary_100: `hsl(${BLUE_HUE}, 99%, ${tokens.dimScale[11]}%)`,
|
||||
primary_200: `hsl(${BLUE_HUE}, 99%, ${tokens.dimScale[10]}%)`,
|
||||
primary_300: `hsl(${BLUE_HUE}, 99%, ${tokens.dimScale[9]}%)`,
|
||||
primary_400: `hsl(${BLUE_HUE}, 99%, ${tokens.dimScale[8]}%)`,
|
||||
primary_500: `hsl(${BLUE_HUE}, 99%, ${tokens.dimScale[7]}%)`,
|
||||
primary_600: `hsl(${BLUE_HUE}, 95%, ${tokens.dimScale[6]}%)`,
|
||||
primary_700: `hsl(${BLUE_HUE}, 90%, ${tokens.dimScale[5]}%)`,
|
||||
primary_800: `hsl(${BLUE_HUE}, 82%, ${tokens.dimScale[4]}%)`,
|
||||
primary_900: `hsl(${BLUE_HUE}, 70%, ${tokens.dimScale[3]}%)`,
|
||||
primary_950: `hsl(${BLUE_HUE}, 60%, ${tokens.dimScale[2]}%)`,
|
||||
primary_975: `hsl(${BLUE_HUE}, 50%, ${tokens.dimScale[1]}%)`,
|
||||
|
||||
positive_25: `hsl(${GREEN_HUE}, 82%, ${tokens.dimScale[13]}%)`,
|
||||
positive_50: `hsl(${GREEN_HUE}, 82%, ${tokens.dimScale[12]}%)`,
|
||||
positive_100: `hsl(${GREEN_HUE}, 82%, ${tokens.dimScale[11]}%)`,
|
||||
positive_200: `hsl(${GREEN_HUE}, 82%, ${tokens.dimScale[10]}%)`,
|
||||
positive_300: `hsl(${GREEN_HUE}, 82%, ${tokens.dimScale[9]}%)`,
|
||||
positive_400: `hsl(${GREEN_HUE}, 82%, ${tokens.dimScale[8]}%)`,
|
||||
positive_500: `hsl(${GREEN_HUE}, 82%, ${tokens.dimScale[7]}%)`,
|
||||
positive_600: `hsl(${GREEN_HUE}, 82%, ${tokens.dimScale[6]}%)`,
|
||||
positive_700: `hsl(${GREEN_HUE}, 82%, ${tokens.dimScale[5]}%)`,
|
||||
positive_800: `hsl(${GREEN_HUE}, 82%, ${tokens.dimScale[4]}%)`,
|
||||
positive_900: `hsl(${GREEN_HUE}, 70%, ${tokens.dimScale[3]}%)`,
|
||||
positive_950: `hsl(${GREEN_HUE}, 60%, ${tokens.dimScale[2]}%)`,
|
||||
positive_975: `hsl(${GREEN_HUE}, 50%, ${tokens.dimScale[1]}%)`,
|
||||
|
||||
negative_25: `hsl(${RED_HUE}, 91%, ${tokens.dimScale[13]}%)`,
|
||||
negative_50: `hsl(${RED_HUE}, 91%, ${tokens.dimScale[12]}%)`,
|
||||
negative_100: `hsl(${RED_HUE}, 91%, ${tokens.dimScale[11]}%)`,
|
||||
negative_200: `hsl(${RED_HUE}, 91%, ${tokens.dimScale[10]}%)`,
|
||||
negative_300: `hsl(${RED_HUE}, 91%, ${tokens.dimScale[9]}%)`,
|
||||
negative_400: `hsl(${RED_HUE}, 91%, ${tokens.dimScale[8]}%)`,
|
||||
negative_500: `hsl(${RED_HUE}, 91%, ${tokens.dimScale[7]}%)`,
|
||||
negative_600: `hsl(${RED_HUE}, 91%, ${tokens.dimScale[6]}%)`,
|
||||
negative_700: `hsl(${RED_HUE}, 91%, ${tokens.dimScale[5]}%)`,
|
||||
negative_800: `hsl(${RED_HUE}, 88%, ${tokens.dimScale[4]}%)`,
|
||||
negative_900: `hsl(${RED_HUE}, 84%, ${tokens.dimScale[3]}%)`,
|
||||
negative_950: `hsl(${RED_HUE}, 80%, ${tokens.dimScale[2]}%)`,
|
||||
negative_975: `hsl(${RED_HUE}, 70%, ${tokens.dimScale[1]}%)`,
|
||||
} as const
|
||||
|
||||
export const light = {
|
||||
|
@ -404,17 +440,17 @@ export const dim: Theme = {
|
|||
shadow_sm: {
|
||||
...atoms.shadow_sm,
|
||||
shadowOpacity: 0.7,
|
||||
shadowColor: `hsl(211, 28%, 3%)`,
|
||||
shadowColor: `hsl(${BLUE_HUE}, 28%, 6%)`,
|
||||
},
|
||||
shadow_md: {
|
||||
...atoms.shadow_md,
|
||||
shadowOpacity: 0.7,
|
||||
shadowColor: `hsl(211, 28%, 3%)`,
|
||||
shadowColor: `hsl(${BLUE_HUE}, 28%, 6%)`,
|
||||
},
|
||||
shadow_lg: {
|
||||
...atoms.shadow_lg,
|
||||
shadowOpacity: 0.7,
|
||||
shadowColor: `hsl(211, 28%, 3%)`,
|
||||
shadowColor: `hsl(${BLUE_HUE}, 28%, 6%)`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,25 +1,32 @@
|
|||
const BLUE_HUE = 211
|
||||
const RED_HUE = 346
|
||||
const GREEN_HUE = 152
|
||||
import {
|
||||
BLUE_HUE,
|
||||
RED_HUE,
|
||||
GREEN_HUE,
|
||||
generateScale,
|
||||
} 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',
|
||||
|
||||
gray_0: `hsl(${BLUE_HUE}, 20%, 100%)`,
|
||||
gray_25: `hsl(${BLUE_HUE}, 20%, 97%)`,
|
||||
gray_50: `hsl(${BLUE_HUE}, 20%, 95%)`,
|
||||
gray_100: `hsl(${BLUE_HUE}, 20%, 90%)`,
|
||||
gray_200: `hsl(${BLUE_HUE}, 20%, 80%)`,
|
||||
gray_300: `hsl(${BLUE_HUE}, 20%, 70%)`,
|
||||
gray_400: `hsl(${BLUE_HUE}, 20%, 60%)`,
|
||||
gray_500: `hsl(${BLUE_HUE}, 20%, 50%)`,
|
||||
gray_600: `hsl(${BLUE_HUE}, 24%, 42%)`,
|
||||
gray_700: `hsl(${BLUE_HUE}, 24%, 34%)`,
|
||||
gray_800: `hsl(${BLUE_HUE}, 28%, 26%)`,
|
||||
gray_900: `hsl(${BLUE_HUE}, 28%, 18%)`,
|
||||
gray_950: `hsl(${BLUE_HUE}, 28%, 10%)`,
|
||||
gray_975: `hsl(${BLUE_HUE}, 28%, 7%)`,
|
||||
gray_1000: `hsl(${BLUE_HUE}, 28%, 4%)`,
|
||||
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%)`,
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
export const BLUE_HUE = 211
|
||||
export const RED_HUE = 346
|
||||
export const GREEN_HUE = 152
|
||||
|
||||
/**
|
||||
* Smooth progression of lightness "stops" for generating HSL colors.
|
||||
*/
|
||||
export const COLOR_STOPS = [
|
||||
0, 0.05, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.85, 0.9, 0.95, 1,
|
||||
]
|
||||
|
||||
export function generateScale(start: number, end: number) {
|
||||
const range = end - start
|
||||
return COLOR_STOPS.map(stop => {
|
||||
return start + range * stop
|
||||
})
|
||||
}
|
|
@ -165,7 +165,7 @@ export function Button({
|
|||
|
||||
if (!disabled) {
|
||||
baseStyles.push(a.border, {
|
||||
borderColor: tokens.color.blue_500,
|
||||
borderColor: t.palette.primary_500,
|
||||
})
|
||||
hoverStyles.push(a.border, {
|
||||
backgroundColor: light
|
||||
|
@ -174,7 +174,7 @@ export function Button({
|
|||
})
|
||||
} else {
|
||||
baseStyles.push(a.border, {
|
||||
borderColor: light ? tokens.color.blue_200 : tokens.color.blue_900,
|
||||
borderColor: light ? t.palette.primary_200 : t.palette.primary_900,
|
||||
})
|
||||
}
|
||||
} else if (variant === 'ghost') {
|
||||
|
@ -191,20 +191,14 @@ export function Button({
|
|||
if (variant === 'solid') {
|
||||
if (!disabled) {
|
||||
baseStyles.push({
|
||||
backgroundColor: light
|
||||
? tokens.color.gray_50
|
||||
: tokens.color.gray_900,
|
||||
backgroundColor: t.palette.contrast_50,
|
||||
})
|
||||
hoverStyles.push({
|
||||
backgroundColor: light
|
||||
? tokens.color.gray_100
|
||||
: tokens.color.gray_950,
|
||||
backgroundColor: t.palette.contrast_100,
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({
|
||||
backgroundColor: light
|
||||
? tokens.color.gray_200
|
||||
: tokens.color.gray_950,
|
||||
backgroundColor: t.palette.contrast_200,
|
||||
})
|
||||
}
|
||||
} else if (variant === 'outline') {
|
||||
|
@ -214,21 +208,19 @@ export function Button({
|
|||
|
||||
if (!disabled) {
|
||||
baseStyles.push(a.border, {
|
||||
borderColor: light ? tokens.color.gray_300 : tokens.color.gray_700,
|
||||
borderColor: t.palette.contrast_300,
|
||||
})
|
||||
hoverStyles.push(a.border, t.atoms.bg_contrast_50)
|
||||
hoverStyles.push(t.atoms.bg_contrast_50)
|
||||
} else {
|
||||
baseStyles.push(a.border, {
|
||||
borderColor: light ? tokens.color.gray_200 : tokens.color.gray_800,
|
||||
borderColor: t.palette.contrast_200,
|
||||
})
|
||||
}
|
||||
} else if (variant === 'ghost') {
|
||||
if (!disabled) {
|
||||
baseStyles.push(t.atoms.bg)
|
||||
hoverStyles.push({
|
||||
backgroundColor: light
|
||||
? tokens.color.gray_100
|
||||
: tokens.color.gray_900,
|
||||
backgroundColor: t.palette.contrast_100,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -236,14 +228,14 @@ export function Button({
|
|||
if (variant === 'solid') {
|
||||
if (!disabled) {
|
||||
baseStyles.push({
|
||||
backgroundColor: t.palette.negative_400,
|
||||
backgroundColor: t.palette.negative_500,
|
||||
})
|
||||
hoverStyles.push({
|
||||
backgroundColor: t.palette.negative_500,
|
||||
backgroundColor: t.palette.negative_600,
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({
|
||||
backgroundColor: t.palette.negative_600,
|
||||
backgroundColor: t.palette.negative_700,
|
||||
})
|
||||
}
|
||||
} else if (variant === 'outline') {
|
||||
|
@ -253,7 +245,7 @@ export function Button({
|
|||
|
||||
if (!disabled) {
|
||||
baseStyles.push(a.border, {
|
||||
borderColor: t.palette.negative_400,
|
||||
borderColor: t.palette.negative_500,
|
||||
})
|
||||
hoverStyles.push(a.border, {
|
||||
backgroundColor: light
|
||||
|
@ -273,7 +265,7 @@ export function Button({
|
|||
hoverStyles.push({
|
||||
backgroundColor: light
|
||||
? t.palette.negative_100
|
||||
: t.palette.negative_950,
|
||||
: t.palette.negative_975,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -461,31 +453,31 @@ export function useSharedButtonTextStyles() {
|
|||
if (variant === 'solid' || variant === 'gradient') {
|
||||
if (!disabled) {
|
||||
baseStyles.push({
|
||||
color: light ? tokens.color.gray_700 : tokens.color.gray_100,
|
||||
color: t.palette.contrast_700,
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({
|
||||
color: light ? tokens.color.gray_400 : tokens.color.gray_700,
|
||||
color: t.palette.contrast_400,
|
||||
})
|
||||
}
|
||||
} else if (variant === 'outline') {
|
||||
if (!disabled) {
|
||||
baseStyles.push({
|
||||
color: light ? tokens.color.gray_600 : tokens.color.gray_300,
|
||||
color: t.palette.contrast_600,
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({
|
||||
color: light ? tokens.color.gray_400 : tokens.color.gray_700,
|
||||
color: t.palette.contrast_300,
|
||||
})
|
||||
}
|
||||
} else if (variant === 'ghost') {
|
||||
if (!disabled) {
|
||||
baseStyles.push({
|
||||
color: light ? tokens.color.gray_600 : tokens.color.gray_300,
|
||||
color: t.palette.contrast_600,
|
||||
})
|
||||
} else {
|
||||
baseStyles.push({
|
||||
color: light ? tokens.color.gray_400 : tokens.color.gray_600,
|
||||
color: t.palette.contrast_300,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
import React, {useImperativeHandle} from 'react'
|
||||
import {View, Dimensions} from 'react-native'
|
||||
import {View, Dimensions, Keyboard, Pressable} from 'react-native'
|
||||
import BottomSheet, {
|
||||
BottomSheetBackdrop,
|
||||
BottomSheetBackdropProps,
|
||||
BottomSheetScrollView,
|
||||
BottomSheetTextInput,
|
||||
BottomSheetView,
|
||||
useBottomSheet,
|
||||
WINDOW_HEIGHT,
|
||||
} from '@gorhom/bottom-sheet'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import Animated, {useAnimatedStyle} from 'react-native-reanimated'
|
||||
|
||||
import {useTheme, atoms as a, flatten} from '#/alf'
|
||||
import {Portal} from '#/components/Portal'
|
||||
|
@ -26,6 +29,47 @@ export * from '#/components/Dialog/types'
|
|||
// @ts-ignore
|
||||
export const Input = createInput(BottomSheetTextInput)
|
||||
|
||||
function Backdrop(props: BottomSheetBackdropProps) {
|
||||
const t = useTheme()
|
||||
const bottomSheet = useBottomSheet()
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => {
|
||||
const opacity =
|
||||
(Math.abs(WINDOW_HEIGHT - props.animatedPosition.value) - 50) / 1000
|
||||
|
||||
return {
|
||||
opacity: Math.min(Math.max(opacity, 0), 0.55),
|
||||
}
|
||||
})
|
||||
|
||||
const onPress = React.useCallback(() => {
|
||||
bottomSheet.close()
|
||||
}, [bottomSheet])
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[
|
||||
t.atoms.bg_contrast_300,
|
||||
{
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
position: 'absolute',
|
||||
},
|
||||
animatedStyle,
|
||||
]}>
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Dialog backdrop"
|
||||
accessibilityHint="Press the backdrop to close the dialog"
|
||||
style={{flex: 1}}
|
||||
onPress={onPress}
|
||||
/>
|
||||
</Animated.View>
|
||||
)
|
||||
}
|
||||
|
||||
export function Outer({
|
||||
children,
|
||||
control,
|
||||
|
@ -78,6 +122,7 @@ export function Outer({
|
|||
const onChange = React.useCallback(
|
||||
(index: number) => {
|
||||
if (index === -1) {
|
||||
Keyboard.dismiss()
|
||||
try {
|
||||
closeCallback.current?.()
|
||||
} catch (e: any) {
|
||||
|
@ -113,15 +158,7 @@ export function Outer({
|
|||
ref={sheet}
|
||||
index={openIndex}
|
||||
backgroundStyle={{backgroundColor: 'transparent'}}
|
||||
backdropComponent={props => (
|
||||
<BottomSheetBackdrop
|
||||
opacity={0.4}
|
||||
appearsOnIndex={0}
|
||||
disappearsOnIndex={-1}
|
||||
{...props}
|
||||
style={[flatten(props.style), t.atoms.bg_contrast_300]}
|
||||
/>
|
||||
)}
|
||||
backdropComponent={Backdrop}
|
||||
handleIndicatorStyle={{backgroundColor: t.palette.primary_500}}
|
||||
handleStyle={{display: 'none'}}
|
||||
onChange={onChange}>
|
||||
|
@ -190,8 +227,15 @@ export function ScrollableInner({children, style}: DialogInnerProps) {
|
|||
|
||||
export function Handle() {
|
||||
const t = useTheme()
|
||||
|
||||
const onTouchStart = React.useCallback(() => {
|
||||
Keyboard.dismiss()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<View style={[a.absolute, a.w_full, a.align_center, a.z_10, {height: 40}]}>
|
||||
<View
|
||||
style={[a.absolute, a.w_full, a.align_center, a.z_10, {height: 40}]}
|
||||
onTouchStart={onTouchStart}>
|
||||
<View
|
||||
style={[
|
||||
a.rounded_sm,
|
||||
|
|
|
@ -49,7 +49,7 @@ type BaseLinkProps = Pick<
|
|||
*
|
||||
* Note: atm this only works for `InlineLink`s with a string child.
|
||||
*/
|
||||
warnOnMismatchingTextChild?: boolean
|
||||
disableMismatchWarning?: boolean
|
||||
|
||||
/**
|
||||
* Callback for when the link is pressed. Prevent default and return `false`
|
||||
|
@ -69,7 +69,7 @@ export function useLink({
|
|||
to,
|
||||
displayText,
|
||||
action = 'push',
|
||||
warnOnMismatchingTextChild,
|
||||
disableMismatchWarning,
|
||||
onPress: outerOnPress,
|
||||
}: BaseLinkProps & {
|
||||
displayText: string
|
||||
|
@ -90,7 +90,7 @@ export function useLink({
|
|||
if (exitEarlyIfFalse === false) return
|
||||
|
||||
const requiresWarning = Boolean(
|
||||
warnOnMismatchingTextChild &&
|
||||
!disableMismatchWarning &&
|
||||
displayText &&
|
||||
isExternal &&
|
||||
linkRequiresWarning(href, displayText),
|
||||
|
@ -148,7 +148,7 @@ export function useLink({
|
|||
},
|
||||
[
|
||||
outerOnPress,
|
||||
warnOnMismatchingTextChild,
|
||||
disableMismatchWarning,
|
||||
displayText,
|
||||
isExternal,
|
||||
href,
|
||||
|
@ -167,7 +167,7 @@ export function useLink({
|
|||
}
|
||||
}
|
||||
|
||||
export type LinkProps = Omit<BaseLinkProps, 'warnOnMismatchingTextChild'> &
|
||||
export type LinkProps = Omit<BaseLinkProps, 'disableMismatchWarning'> &
|
||||
Omit<ButtonProps, 'onPress' | 'disabled' | 'label'>
|
||||
|
||||
/**
|
||||
|
@ -226,7 +226,7 @@ export function InlineLink({
|
|||
children,
|
||||
to,
|
||||
action = 'push',
|
||||
warnOnMismatchingTextChild,
|
||||
disableMismatchWarning,
|
||||
style,
|
||||
onPress: outerOnPress,
|
||||
download,
|
||||
|
@ -239,7 +239,7 @@ export function InlineLink({
|
|||
to,
|
||||
displayText: stringChildren ? children : '',
|
||||
action,
|
||||
warnOnMismatchingTextChild,
|
||||
disableMismatchWarning,
|
||||
onPress: outerOnPress,
|
||||
})
|
||||
const {
|
||||
|
|
|
@ -0,0 +1,246 @@
|
|||
import React from 'react'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {View} from 'react-native'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Trans} from '@lingui/macro'
|
||||
import {cleanError} from 'lib/strings/errors'
|
||||
import {Button} from '#/components/Button'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {StackActions} from '@react-navigation/native'
|
||||
import {useNavigation} from '@react-navigation/core'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
import {router} from '#/routes'
|
||||
|
||||
export function ListFooter({
|
||||
isFetching,
|
||||
isError,
|
||||
error,
|
||||
onRetry,
|
||||
}: {
|
||||
isFetching: boolean
|
||||
isError: boolean
|
||||
error?: string
|
||||
onRetry?: () => Promise<unknown>
|
||||
}) {
|
||||
const t = useTheme()
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
a.border_t,
|
||||
a.pb_lg,
|
||||
t.atoms.border_contrast_low,
|
||||
{height: 100},
|
||||
]}>
|
||||
{isFetching ? (
|
||||
<Loader size="xl" />
|
||||
) : (
|
||||
<ListFooterMaybeError
|
||||
isError={isError}
|
||||
error={error}
|
||||
onRetry={onRetry}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function ListFooterMaybeError({
|
||||
isError,
|
||||
error,
|
||||
onRetry,
|
||||
}: {
|
||||
isError: boolean
|
||||
error?: string
|
||||
onRetry?: () => Promise<unknown>
|
||||
}) {
|
||||
const t = useTheme()
|
||||
|
||||
if (!isError) return null
|
||||
|
||||
return (
|
||||
<View style={[a.w_full, a.px_lg]}>
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.gap_md,
|
||||
a.p_md,
|
||||
a.rounded_sm,
|
||||
a.align_center,
|
||||
t.atoms.bg_contrast_25,
|
||||
]}>
|
||||
<Text
|
||||
style={[a.flex_1, a.text_sm, t.atoms.text_contrast_medium]}
|
||||
numberOfLines={2}>
|
||||
{error ? (
|
||||
cleanError(error)
|
||||
) : (
|
||||
<Trans>Oops, something went wrong!</Trans>
|
||||
)}
|
||||
</Text>
|
||||
<Button
|
||||
variant="gradient"
|
||||
label="Press to retry"
|
||||
style={[
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
a.rounded_sm,
|
||||
a.overflow_hidden,
|
||||
a.px_md,
|
||||
a.py_sm,
|
||||
]}
|
||||
onPress={onRetry}>
|
||||
Retry
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function ListHeaderDesktop({
|
||||
title,
|
||||
subtitle,
|
||||
}: {
|
||||
title: string
|
||||
subtitle?: string
|
||||
}) {
|
||||
const {gtTablet} = useBreakpoints()
|
||||
const t = useTheme()
|
||||
|
||||
if (!gtTablet) return null
|
||||
|
||||
return (
|
||||
<View style={[a.w_full, a.py_lg, a.px_xl, a.gap_xs]}>
|
||||
<Text style={[a.text_3xl, a.font_bold]}>{title}</Text>
|
||||
{subtitle ? (
|
||||
<Text style={[a.text_md, t.atoms.text_contrast_medium]}>
|
||||
{subtitle}
|
||||
</Text>
|
||||
) : undefined}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function ListMaybePlaceholder({
|
||||
isLoading,
|
||||
isEmpty,
|
||||
isError,
|
||||
empty,
|
||||
error,
|
||||
notFoundType = 'page',
|
||||
onRetry,
|
||||
}: {
|
||||
isLoading: boolean
|
||||
isEmpty: boolean
|
||||
isError: boolean
|
||||
empty?: string
|
||||
error?: string
|
||||
notFoundType?: 'page' | 'results'
|
||||
onRetry?: () => Promise<unknown>
|
||||
}) {
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const t = useTheme()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
|
||||
const canGoBack = navigation.canGoBack()
|
||||
const onGoBack = React.useCallback(() => {
|
||||
if (canGoBack) {
|
||||
navigation.goBack()
|
||||
} else {
|
||||
navigation.navigate('HomeTab')
|
||||
|
||||
// Checking the state for routes ensures that web doesn't encounter errors while going back
|
||||
if (navigation.getState()?.routes) {
|
||||
navigation.dispatch(StackActions.push(...router.matchPath('/')))
|
||||
} else {
|
||||
navigation.navigate('HomeTab')
|
||||
navigation.dispatch(StackActions.popToTop())
|
||||
}
|
||||
}
|
||||
}, [navigation, canGoBack])
|
||||
|
||||
if (!isEmpty) return null
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.align_center,
|
||||
!gtMobile ? [a.justify_between, a.border_t] : a.gap_5xl,
|
||||
t.atoms.border_contrast_low,
|
||||
{paddingTop: 175, paddingBottom: 110},
|
||||
]}>
|
||||
{isLoading ? (
|
||||
<View style={[a.w_full, a.align_center, {top: 100}]}>
|
||||
<Loader size="xl" />
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<View style={[a.w_full, a.align_center, a.gap_lg]}>
|
||||
<Text style={[a.font_bold, a.text_3xl]}>
|
||||
{isError ? (
|
||||
<Trans>Oops!</Trans>
|
||||
) : isEmpty ? (
|
||||
<>
|
||||
{notFoundType === 'results' ? (
|
||||
<Trans>No results found</Trans>
|
||||
) : (
|
||||
<Trans>Page not found</Trans>
|
||||
)}
|
||||
</>
|
||||
) : undefined}
|
||||
</Text>
|
||||
|
||||
{isError ? (
|
||||
<Text
|
||||
style={[a.text_md, a.text_center, t.atoms.text_contrast_high]}>
|
||||
{error ? error : <Trans>Something went wrong!</Trans>}
|
||||
</Text>
|
||||
) : isEmpty ? (
|
||||
<Text
|
||||
style={[a.text_md, a.text_center, t.atoms.text_contrast_high]}>
|
||||
{empty ? (
|
||||
empty
|
||||
) : (
|
||||
<Trans>
|
||||
We're sorry! We can't find the page you were looking for.
|
||||
</Trans>
|
||||
)}
|
||||
</Text>
|
||||
) : undefined}
|
||||
</View>
|
||||
<View
|
||||
style={[a.gap_md, !gtMobile ? [a.w_full, a.px_lg] : {width: 350}]}>
|
||||
{isError && onRetry && (
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
label="Click here"
|
||||
onPress={onRetry}
|
||||
size="large"
|
||||
style={[
|
||||
a.rounded_sm,
|
||||
a.overflow_hidden,
|
||||
{paddingVertical: 10},
|
||||
]}>
|
||||
Retry
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="solid"
|
||||
color={isError && onRetry ? 'secondary' : 'primary'}
|
||||
label="Click here"
|
||||
onPress={onGoBack}
|
||||
size="large"
|
||||
style={[a.rounded_sm, a.overflow_hidden, {paddingVertical: 10}]}>
|
||||
Go Back
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
|
@ -105,8 +105,7 @@ export function RichText({
|
|||
to={link.uri}
|
||||
style={[...styles, {pointerEvents: 'auto'}]}
|
||||
// @ts-ignore TODO
|
||||
dataSet={WORD_WRAP}
|
||||
warnOnMismatchingLabel>
|
||||
dataSet={WORD_WRAP}>
|
||||
{toShortUrl(segment.text)}
|
||||
</InlineLink>,
|
||||
)
|
||||
|
@ -121,6 +120,7 @@ export function RichText({
|
|||
<RichTextTag
|
||||
key={key}
|
||||
text={segment.text}
|
||||
tag={tag.tag}
|
||||
style={styles}
|
||||
selectable={selectable}
|
||||
authorHandle={authorHandle}
|
||||
|
@ -146,12 +146,14 @@ export function RichText({
|
|||
}
|
||||
|
||||
function RichTextTag({
|
||||
text: tag,
|
||||
text,
|
||||
tag,
|
||||
style,
|
||||
selectable,
|
||||
authorHandle,
|
||||
}: {
|
||||
text: string
|
||||
tag: string
|
||||
selectable?: boolean
|
||||
authorHandle?: string
|
||||
} & TextStyleProp) {
|
||||
|
@ -185,8 +187,8 @@ function RichTextTag({
|
|||
<Text
|
||||
selectable={selectable}
|
||||
{...native({
|
||||
accessibilityLabel: _(msg`Hashtag: ${tag}`),
|
||||
accessibilityHint: _(msg`Click here to open tag menu for ${tag}`),
|
||||
accessibilityLabel: _(msg`Hashtag: #${tag}`),
|
||||
accessibilityHint: _(msg`Click here to open tag menu for #${tag}`),
|
||||
accessibilityRole: isNative ? 'button' : undefined,
|
||||
onPress: open,
|
||||
onPressIn: onPressIn,
|
||||
|
@ -214,7 +216,7 @@ function RichTextTag({
|
|||
textDecorationColor: t.palette.primary_500,
|
||||
},
|
||||
]}>
|
||||
{tag}
|
||||
{text}
|
||||
</Text>
|
||||
</TagMenu>
|
||||
</React.Fragment>
|
||||
|
|
|
@ -34,6 +34,10 @@ export function TagMenu({
|
|||
authorHandle,
|
||||
}: React.PropsWithChildren<{
|
||||
control: Dialog.DialogOuterProps['control']
|
||||
/**
|
||||
* This should be the sanitized tag value from the facet itself, not the
|
||||
* "display" value with a leading `#`.
|
||||
*/
|
||||
tag: string
|
||||
authorHandle?: string
|
||||
}>) {
|
||||
|
@ -52,16 +56,16 @@ export function TagMenu({
|
|||
variables: optimisticRemove,
|
||||
reset: resetRemove,
|
||||
} = useRemoveMutedWordMutation()
|
||||
const displayTag = '#' + tag
|
||||
|
||||
const sanitizedTag = tag.replace(/^#/, '')
|
||||
const isMuted = Boolean(
|
||||
(preferences?.mutedWords?.find(
|
||||
m => m.value === sanitizedTag && m.targets.includes('tag'),
|
||||
m => m.value === tag && m.targets.includes('tag'),
|
||||
) ??
|
||||
optimisticUpsert?.find(
|
||||
m => m.value === sanitizedTag && m.targets.includes('tag'),
|
||||
m => m.value === tag && m.targets.includes('tag'),
|
||||
)) &&
|
||||
!(optimisticRemove?.value === sanitizedTag),
|
||||
!(optimisticRemove?.value === tag),
|
||||
)
|
||||
|
||||
return (
|
||||
|
@ -71,7 +75,7 @@ export function TagMenu({
|
|||
<Dialog.Outer control={control}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.Inner label={_(msg`Tag menu: ${tag}`)}>
|
||||
<Dialog.Inner label={_(msg`Tag menu: ${displayTag}`)}>
|
||||
{isPreferencesLoading ? (
|
||||
<View style={[a.w_full, a.align_center]}>
|
||||
<Loader size="lg" />
|
||||
|
@ -87,18 +91,14 @@ export function TagMenu({
|
|||
t.atoms.bg_contrast_25,
|
||||
]}>
|
||||
<Link
|
||||
label={_(msg`Search for all posts with tag ${tag}`)}
|
||||
to={makeSearchLink({query: tag})}
|
||||
label={_(msg`Search for all posts with tag ${displayTag}`)}
|
||||
to={makeSearchLink({query: displayTag})}
|
||||
onPress={e => {
|
||||
e.preventDefault()
|
||||
|
||||
control.close(() => {
|
||||
// @ts-ignore :ron_swanson: "I know more than you"
|
||||
navigation.navigate('SearchTab', {
|
||||
screen: 'Search',
|
||||
params: {
|
||||
q: tag,
|
||||
},
|
||||
navigation.push('Hashtag', {
|
||||
tag: tag.replaceAll('#', '%23'),
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -128,7 +128,7 @@ export function TagMenu({
|
|||
<Trans>
|
||||
See{' '}
|
||||
<Text style={[a.text_md, a.font_bold, t.atoms.text]}>
|
||||
{tag}
|
||||
{displayTag}
|
||||
</Text>{' '}
|
||||
posts
|
||||
</Trans>
|
||||
|
@ -142,21 +142,19 @@ export function TagMenu({
|
|||
|
||||
<Link
|
||||
label={_(
|
||||
msg`Search for all posts by @${authorHandle} with tag ${tag}`,
|
||||
msg`Search for all posts by @${authorHandle} with tag ${displayTag}`,
|
||||
)}
|
||||
to={makeSearchLink({query: tag, from: authorHandle})}
|
||||
to={makeSearchLink({
|
||||
query: displayTag,
|
||||
from: authorHandle,
|
||||
})}
|
||||
onPress={e => {
|
||||
e.preventDefault()
|
||||
|
||||
control.close(() => {
|
||||
// @ts-ignore :ron_swanson: "I know more than you"
|
||||
navigation.navigate('SearchTab', {
|
||||
screen: 'Search',
|
||||
params: {
|
||||
q:
|
||||
tag +
|
||||
(authorHandle ? ` from:${authorHandle}` : ''),
|
||||
},
|
||||
navigation.push('Hashtag', {
|
||||
tag: tag.replaceAll('#', '%23'),
|
||||
author: authorHandle,
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -190,7 +188,7 @@ export function TagMenu({
|
|||
See{' '}
|
||||
<Text
|
||||
style={[a.text_md, a.font_bold, t.atoms.text]}>
|
||||
{tag}
|
||||
{displayTag}
|
||||
</Text>{' '}
|
||||
posts by this user
|
||||
</Trans>
|
||||
|
@ -207,22 +205,20 @@ export function TagMenu({
|
|||
<Button
|
||||
label={
|
||||
isMuted
|
||||
? _(msg`Unmute all ${tag} posts`)
|
||||
: _(msg`Mute all ${tag} posts`)
|
||||
? _(msg`Unmute all ${displayTag} posts`)
|
||||
: _(msg`Mute all ${displayTag} posts`)
|
||||
}
|
||||
onPress={() => {
|
||||
control.close(() => {
|
||||
if (isMuted) {
|
||||
resetUpsert()
|
||||
removeMutedWord({
|
||||
value: sanitizedTag,
|
||||
value: tag,
|
||||
targets: ['tag'],
|
||||
})
|
||||
} else {
|
||||
resetRemove()
|
||||
upsertMutedWord([
|
||||
{value: sanitizedTag, targets: ['tag']},
|
||||
])
|
||||
upsertMutedWord([{value: tag, targets: ['tag']}])
|
||||
}
|
||||
})
|
||||
}}>
|
||||
|
@ -252,7 +248,7 @@ export function TagMenu({
|
|||
]}>
|
||||
{isMuted ? _(msg`Unmute`) : _(msg`Mute`)}{' '}
|
||||
<Text style={[a.text_md, a.font_bold, t.atoms.text]}>
|
||||
{tag}
|
||||
{displayTag}
|
||||
</Text>{' '}
|
||||
<Trans>posts</Trans>
|
||||
</Text>
|
||||
|
|
|
@ -14,18 +14,34 @@ import {
|
|||
} from '#/state/queries/preferences'
|
||||
import {enforceLen} from '#/lib/strings/helpers'
|
||||
import {web} from '#/alf'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
|
||||
export function useTagMenuControl() {}
|
||||
export function useTagMenuControl(): Dialog.DialogControlProps {
|
||||
return {
|
||||
id: '',
|
||||
// @ts-ignore
|
||||
ref: null,
|
||||
open: () => {
|
||||
throw new Error(`TagMenu controls are only available on native platforms`)
|
||||
},
|
||||
close: () => {
|
||||
throw new Error(`TagMenu controls are only available on native platforms`)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function TagMenu({
|
||||
children,
|
||||
tag,
|
||||
authorHandle,
|
||||
}: React.PropsWithChildren<{
|
||||
/**
|
||||
* This should be the sanitized tag value from the facet itself, not the
|
||||
* "display" value with a leading `#`.
|
||||
*/
|
||||
tag: string
|
||||
authorHandle?: string
|
||||
}>) {
|
||||
const sanitizedTag = tag.replace(/^#/, '')
|
||||
const {_} = useLingui()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {data: preferences} = usePreferencesQuery()
|
||||
|
@ -35,22 +51,22 @@ export function TagMenu({
|
|||
useRemoveMutedWordMutation()
|
||||
const isMuted = Boolean(
|
||||
(preferences?.mutedWords?.find(
|
||||
m => m.value === sanitizedTag && m.targets.includes('tag'),
|
||||
m => m.value === tag && m.targets.includes('tag'),
|
||||
) ??
|
||||
optimisticUpsert?.find(
|
||||
m => m.value === sanitizedTag && m.targets.includes('tag'),
|
||||
m => m.value === tag && m.targets.includes('tag'),
|
||||
)) &&
|
||||
!(optimisticRemove?.value === sanitizedTag),
|
||||
!(optimisticRemove?.value === tag),
|
||||
)
|
||||
const truncatedTag = enforceLen(tag, 15, true, 'middle')
|
||||
const truncatedTag = '#' + enforceLen(tag, 15, true, 'middle')
|
||||
|
||||
const dropdownItems = React.useMemo(() => {
|
||||
return [
|
||||
{
|
||||
label: _(msg`See ${truncatedTag} posts`),
|
||||
onPress() {
|
||||
navigation.navigate('Search', {
|
||||
q: tag,
|
||||
navigation.push('Hashtag', {
|
||||
tag: tag.replaceAll('#', '%23'),
|
||||
})
|
||||
},
|
||||
testID: 'tagMenuSearch',
|
||||
|
@ -66,11 +82,9 @@ export function TagMenu({
|
|||
!isInvalidHandle(authorHandle) && {
|
||||
label: _(msg`See ${truncatedTag} posts by user`),
|
||||
onPress() {
|
||||
navigation.navigate({
|
||||
name: 'Search',
|
||||
params: {
|
||||
q: tag + (authorHandle ? ` from:${authorHandle}` : ''),
|
||||
},
|
||||
navigation.push('Hashtag', {
|
||||
tag: tag.replaceAll('#', '%23'),
|
||||
author: authorHandle,
|
||||
})
|
||||
},
|
||||
testID: 'tagMenuSeachByUser',
|
||||
|
@ -91,9 +105,9 @@ export function TagMenu({
|
|||
: _(msg`Mute ${truncatedTag}`),
|
||||
onPress() {
|
||||
if (isMuted) {
|
||||
removeMutedWord({value: sanitizedTag, targets: ['tag']})
|
||||
removeMutedWord({value: tag, targets: ['tag']})
|
||||
} else {
|
||||
upsertMutedWord([{value: sanitizedTag, targets: ['tag']}])
|
||||
upsertMutedWord([{value: tag, targets: ['tag']}])
|
||||
}
|
||||
},
|
||||
testID: 'tagMenuMute',
|
||||
|
@ -114,7 +128,6 @@ export function TagMenu({
|
|||
preferences,
|
||||
tag,
|
||||
truncatedTag,
|
||||
sanitizedTag,
|
||||
upsertMutedWord,
|
||||
removeMutedWord,
|
||||
])
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {Keyboard, View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {AppBskyActorDefs} from '@atproto/api'
|
||||
import {AppBskyActorDefs, sanitizeMutedWordValue} from '@atproto/api'
|
||||
|
||||
import {
|
||||
usePreferencesQuery,
|
||||
|
@ -10,7 +10,14 @@ import {
|
|||
useRemoveMutedWordMutation,
|
||||
} from '#/state/queries/preferences'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {atoms as a, useTheme, useBreakpoints, ViewStyleProp, web} from '#/alf'
|
||||
import {
|
||||
atoms as a,
|
||||
useTheme,
|
||||
useBreakpoints,
|
||||
ViewStyleProp,
|
||||
web,
|
||||
native,
|
||||
} from '#/alf'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||
|
@ -48,27 +55,33 @@ function MutedWordsInner({}: {control: Dialog.DialogOuterProps['control']}) {
|
|||
const {isPending, mutateAsync: addMutedWord} = useUpsertMutedWordsMutation()
|
||||
const [field, setField] = React.useState('')
|
||||
const [options, setOptions] = React.useState(['content'])
|
||||
const [_error, setError] = React.useState('')
|
||||
const [error, setError] = React.useState('')
|
||||
|
||||
const submit = React.useCallback(async () => {
|
||||
const value = field.trim()
|
||||
const sanitizedValue = sanitizeMutedWordValue(field)
|
||||
const targets = ['tag', options.includes('content') && 'content'].filter(
|
||||
Boolean,
|
||||
) as AppBskyActorDefs.MutedWord['targets']
|
||||
|
||||
if (!value || !targets.length) return
|
||||
if (!sanitizedValue || !targets.length) {
|
||||
setField('')
|
||||
setError(_(msg`Please enter a valid word, tag, or phrase to mute`))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await addMutedWord([{value, targets}])
|
||||
// send raw value and rely on SDK as sanitization source of truth
|
||||
await addMutedWord([{value: field, targets}])
|
||||
setField('')
|
||||
} catch (e: any) {
|
||||
logger.error(`Failed to save muted word`, {message: e.message})
|
||||
setError(e.message)
|
||||
}
|
||||
}, [field, options, addMutedWord, setField])
|
||||
}, [_, field, options, addMutedWord, setField])
|
||||
|
||||
return (
|
||||
<Dialog.ScrollableInner label={_(msg`Manage your muted words and tags`)}>
|
||||
<View onTouchStart={Keyboard.dismiss}>
|
||||
<Text
|
||||
style={[a.text_md, a.font_bold, a.pb_sm, t.atoms.text_contrast_high]}>
|
||||
<Trans>Add muted words and tags</Trans>
|
||||
|
@ -87,7 +100,12 @@ function MutedWordsInner({}: {control: Dialog.DialogOuterProps['control']}) {
|
|||
label={_(msg`Enter a word or tag`)}
|
||||
placeholder={_(msg`Enter a word or tag`)}
|
||||
value={field}
|
||||
onChangeText={setField}
|
||||
onChangeText={value => {
|
||||
if (error) {
|
||||
setError('')
|
||||
}
|
||||
setField(value)
|
||||
}}
|
||||
onSubmitEditing={submit}
|
||||
/>
|
||||
|
||||
|
@ -99,7 +117,7 @@ function MutedWordsInner({}: {control: Dialog.DialogOuterProps['control']}) {
|
|||
<View
|
||||
style={[
|
||||
a.pt_sm,
|
||||
a.pb_md,
|
||||
a.py_sm,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.gap_sm,
|
||||
|
@ -151,16 +169,41 @@ function MutedWordsInner({}: {control: Dialog.DialogOuterProps['control']}) {
|
|||
</View>
|
||||
</Toggle.Group>
|
||||
|
||||
{error && (
|
||||
<View
|
||||
style={[
|
||||
a.mb_lg,
|
||||
a.flex_row,
|
||||
a.rounded_sm,
|
||||
a.p_md,
|
||||
a.mb_xs,
|
||||
t.atoms.bg_contrast_25,
|
||||
{
|
||||
backgroundColor: t.palette.negative_400,
|
||||
},
|
||||
]}>
|
||||
<Text
|
||||
style={[
|
||||
a.italic,
|
||||
{color: t.palette.white},
|
||||
native({marginTop: 2}),
|
||||
]}>
|
||||
{error}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<Text
|
||||
style={[
|
||||
a.pt_xs,
|
||||
a.text_sm,
|
||||
a.italic,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
<Trans>
|
||||
We recommend avoiding common words that appear in many posts, since
|
||||
it can result in no posts being shown.
|
||||
We recommend avoiding common words that appear in many posts,
|
||||
since it can result in no posts being shown.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
|
@ -169,7 +212,12 @@ function MutedWordsInner({}: {control: Dialog.DialogOuterProps['control']}) {
|
|||
|
||||
<View style={[a.pt_2xl]}>
|
||||
<Text
|
||||
style={[a.text_md, a.font_bold, a.pb_md, t.atoms.text_contrast_high]}>
|
||||
style={[
|
||||
a.text_md,
|
||||
a.font_bold,
|
||||
a.pb_md,
|
||||
t.atoms.text_contrast_high,
|
||||
]}>
|
||||
<Trans>Your muted words</Trans>
|
||||
</Text>
|
||||
|
||||
|
@ -208,6 +256,7 @@ function MutedWordsInner({}: {control: Dialog.DialogOuterProps['control']}) {
|
|||
{isNative && <View style={{height: 20}} />}
|
||||
|
||||
<Dialog.Close />
|
||||
</View>
|
||||
</Dialog.ScrollableInner>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
} from 'react-native'
|
||||
|
||||
import {HITSLOP_20} from 'lib/constants'
|
||||
import {useTheme, atoms as a, web, tokens, android} from '#/alf'
|
||||
import {useTheme, atoms as a, web, android} from '#/alf'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {Props as SVGIconProps} from '#/components/icons/common'
|
||||
|
@ -110,7 +110,7 @@ export function useSharedInputStyles() {
|
|||
{
|
||||
backgroundColor:
|
||||
t.name === 'light' ? t.palette.negative_25 : t.palette.negative_900,
|
||||
borderColor: tokens.color.red_500,
|
||||
borderColor: t.palette.negative_500,
|
||||
},
|
||||
]
|
||||
|
||||
|
|
|
@ -301,7 +301,7 @@ export function createSharedToggleStyles({
|
|||
if (isInvalid) {
|
||||
base.push({
|
||||
backgroundColor:
|
||||
t.name === 'light' ? t.palette.negative_25 : t.palette.negative_900,
|
||||
t.name === 'light' ? t.palette.negative_25 : t.palette.negative_975,
|
||||
borderColor:
|
||||
t.name === 'light' ? t.palette.negative_300 : t.palette.negative_800,
|
||||
})
|
||||
|
@ -310,7 +310,7 @@ export function createSharedToggleStyles({
|
|||
baseHover.push({
|
||||
backgroundColor:
|
||||
t.name === 'light' ? t.palette.negative_25 : t.palette.negative_900,
|
||||
borderColor: t.palette.negative_500,
|
||||
borderColor: t.palette.negative_600,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,15 +15,20 @@ export function useIntentHandler() {
|
|||
|
||||
React.useEffect(() => {
|
||||
const handleIncomingURL = (url: string) => {
|
||||
// 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
|
||||
// path parameter is in pathname rather than in hostname.
|
||||
if (url.startsWith('bluesky://') && !url.startsWith('bluesky:///')) {
|
||||
url = url.replace('bluesky://', 'bluesky:///')
|
||||
}
|
||||
|
||||
const urlp = new URL(url)
|
||||
const [_, intentTypeNative, intentTypeWeb] = urlp.pathname.split('/')
|
||||
const [_, intent, intentType] = urlp.pathname.split('/')
|
||||
|
||||
// On native, our links look like bluesky://intent/SomeIntent, so we have to check the hostname for the
|
||||
// intent check. On web, we have to check the first part of the path since we have an actual hostname
|
||||
const intentType = isNative ? intentTypeNative : intentTypeWeb
|
||||
const isIntent = isNative
|
||||
? urlp.hostname === 'intent'
|
||||
: intentTypeNative === 'intent'
|
||||
const isIntent = intent === 'intent'
|
||||
const params = urlp.searchParams
|
||||
|
||||
if (!isIntent) return
|
||||
|
@ -69,10 +74,7 @@ function useComposeIntent() {
|
|||
return false
|
||||
}
|
||||
// We also should just filter out cases that don't have all the info we need
|
||||
if (!VALID_IMAGE_REGEX.test(part)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return VALID_IMAGE_REGEX.test(part)
|
||||
})
|
||||
.map(part => {
|
||||
const [uri, width, height] = part.split('|')
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
AppBskyFeedPost,
|
||||
AppBskyRichtextFacet,
|
||||
AppBskyEmbedImages,
|
||||
AppBskyEmbedExternal,
|
||||
} from '@atproto/api'
|
||||
|
||||
type ModeratePost = typeof moderatePost
|
||||
|
@ -205,44 +206,151 @@ export function moderatePost_wrapped(
|
|||
|
||||
if (subject.embed) {
|
||||
let embedHidden = false
|
||||
let embedMuted = false
|
||||
let externalMuted = false
|
||||
|
||||
if (AppBskyEmbedRecord.isViewRecord(subject.embed.record)) {
|
||||
embedHidden = hiddenPosts.includes(subject.embed.record.uri)
|
||||
|
||||
if (AppBskyFeedPost.isRecord(subject.embed.record.value)) {
|
||||
embedHidden =
|
||||
embedHidden ||
|
||||
hasMutedWord({
|
||||
mutedWords,
|
||||
text: subject.embed.record.value.text,
|
||||
facets: subject.embed.record.value.facets,
|
||||
outlineTags: subject.embed.record.value.tags,
|
||||
languages: subject.embed.record.value.langs,
|
||||
isOwnPost,
|
||||
})
|
||||
|
||||
if (AppBskyEmbedImages.isMain(subject.embed.record.value.embed)) {
|
||||
for (const image of subject.embed.record.value.embed.images) {
|
||||
embedHidden =
|
||||
embedHidden ||
|
||||
hasMutedWord({
|
||||
mutedWords,
|
||||
text: image.alt,
|
||||
facets: [],
|
||||
outlineTags: [],
|
||||
languages: subject.embed.record.value.langs,
|
||||
isOwnPost,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
AppBskyEmbedRecordWithMedia.isView(subject.embed) &&
|
||||
AppBskyEmbedRecord.isViewRecord(subject.embed.record.record)
|
||||
) {
|
||||
// TODO what
|
||||
embedHidden = hiddenPosts.includes(subject.embed.record.record.uri)
|
||||
}
|
||||
|
||||
if (AppBskyEmbedRecord.isViewRecord(subject.embed.record)) {
|
||||
if (AppBskyFeedPost.isRecord(subject.embed.record.value)) {
|
||||
const embeddedPost = subject.embed.record.value
|
||||
|
||||
embedMuted =
|
||||
embedMuted ||
|
||||
hasMutedWord({
|
||||
mutedWords,
|
||||
text: embeddedPost.text,
|
||||
facets: embeddedPost.facets,
|
||||
outlineTags: embeddedPost.tags,
|
||||
languages: embeddedPost.langs,
|
||||
isOwnPost,
|
||||
})
|
||||
|
||||
if (AppBskyEmbedImages.isMain(embeddedPost.embed)) {
|
||||
for (const image of embeddedPost.embed.images) {
|
||||
embedMuted =
|
||||
embedMuted ||
|
||||
hasMutedWord({
|
||||
mutedWords,
|
||||
text: image.alt,
|
||||
facets: [],
|
||||
outlineTags: [],
|
||||
languages: embeddedPost.langs,
|
||||
isOwnPost,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (AppBskyEmbedExternal.isMain(embeddedPost.embed)) {
|
||||
const {external} = embeddedPost.embed
|
||||
|
||||
embedMuted =
|
||||
embedMuted ||
|
||||
hasMutedWord({
|
||||
mutedWords,
|
||||
text: external.title + ' ' + external.description,
|
||||
facets: [],
|
||||
outlineTags: [],
|
||||
languages: [],
|
||||
isOwnPost,
|
||||
})
|
||||
}
|
||||
|
||||
if (AppBskyEmbedRecordWithMedia.isMain(embeddedPost.embed)) {
|
||||
if (AppBskyEmbedExternal.isMain(embeddedPost.embed.media)) {
|
||||
const {external} = embeddedPost.embed.media
|
||||
|
||||
embedMuted =
|
||||
embedMuted ||
|
||||
hasMutedWord({
|
||||
mutedWords,
|
||||
text: external.title + ' ' + external.description,
|
||||
facets: [],
|
||||
outlineTags: [],
|
||||
languages: [],
|
||||
isOwnPost,
|
||||
})
|
||||
}
|
||||
|
||||
if (AppBskyEmbedImages.isMain(embeddedPost.embed.media)) {
|
||||
for (const image of embeddedPost.embed.media.images) {
|
||||
embedMuted =
|
||||
embedMuted ||
|
||||
hasMutedWord({
|
||||
mutedWords,
|
||||
text: image.alt,
|
||||
facets: [],
|
||||
outlineTags: [],
|
||||
languages: AppBskyFeedPost.isRecord(embeddedPost.record)
|
||||
? embeddedPost.langs
|
||||
: [],
|
||||
isOwnPost,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (AppBskyEmbedExternal.isView(subject.embed)) {
|
||||
const {external} = subject.embed
|
||||
|
||||
externalMuted =
|
||||
externalMuted ||
|
||||
hasMutedWord({
|
||||
mutedWords,
|
||||
text: external.title + ' ' + external.description,
|
||||
facets: [],
|
||||
outlineTags: [],
|
||||
languages: [],
|
||||
isOwnPost,
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
AppBskyEmbedRecordWithMedia.isView(subject.embed) &&
|
||||
AppBskyEmbedRecord.isViewRecord(subject.embed.record.record)
|
||||
) {
|
||||
if (AppBskyFeedPost.isRecord(subject.embed.record.record.value)) {
|
||||
const post = subject.embed.record.record.value
|
||||
embedMuted =
|
||||
embedMuted ||
|
||||
hasMutedWord({
|
||||
mutedWords,
|
||||
text: post.text,
|
||||
facets: post.facets,
|
||||
outlineTags: post.tags,
|
||||
languages: post.langs,
|
||||
isOwnPost,
|
||||
})
|
||||
}
|
||||
|
||||
if (AppBskyEmbedImages.isView(subject.embed.media)) {
|
||||
for (const image of subject.embed.media.images) {
|
||||
embedMuted =
|
||||
embedMuted ||
|
||||
hasMutedWord({
|
||||
mutedWords,
|
||||
text: image.alt,
|
||||
facets: [],
|
||||
outlineTags: [],
|
||||
languages: AppBskyFeedPost.isRecord(subject.record)
|
||||
? subject.record.langs
|
||||
: [],
|
||||
isOwnPost,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (embedHidden) {
|
||||
moderations.embed.filter = true
|
||||
moderations.embed.blur = true
|
||||
|
@ -254,6 +362,17 @@ export function moderatePost_wrapped(
|
|||
priority: 1,
|
||||
}
|
||||
}
|
||||
} else if (externalMuted || embedMuted) {
|
||||
moderations.content.filter = true
|
||||
moderations.content.blur = true
|
||||
if (!moderations.content.cause) {
|
||||
moderations.content.cause = {
|
||||
// @ts-ignore Temporary extension to the moderation system -prf
|
||||
type: 'muted-word',
|
||||
source: {type: 'user'},
|
||||
priority: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ export type CommonNavigatorParams = {
|
|||
PreferencesThreads: undefined
|
||||
PreferencesExternalEmbeds: undefined
|
||||
Search: {q?: string}
|
||||
Hashtag: {tag: string; author?: string}
|
||||
}
|
||||
|
||||
export type BottomTabNavigatorParams = CommonNavigatorParams & {
|
||||
|
@ -69,6 +70,7 @@ export type FlatNavigatorParams = CommonNavigatorParams & {
|
|||
Search: {q?: string}
|
||||
Feeds: undefined
|
||||
Notifications: undefined
|
||||
Hashtag: {tag: string; author?: string}
|
||||
}
|
||||
|
||||
export type AllNavigatorParams = CommonNavigatorParams & {
|
||||
|
@ -81,6 +83,7 @@ export type AllNavigatorParams = CommonNavigatorParams & {
|
|||
NotificationsTab: undefined
|
||||
Notifications: undefined
|
||||
MyProfileTab: undefined
|
||||
Hashtag: {tag: string; author?: string}
|
||||
}
|
||||
|
||||
// NOTE
|
||||
|
|
|
@ -157,17 +157,11 @@ export function linkRequiresWarning(uri: string, label: string) {
|
|||
|
||||
const host = urip.hostname.toLowerCase()
|
||||
|
||||
if (host === 'bsky.app') {
|
||||
// Hosts that end with bsky.app or bsky.social should be trusted by default.
|
||||
if (host.endsWith('bsky.app') || host.endsWith('bsky.social')) {
|
||||
// if this is a link to internal content,
|
||||
// warn if it represents itself as a URL to another app
|
||||
if (
|
||||
labelDomain &&
|
||||
labelDomain !== 'bsky.app' &&
|
||||
isPossiblyAUrl(labelDomain)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
return !!labelDomain && labelDomain !== host && isPossiblyAUrl(labelDomain)
|
||||
} else {
|
||||
// if this is a link to external content,
|
||||
// warn if the label doesnt match the target
|
||||
|
|
|
@ -357,8 +357,8 @@ export const dimTheme: Theme = {
|
|||
textVeryLight: dimPalette.contrast_400,
|
||||
replyLine: dimPalette.contrast_200,
|
||||
replyLineDot: dimPalette.contrast_200,
|
||||
unreadNotifBg: `hsl(211, 48%, 17%)`,
|
||||
unreadNotifBorder: `hsl(211, 48%, 30%)`,
|
||||
unreadNotifBg: dimPalette.primary_975,
|
||||
unreadNotifBorder: dimPalette.primary_900,
|
||||
postCtrl: dimPalette.contrast_500,
|
||||
brandText: dimPalette.primary_500,
|
||||
emptyStateIcon: dimPalette.contrast_300,
|
||||
|
|
|
@ -33,4 +33,5 @@ export const router = new Router({
|
|||
TermsOfService: '/support/tos',
|
||||
CommunityGuidelines: '/support/community-guidelines',
|
||||
CopyrightPolicy: '/support/copyright',
|
||||
Hashtag: '/hashtag/:tag',
|
||||
})
|
||||
|
|
|
@ -0,0 +1,164 @@
|
|||
import React from 'react'
|
||||
import {ListRenderItemInfo, Pressable} from 'react-native'
|
||||
import {atoms as a, useBreakpoints} from '#/alf'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
import {useSetMinimalShellMode} from 'state/shell'
|
||||
import {ViewHeader} from 'view/com/util/ViewHeader'
|
||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
import {CommonNavigatorParams} from 'lib/routes/types'
|
||||
import {useSearchPostsQuery} from 'state/queries/search-posts'
|
||||
import {Post} from 'view/com/post/Post'
|
||||
import {PostView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
|
||||
import {enforceLen} from 'lib/strings/helpers'
|
||||
import {
|
||||
ListFooter,
|
||||
ListHeaderDesktop,
|
||||
ListMaybePlaceholder,
|
||||
} from '#/components/Lists'
|
||||
import {List} from 'view/com/util/List'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {sanitizeHandle} from 'lib/strings/handles'
|
||||
import {CenteredView} from 'view/com/util/Views'
|
||||
import {ArrowOutOfBox_Stroke2_Corner0_Rounded} from '#/components/icons/ArrowOutOfBox'
|
||||
import {shareUrl} from 'lib/sharing'
|
||||
import {HITSLOP_10} from 'lib/constants'
|
||||
import {isNative} from 'platform/detection'
|
||||
|
||||
const renderItem = ({item}: ListRenderItemInfo<PostView>) => {
|
||||
return <Post post={item} />
|
||||
}
|
||||
|
||||
const keyExtractor = (item: PostView, index: number) => {
|
||||
return `${item.uri}-${index}`
|
||||
}
|
||||
|
||||
export default function HashtagScreen({
|
||||
route,
|
||||
}: NativeStackScreenProps<CommonNavigatorParams, 'Hashtag'>) {
|
||||
const {tag, author} = route.params
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const {_} = useLingui()
|
||||
const [isPTR, setIsPTR] = React.useState(false)
|
||||
|
||||
const fullTag = React.useMemo(() => {
|
||||
return `#${tag.replaceAll('%23', '#')}`
|
||||
}, [tag])
|
||||
|
||||
const queryParam = React.useMemo(() => {
|
||||
if (!author) return fullTag
|
||||
return `${fullTag} from:${sanitizeHandle(author)}`
|
||||
}, [fullTag, author])
|
||||
|
||||
const headerTitle = React.useMemo(() => {
|
||||
return enforceLen(fullTag.toLowerCase(), 24, true, 'middle')
|
||||
}, [fullTag])
|
||||
|
||||
const sanitizedAuthor = React.useMemo(() => {
|
||||
if (!author) return
|
||||
return sanitizeHandle(author)
|
||||
}, [author])
|
||||
|
||||
const {
|
||||
data,
|
||||
isFetching,
|
||||
isLoading,
|
||||
isRefetching,
|
||||
isError,
|
||||
error,
|
||||
refetch,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
} = useSearchPostsQuery({query: queryParam})
|
||||
|
||||
const posts = React.useMemo(() => {
|
||||
return data?.pages.flatMap(page => page.posts) || []
|
||||
}, [data])
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
setMinimalShellMode(false)
|
||||
}, [setMinimalShellMode]),
|
||||
)
|
||||
|
||||
const onShare = React.useCallback(() => {
|
||||
const url = new URL('https://bsky.app')
|
||||
url.pathname = `/hashtag/${tag}`
|
||||
if (author) {
|
||||
url.searchParams.set('author', author)
|
||||
}
|
||||
shareUrl(url.toString())
|
||||
}, [tag, author])
|
||||
|
||||
const onRefresh = React.useCallback(async () => {
|
||||
setIsPTR(true)
|
||||
await refetch()
|
||||
setIsPTR(false)
|
||||
}, [refetch])
|
||||
|
||||
const onEndReached = React.useCallback(() => {
|
||||
if (isFetching || !hasNextPage || error) return
|
||||
fetchNextPage()
|
||||
}, [isFetching, hasNextPage, error, fetchNextPage])
|
||||
|
||||
return (
|
||||
<CenteredView style={a.flex_1} sideBorders={gtMobile}>
|
||||
<ViewHeader
|
||||
title={headerTitle}
|
||||
subtitle={author ? _(msg`From @${sanitizedAuthor}`) : undefined}
|
||||
canGoBack
|
||||
renderButton={
|
||||
isNative
|
||||
? () => (
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
onPress={onShare}
|
||||
hitSlop={HITSLOP_10}>
|
||||
<ArrowOutOfBox_Stroke2_Corner0_Rounded
|
||||
size="lg"
|
||||
onPress={onShare}
|
||||
/>
|
||||
</Pressable>
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<ListMaybePlaceholder
|
||||
isLoading={isLoading || isRefetching}
|
||||
isError={isError}
|
||||
isEmpty={posts.length < 1}
|
||||
onRetry={refetch}
|
||||
notFoundType="results"
|
||||
empty={_(msg`We couldn't find any results for that hashtag.`)}
|
||||
/>
|
||||
{!isLoading && posts.length > 0 && (
|
||||
<List<PostView>
|
||||
data={posts}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={keyExtractor}
|
||||
refreshing={isPTR}
|
||||
onRefresh={onRefresh}
|
||||
onEndReached={onEndReached}
|
||||
onEndReachedThreshold={4}
|
||||
// @ts-ignore web only -prf
|
||||
desktopFixedHeight
|
||||
ListHeaderComponent={
|
||||
<ListHeaderDesktop
|
||||
title={headerTitle}
|
||||
subtitle={author ? _(msg`From @${sanitizedAuthor}`) : undefined}
|
||||
/>
|
||||
}
|
||||
ListFooterComponent={
|
||||
<ListFooter
|
||||
isFetching={isFetching && !isRefetching}
|
||||
isError={isError}
|
||||
error={error?.name}
|
||||
onRetry={fetchNextPage}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</CenteredView>
|
||||
)
|
||||
}
|
|
@ -18,6 +18,8 @@ import {Mark} from '@tiptap/core'
|
|||
import {Plugin, PluginKey} from '@tiptap/pm/state'
|
||||
import {Node as ProsemirrorNode} from '@tiptap/pm/model'
|
||||
import {Decoration, DecorationSet} from '@tiptap/pm/view'
|
||||
import {URL_REGEX} from '@atproto/api'
|
||||
|
||||
import {isValidDomain} from 'lib/strings/url-helpers'
|
||||
|
||||
export const LinkDecorator = Mark.create({
|
||||
|
@ -78,8 +80,7 @@ function linkDecorator() {
|
|||
|
||||
function iterateUris(str: string, cb: (from: number, to: number) => void) {
|
||||
let match
|
||||
const re =
|
||||
/(^|\s|\()((https?:\/\/[\S]+)|((?<domain>[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*))/gim
|
||||
const re = URL_REGEX
|
||||
while ((match = re.exec(str))) {
|
||||
let uri = match[2]
|
||||
if (!uri.startsWith('http')) {
|
||||
|
|
|
@ -18,28 +18,36 @@ import {Mark} from '@tiptap/core'
|
|||
import {Plugin, PluginKey} from '@tiptap/pm/state'
|
||||
import {Node as ProsemirrorNode} from '@tiptap/pm/model'
|
||||
import {Decoration, DecorationSet} from '@tiptap/pm/view'
|
||||
import {TAG_REGEX, TRAILING_PUNCTUATION_REGEX} from '@atproto/api'
|
||||
|
||||
function getDecorations(doc: ProsemirrorNode) {
|
||||
const decorations: Decoration[] = []
|
||||
|
||||
doc.descendants((node, pos) => {
|
||||
if (node.isText && node.text) {
|
||||
const regex = /(?:^|\s)(#[^\d\s]\S*)(?=\s)?/g
|
||||
const regex = TAG_REGEX
|
||||
const textContent = node.textContent
|
||||
|
||||
let match
|
||||
while ((match = regex.exec(textContent))) {
|
||||
const [matchedString, tag] = match
|
||||
const [matchedString, _, tag] = match
|
||||
|
||||
if (tag.length > 66) continue
|
||||
if (!tag || tag.replace(TRAILING_PUNCTUATION_REGEX, '').length > 64)
|
||||
continue
|
||||
|
||||
const [trailingPunc = ''] = tag.match(/\p{P}+$/u) || []
|
||||
const [trailingPunc = ''] = tag.match(TRAILING_PUNCTUATION_REGEX) || []
|
||||
const matchedFrom = match.index + matchedString.indexOf(tag)
|
||||
const matchedTo = matchedFrom + (tag.length - trailingPunc.length)
|
||||
|
||||
const from = match.index + matchedString.indexOf(tag)
|
||||
const to = from + (tag.length - trailingPunc.length)
|
||||
/*
|
||||
* The match is exclusive of `#` so we need to adjust the start of the
|
||||
* highlight by -1 to include the `#`
|
||||
*/
|
||||
const start = pos + matchedFrom - 1
|
||||
const end = pos + matchedTo
|
||||
|
||||
decorations.push(
|
||||
Decoration.inline(pos + from, pos + to, {
|
||||
Decoration.inline(start, end, {
|
||||
class: 'autolink',
|
||||
}),
|
||||
)
|
||||
|
|
|
@ -52,7 +52,7 @@ export function HomeHeader(
|
|||
)
|
||||
|
||||
return (
|
||||
<HomeHeaderLayout>
|
||||
<HomeHeaderLayout tabBarAnchor={props.tabBarAnchor}>
|
||||
<TabBar
|
||||
key={items.join(',')}
|
||||
onPressSelected={props.onPressSelected}
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
import React from 'react'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import Animated from 'react-native-reanimated'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {HomeHeaderLayoutMobile} from './HomeHeaderLayoutMobile'
|
||||
import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
|
||||
import {useShellLayout} from '#/state/shell/shell-layout'
|
||||
import {Logo} from '#/view/icons/Logo'
|
||||
import {Link, TextLink} from '../util/Link'
|
||||
import {Link} from '../util/Link'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconStyle,
|
||||
|
@ -16,41 +13,42 @@ import {useLingui} from '@lingui/react'
|
|||
import {msg} from '@lingui/macro'
|
||||
import {CogIcon} from '#/lib/icons'
|
||||
|
||||
export function HomeHeaderLayout({children}: {children: React.ReactNode}) {
|
||||
export function HomeHeaderLayout(props: {
|
||||
children: React.ReactNode
|
||||
tabBarAnchor: JSX.Element | null | undefined
|
||||
}) {
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
if (isMobile) {
|
||||
return <HomeHeaderLayoutMobile>{children}</HomeHeaderLayoutMobile>
|
||||
return <HomeHeaderLayoutMobile {...props} />
|
||||
} else {
|
||||
return <HomeHeaderLayoutTablet>{children}</HomeHeaderLayoutTablet>
|
||||
return <HomeHeaderLayoutDesktopAndTablet {...props} />
|
||||
}
|
||||
}
|
||||
|
||||
function HomeHeaderLayoutTablet({children}: {children: React.ReactNode}) {
|
||||
function HomeHeaderLayoutDesktopAndTablet({
|
||||
children,
|
||||
tabBarAnchor,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
tabBarAnchor: JSX.Element | null | undefined
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const {headerMinimalShellTransform} = useMinimalShellMode()
|
||||
const {headerHeight} = useShellLayout()
|
||||
const {_} = useLingui()
|
||||
|
||||
return (
|
||||
// @ts-ignore the type signature for transform wrong here, translateX and translateY need to be in separate objects -prf
|
||||
<Animated.View
|
||||
style={[pal.view, pal.border, styles.tabBar, headerMinimalShellTransform]}
|
||||
onLayout={e => {
|
||||
headerHeight.value = e.nativeEvent.layout.height
|
||||
}}>
|
||||
<View style={[pal.view, styles.topBar]}>
|
||||
<TextLink
|
||||
type="title-lg"
|
||||
<>
|
||||
<View style={[pal.view, pal.border, styles.bar, styles.topBar]}>
|
||||
<Link
|
||||
href="/settings/following-feed"
|
||||
hitSlop={10}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Following Feed Preferences`)}
|
||||
accessibilityHint=""
|
||||
text={
|
||||
accessibilityHint="">
|
||||
<FontAwesomeIcon
|
||||
icon="sliders"
|
||||
style={pal.textLight as FontAwesomeIconStyle}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Link>
|
||||
<Logo width={28} />
|
||||
<Link
|
||||
href="/settings/saved-feeds"
|
||||
|
@ -61,32 +59,38 @@ function HomeHeaderLayoutTablet({children}: {children: React.ReactNode}) {
|
|||
<CogIcon size={22} strokeWidth={2} style={pal.textLight} />
|
||||
</Link>
|
||||
</View>
|
||||
{tabBarAnchor}
|
||||
<View style={[pal.view, pal.border, styles.bar, styles.tabBar]}>
|
||||
{children}
|
||||
</Animated.View>
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
bar: {
|
||||
// @ts-ignore Web only
|
||||
left: 'calc(50% - 300px)',
|
||||
width: 600,
|
||||
borderLeftWidth: 1,
|
||||
borderRightWidth: 1,
|
||||
},
|
||||
topBar: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 18,
|
||||
paddingVertical: 8,
|
||||
marginTop: 8,
|
||||
width: '100%',
|
||||
paddingTop: 16,
|
||||
paddingBottom: 8,
|
||||
},
|
||||
tabBar: {
|
||||
// @ts-ignore Web only
|
||||
position: 'sticky',
|
||||
zIndex: 1,
|
||||
// @ts-ignore Web only -prf
|
||||
left: 'calc(50% - 300px)',
|
||||
width: 600,
|
||||
top: 0,
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
borderLeftWidth: 1,
|
||||
borderRightWidth: 1,
|
||||
zIndex: 1,
|
||||
},
|
||||
})
|
||||
|
|
|
@ -23,6 +23,7 @@ export function HomeHeaderLayoutMobile({
|
|||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
tabBarAnchor: JSX.Element | null | undefined
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
|
|
|
@ -159,7 +159,7 @@ export const TextLink = memo(function TextLink({
|
|||
dataSet,
|
||||
title,
|
||||
onPress,
|
||||
warnOnMismatchingLabel,
|
||||
disableMismatchWarning,
|
||||
navigationAction,
|
||||
...orgProps
|
||||
}: {
|
||||
|
@ -172,7 +172,7 @@ export const TextLink = memo(function TextLink({
|
|||
lineHeight?: number
|
||||
dataSet?: any
|
||||
title?: string
|
||||
warnOnMismatchingLabel?: boolean
|
||||
disableMismatchWarning?: boolean
|
||||
navigationAction?: 'push' | 'replace' | 'navigate'
|
||||
} & TextProps) {
|
||||
const {...props} = useLinkProps({to: sanitizeUrl(href)})
|
||||
|
@ -180,14 +180,14 @@ export const TextLink = memo(function TextLink({
|
|||
const {openModal, closeModal} = useModalControls()
|
||||
const openLink = useOpenLink()
|
||||
|
||||
if (warnOnMismatchingLabel && typeof text !== 'string') {
|
||||
if (!disableMismatchWarning && typeof text !== 'string') {
|
||||
console.error('Unable to detect mismatching label')
|
||||
}
|
||||
|
||||
props.onPress = React.useCallback(
|
||||
(e?: Event) => {
|
||||
const requiresWarning =
|
||||
warnOnMismatchingLabel &&
|
||||
!disableMismatchWarning &&
|
||||
linkRequiresWarning(href, typeof text === 'string' ? text : '')
|
||||
if (requiresWarning) {
|
||||
e?.preventDefault?.()
|
||||
|
@ -227,7 +227,7 @@ export const TextLink = memo(function TextLink({
|
|||
navigation,
|
||||
href,
|
||||
text,
|
||||
warnOnMismatchingLabel,
|
||||
disableMismatchWarning,
|
||||
navigationAction,
|
||||
openLink,
|
||||
],
|
||||
|
|
|
@ -13,11 +13,13 @@ import Animated from 'react-native-reanimated'
|
|||
import {useSetDrawerOpen} from '#/state/shell'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useTheme} from '#/alf'
|
||||
|
||||
const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20}
|
||||
|
||||
export function ViewHeader({
|
||||
title,
|
||||
subtitle,
|
||||
canGoBack,
|
||||
showBackButton = true,
|
||||
hideOnScroll,
|
||||
|
@ -26,6 +28,7 @@ export function ViewHeader({
|
|||
renderButton,
|
||||
}: {
|
||||
title: string
|
||||
subtitle?: string
|
||||
canGoBack?: boolean
|
||||
showBackButton?: boolean
|
||||
hideOnScroll?: boolean
|
||||
|
@ -39,6 +42,7 @@ export function ViewHeader({
|
|||
const navigation = useNavigation<NavigationProp>()
|
||||
const {track} = useAnalytics()
|
||||
const {isDesktop, isTablet} = useWebMediaQueries()
|
||||
const t = useTheme()
|
||||
|
||||
const onPressBack = React.useCallback(() => {
|
||||
if (navigation.canGoBack()) {
|
||||
|
@ -71,6 +75,8 @@ export function ViewHeader({
|
|||
|
||||
return (
|
||||
<Container hideOnScroll={hideOnScroll || false} showBorder={showBorder}>
|
||||
<View style={{flex: 1}}>
|
||||
<View style={{flexDirection: 'row', alignItems: 'center'}}>
|
||||
{showBackButton ? (
|
||||
<TouchableOpacity
|
||||
testID="viewHeaderDrawerBtn"
|
||||
|
@ -107,6 +113,22 @@ export function ViewHeader({
|
|||
) : showBackButton ? (
|
||||
<View style={canGoBack ? styles.backBtn : styles.backBtnWide} />
|
||||
) : null}
|
||||
</View>
|
||||
{subtitle ? (
|
||||
<View
|
||||
style={[styles.titleContainer, {marginTop: -3}]}
|
||||
pointerEvents="none">
|
||||
<Text
|
||||
style={[
|
||||
pal.text,
|
||||
styles.subtitle,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
{subtitle}
|
||||
</Text>
|
||||
</View>
|
||||
) : undefined}
|
||||
</View>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
@ -185,7 +207,6 @@ function Container({
|
|||
const styles = StyleSheet.create({
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
width: '100%',
|
||||
|
@ -207,12 +228,14 @@ const styles = StyleSheet.create({
|
|||
titleContainer: {
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
paddingRight: 10,
|
||||
alignItems: 'center',
|
||||
},
|
||||
title: {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
|
||||
subtitle: {
|
||||
fontSize: 13,
|
||||
},
|
||||
backBtn: {
|
||||
width: 30,
|
||||
height: 30,
|
||||
|
|
|
@ -46,7 +46,7 @@ export function ContentHider({
|
|||
)
|
||||
}
|
||||
|
||||
const isMute = moderation.cause?.type === 'muted'
|
||||
const isMute = ['muted', 'muted-word'].includes(moderation.cause?.type || '')
|
||||
const desc = describeModerationCause(moderation.cause, 'content')
|
||||
return (
|
||||
<View testID={testID} style={[styles.outer, style]}>
|
||||
|
|
|
@ -47,7 +47,7 @@ export function PostHider({
|
|||
)
|
||||
}
|
||||
|
||||
const isMute = moderation.cause?.type === 'muted'
|
||||
const isMute = ['muted', 'muted-word'].includes(moderation.cause?.type || '')
|
||||
const desc = describeModerationCause(moderation.cause, 'content')
|
||||
return !override ? (
|
||||
<Pressable
|
||||
|
|
|
@ -114,7 +114,6 @@ export function RichText({
|
|||
href={link.uri}
|
||||
style={[style, lineHeightStyle, pal.link, {pointerEvents: 'auto'}]}
|
||||
dataSet={WORD_WRAP}
|
||||
warnOnMismatchingLabel
|
||||
selectable={selectable}
|
||||
/>,
|
||||
)
|
||||
|
|
|
@ -123,8 +123,7 @@ function HomeScreenReady({
|
|||
return (
|
||||
<HomeHeader
|
||||
key="FEEDS_TAB_BAR"
|
||||
selectedPage={props.selectedPage}
|
||||
onSelect={props.onSelect}
|
||||
{...props}
|
||||
testID="homeScreenFeedTabs"
|
||||
onPressSelected={onPressSelected}
|
||||
feeds={pinnedFeedInfos}
|
||||
|
|
|
@ -491,6 +491,8 @@ const styles = StyleSheet.create({
|
|||
container: {
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
// @ts-ignore Web-only.
|
||||
overflowAnchor: 'none', // Fixes jumps when switching tabs while scrolled down.
|
||||
},
|
||||
loading: {
|
||||
paddingVertical: 10,
|
||||
|
|
|
@ -60,7 +60,7 @@ import {
|
|||
import {logger} from '#/logger'
|
||||
import {useAnalytics} from '#/lib/analytics/analytics'
|
||||
import {listenSoftReset} from '#/state/events'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
|
||||
const SECTION_TITLES_CURATE = ['Posts', 'About']
|
||||
const SECTION_TITLES_MOD = ['About']
|
||||
|
@ -699,6 +699,7 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
|
|||
ref,
|
||||
) {
|
||||
const pal = usePalette('default')
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const {currentAccount} = useSession()
|
||||
|
@ -792,7 +793,7 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
|
|||
paddingBottom: isMobile ? 14 : 18,
|
||||
},
|
||||
]}>
|
||||
<Text type="lg-bold">
|
||||
<Text type="lg-bold" style={t.atoms.text}>
|
||||
<Trans>Users</Trans>
|
||||
</Text>
|
||||
{isOwner && (
|
||||
|
@ -817,14 +818,18 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
|
|||
</View>
|
||||
)
|
||||
}, [
|
||||
pal,
|
||||
list,
|
||||
isMobile,
|
||||
pal.border,
|
||||
pal.textLight,
|
||||
pal.colors.link,
|
||||
pal.link,
|
||||
descriptionRT,
|
||||
isCurateList,
|
||||
isOwner,
|
||||
onPressAddUser,
|
||||
list.creator,
|
||||
t.atoms.text,
|
||||
_,
|
||||
onPressAddUser,
|
||||
])
|
||||
|
||||
const renderEmptyState = useCallback(() => {
|
||||
|
|
|
@ -4,7 +4,7 @@ import {View} from 'react-native'
|
|||
import {useTheme, atoms as a} from '#/alf'
|
||||
import {ButtonText} from '#/components/Button'
|
||||
import {InlineLink, Link} from '#/components/Link'
|
||||
import {H1, H3, Text} from '#/components/Typography'
|
||||
import {H1, Text} from '#/components/Typography'
|
||||
|
||||
export function Links() {
|
||||
const t = useTheme()
|
||||
|
@ -13,31 +13,19 @@ export function Links() {
|
|||
<H1>Links</H1>
|
||||
|
||||
<View style={[a.gap_md, a.align_start]}>
|
||||
<InlineLink
|
||||
to="https://bsky.social"
|
||||
warnOnMismatchingTextChild
|
||||
style={[a.text_md]}>
|
||||
External
|
||||
<InlineLink to="https://google.com" style={[a.text_lg]}>
|
||||
https://google.com
|
||||
</InlineLink>
|
||||
<InlineLink to="https://bsky.social" style={[a.text_md, t.atoms.text]}>
|
||||
<H3>External with custom children</H3>
|
||||
<InlineLink to="https://google.com" style={[a.text_lg]}>
|
||||
External with custom children (google.com)
|
||||
</InlineLink>
|
||||
<InlineLink
|
||||
to="https://bsky.social"
|
||||
style={[a.text_md, t.atoms.text_contrast_low]}>
|
||||
External with custom children
|
||||
Internal (bsky.social)
|
||||
</InlineLink>
|
||||
<InlineLink
|
||||
to="https://bsky.social"
|
||||
warnOnMismatchingTextChild
|
||||
style={[a.text_lg]}>
|
||||
https://bsky.social
|
||||
</InlineLink>
|
||||
<InlineLink
|
||||
to="https://bsky.app/profile/bsky.app"
|
||||
warnOnMismatchingTextChild
|
||||
style={[a.text_md]}>
|
||||
Internal
|
||||
<InlineLink to="https://bsky.app/profile/bsky.app" style={[a.text_md]}>
|
||||
Internal (bsky.app)
|
||||
</InlineLink>
|
||||
|
||||
<Link
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
|
||||
import * as tokens from '#/alf/tokens'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
|
||||
export function Palette() {
|
||||
|
@ -28,79 +27,79 @@ export function Palette() {
|
|||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: tokens.color.blue_25},
|
||||
{height: 60, backgroundColor: t.palette.primary_25},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: tokens.color.blue_50},
|
||||
{height: 60, backgroundColor: t.palette.primary_50},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: tokens.color.blue_100},
|
||||
{height: 60, backgroundColor: t.palette.primary_100},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: tokens.color.blue_200},
|
||||
{height: 60, backgroundColor: t.palette.primary_200},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: tokens.color.blue_300},
|
||||
{height: 60, backgroundColor: t.palette.primary_300},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: tokens.color.blue_400},
|
||||
{height: 60, backgroundColor: t.palette.primary_400},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: tokens.color.blue_500},
|
||||
{height: 60, backgroundColor: t.palette.primary_500},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: tokens.color.blue_600},
|
||||
{height: 60, backgroundColor: t.palette.primary_600},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: tokens.color.blue_700},
|
||||
{height: 60, backgroundColor: t.palette.primary_700},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: tokens.color.blue_800},
|
||||
{height: 60, backgroundColor: t.palette.primary_800},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: tokens.color.blue_900},
|
||||
{height: 60, backgroundColor: t.palette.primary_900},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: tokens.color.blue_950},
|
||||
{height: 60, backgroundColor: t.palette.primary_950},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: tokens.color.blue_975},
|
||||
{height: 60, backgroundColor: t.palette.primary_975},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
|
@ -108,153 +107,159 @@ export function Palette() {
|
|||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: tokens.color.green_25},
|
||||
{height: 60, backgroundColor: t.palette.positive_25},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: tokens.color.green_50},
|
||||
{height: 60, backgroundColor: t.palette.positive_50},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: tokens.color.green_100},
|
||||
{height: 60, backgroundColor: t.palette.positive_100},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: tokens.color.green_200},
|
||||
{height: 60, backgroundColor: t.palette.positive_200},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: tokens.color.green_300},
|
||||
{height: 60, backgroundColor: t.palette.positive_300},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: tokens.color.green_400},
|
||||
{height: 60, backgroundColor: t.palette.positive_400},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: tokens.color.green_500},
|
||||
{height: 60, backgroundColor: t.palette.positive_500},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: tokens.color.green_600},
|
||||
{height: 60, backgroundColor: t.palette.positive_600},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: tokens.color.green_700},
|
||||
{height: 60, backgroundColor: t.palette.positive_700},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: tokens.color.green_800},
|
||||
{height: 60, backgroundColor: t.palette.positive_800},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: tokens.color.green_900},
|
||||
{height: 60, backgroundColor: t.palette.positive_900},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: tokens.color.green_950},
|
||||
{height: 60, backgroundColor: t.palette.positive_950},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: tokens.color.green_975},
|
||||
{height: 60, backgroundColor: t.palette.positive_975},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
<View style={[a.flex_row, a.gap_md]}>
|
||||
<View
|
||||
style={[a.flex_1, {height: 60, backgroundColor: tokens.color.red_25}]}
|
||||
/>
|
||||
<View
|
||||
style={[a.flex_1, {height: 60, backgroundColor: tokens.color.red_50}]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: tokens.color.red_100},
|
||||
{height: 60, backgroundColor: t.palette.negative_25},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: tokens.color.red_200},
|
||||
{height: 60, backgroundColor: t.palette.negative_50},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: tokens.color.red_300},
|
||||
{height: 60, backgroundColor: t.palette.negative_100},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: tokens.color.red_400},
|
||||
{height: 60, backgroundColor: t.palette.negative_200},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: tokens.color.red_500},
|
||||
{height: 60, backgroundColor: t.palette.negative_300},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: tokens.color.red_600},
|
||||
{height: 60, backgroundColor: t.palette.negative_400},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: tokens.color.red_700},
|
||||
{height: 60, backgroundColor: t.palette.negative_500},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: tokens.color.red_800},
|
||||
{height: 60, backgroundColor: t.palette.negative_600},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: tokens.color.red_900},
|
||||
{height: 60, backgroundColor: t.palette.negative_700},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: tokens.color.red_950},
|
||||
{height: 60, backgroundColor: t.palette.negative_800},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: tokens.color.red_975},
|
||||
{height: 60, backgroundColor: t.palette.negative_900},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: t.palette.negative_950},
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
{height: 60, backgroundColor: t.palette.negative_975},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
|
|
|
@ -200,10 +200,10 @@ function ComposeBtn() {
|
|||
const fetchHandle = useFetchHandle()
|
||||
|
||||
const getProfileHandle = async () => {
|
||||
const {routes} = getState()
|
||||
const currentRoute = routes[routes.length - 1]
|
||||
const routes = getState()?.routes
|
||||
const currentRoute = routes?.[routes?.length - 1]
|
||||
|
||||
if (currentRoute.name === 'Profile') {
|
||||
if (currentRoute?.name === 'Profile') {
|
||||
let handle: string | undefined = (
|
||||
currentRoute.params as CommonNavigatorParams['Profile']
|
||||
).name
|
||||
|
|
|
@ -48,6 +48,12 @@
|
|||
scrollbar-gutter: stable both-edges;
|
||||
}
|
||||
|
||||
/* Buttons and inputs have a font set by UA, so we'll have to reset that */
|
||||
button, input, textarea {
|
||||
font: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
/* Color theming */
|
||||
/* Default will always be white */
|
||||
:root {
|
||||
|
|
40
yarn.lock
40
yarn.lock
|
@ -34,15 +34,15 @@
|
|||
jsonpointer "^5.0.0"
|
||||
leven "^3.1.0"
|
||||
|
||||
"@atproto/api@^0.10.0":
|
||||
version "0.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.10.0.tgz#ca34dfa8f9b1e6ba021094c40cb0ff3c4c254044"
|
||||
integrity sha512-TSVCHh3UUZLtNzh141JwLicfYTc7TvVFvQJSWeOZLHr3Sk+9hqEY+9Itaqp1DAW92r4i25ChaMc/50sg4etAWQ==
|
||||
"@atproto/api@^0.10.4":
|
||||
version "0.10.4"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.10.4.tgz#b73446f2344783c42c6040082756449443f15750"
|
||||
integrity sha512-9gwZt4v4pngfD4mgsET9i9Ym0PpMSzftTzqBjCbFpObx15zMkFemYnLUnyT/NEww2u/aRxjAe2TeBnU0dIbbuQ==
|
||||
dependencies:
|
||||
"@atproto/common-web" "^0.2.3"
|
||||
"@atproto/lexicon" "^0.3.1"
|
||||
"@atproto/syntax" "^0.1.5"
|
||||
"@atproto/xrpc" "^0.4.1"
|
||||
"@atproto/lexicon" "^0.3.2"
|
||||
"@atproto/syntax" "^0.2.0"
|
||||
"@atproto/xrpc" "^0.4.2"
|
||||
multiformats "^9.9.0"
|
||||
tlds "^1.234.0"
|
||||
typed-emitter "^2.1.0"
|
||||
|
@ -245,6 +245,17 @@
|
|||
multiformats "^9.9.0"
|
||||
zod "^3.21.4"
|
||||
|
||||
"@atproto/lexicon@^0.3.2":
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.3.2.tgz#0085a3acd3a77867b8efe188297a1bbacc55ce5c"
|
||||
integrity sha512-kmGCkrRwpWIqmn/KO4BZwUf8Nmfndk3XvFC06V0ygCWc42g6+t4QP/6ywNW4PgqfZY0Q5aW4EuDfD7KjAFkFtQ==
|
||||
dependencies:
|
||||
"@atproto/common-web" "^0.2.3"
|
||||
"@atproto/syntax" "^0.2.0"
|
||||
iso-datestring-validator "^2.2.2"
|
||||
multiformats "^9.9.0"
|
||||
zod "^3.21.4"
|
||||
|
||||
"@atproto/ozone@^0.0.7":
|
||||
version "0.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/ozone/-/ozone-0.0.7.tgz#bfad82bc1d0900e79401a82f13581f707415505a"
|
||||
|
@ -340,6 +351,13 @@
|
|||
dependencies:
|
||||
"@atproto/common-web" "^0.2.3"
|
||||
|
||||
"@atproto/syntax@^0.2.0":
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/syntax/-/syntax-0.2.0.tgz#4bab724c02e11f8943b8ec101251082cf55067e9"
|
||||
integrity sha512-K+9jl6mtxC9ytlR7msSiP9jVNqtdxEBSt0kOfsC924lqGwuD8nlUAMi1GSMgAZJGg/Rd+0MKXh789heTdeL3HQ==
|
||||
dependencies:
|
||||
"@atproto/common-web" "^0.2.3"
|
||||
|
||||
"@atproto/xrpc-server@^0.4.2":
|
||||
version "0.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/xrpc-server/-/xrpc-server-0.4.2.tgz#23efd89086b85933f1b0cc00c86e895adcaac315"
|
||||
|
@ -365,6 +383,14 @@
|
|||
"@atproto/lexicon" "^0.3.1"
|
||||
zod "^3.21.4"
|
||||
|
||||
"@atproto/xrpc@^0.4.2":
|
||||
version "0.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.4.2.tgz#57812e0624be597b85f21471acf336513f35ccda"
|
||||
integrity sha512-x4x2QB4nWmLjIpz2Ue9n/QVbVyJkk6tQMhvmDQaVFF89E3FcVI4rxF4uhzSxaLpbNtyVQBNEEmNHOr5EJLeHVA==
|
||||
dependencies:
|
||||
"@atproto/lexicon" "^0.3.2"
|
||||
zod "^3.21.4"
|
||||
|
||||
"@aws-crypto/crc32@3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@aws-crypto/crc32/-/crc32-3.0.0.tgz#07300eca214409c33e3ff769cd5697b57fdd38fa"
|
||||
|
|
Loading…
Reference in New Issue