Merge remote-tracking branch 'origin/main' into 3p-moderators
* origin/main: (33 commits) Improved server selector during account creation and signin (#2840) remove line height from text input for composer on ios (#2844) Add follow button to highlighted post (#2828) Cleaning up old codes pt_BR (#2809) Fix wrong translations in zh-CN localization (#2812) Adjust Japanese translation (#2814) Update ja messages.po (#2821) change follow to follow all when !== 20 (#2831) ios adult content link fix 🤦 (#2845) improves build.md completeness (#2835) increase stroke width for active hashtag icon (#2829) Update `blueskyweb.xyz` links to `bsky.social` (#2830) eas nightlies (#2826) Always show post dropdown button at the bottom of the post, add share button to highlighted post (#2646) Measure header layout reliably (#2817) Round line height (#2824) Design system tweaks (#2822) Fix layout calculations (#2816) Fix flashes and jumps when opening profile (#2815) Show text cursor on web bio (#2813) ...
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import {LabelPreference} from '@atproto/api'
|
||||
import {StyleSheet, Pressable, View} from 'react-native'
|
||||
import {StyleSheet, Pressable, View, Linking} from 'react-native'
|
||||
import LinearGradient from 'react-native-linear-gradient'
|
||||
import {ScrollView} from './util'
|
||||
import {s, colors, gradients} from 'lib/styles'
|
||||
@@ -129,6 +129,10 @@ function AdultContentEnabledPref() {
|
||||
}
|
||||
}, [variables, preferences, mutate, _])
|
||||
|
||||
const onAdultContentLinkPress = React.useCallback(() => {
|
||||
Linking.openURL('https://bsky.app/')
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<View style={s.mb10}>
|
||||
{isIOS ? (
|
||||
@@ -138,8 +142,9 @@ function AdultContentEnabledPref() {
|
||||
Adult content can only be enabled via the Web at{' '}
|
||||
<TextLink
|
||||
style={pal.link}
|
||||
href="https://bsky.app"
|
||||
href=""
|
||||
text="bsky.app"
|
||||
onPress={onAdultContentLinkPress}
|
||||
/>
|
||||
.
|
||||
</Trans>
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
import React, {useRef, useEffect} from 'react'
|
||||
import {StyleSheet} from 'react-native'
|
||||
import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {SafeAreaView} from 'react-native-safe-area-context'
|
||||
import BottomSheet from '@gorhom/bottom-sheet'
|
||||
import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {timeout} from 'lib/async/timeout'
|
||||
import {navigate} from '../../../Navigation'
|
||||
import once from 'lodash.once'
|
||||
|
||||
import {useModals, useModalControls} from '#/state/modals'
|
||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||
import * as ConfirmModal from './Confirm'
|
||||
import * as EditProfileModal from './EditProfile'
|
||||
import * as ProfilePreviewModal from './ProfilePreview'
|
||||
import * as ServerInputModal from './ServerInput'
|
||||
import * as RepostModal from './Repost'
|
||||
import * as SelfLabelModal from './SelfLabel'
|
||||
import * as ThreadgateModal from './Threadgate'
|
||||
@@ -50,34 +44,14 @@ export function ModalsContainer() {
|
||||
const {closeModal} = useModalControls()
|
||||
const bottomSheetRef = useRef<BottomSheet>(null)
|
||||
const pal = usePalette('default')
|
||||
const safeAreaInsets = useSafeAreaInsets()
|
||||
|
||||
const activeModal = activeModals[activeModals.length - 1]
|
||||
|
||||
const navigateOnce = once(navigate)
|
||||
|
||||
// It seems like the bottom sheet bugs out when this callback changes.
|
||||
const onBottomSheetAnimate = useNonReactiveCallback(
|
||||
(_fromIndex: number, toIndex: number) => {
|
||||
if (activeModal?.name === 'profile-preview' && toIndex === 1) {
|
||||
// begin loading the profile screen behind the scenes
|
||||
navigateOnce('Profile', {name: activeModal.did})
|
||||
}
|
||||
},
|
||||
)
|
||||
const onBottomSheetChange = async (snapPoint: number) => {
|
||||
if (snapPoint === -1) {
|
||||
closeModal()
|
||||
} else if (activeModal?.name === 'profile-preview' && snapPoint === 1) {
|
||||
await navigateOnce('Profile', {name: activeModal.did})
|
||||
// There is no particular callback for when the view has actually been presented.
|
||||
// This delay gives us a decent chance the navigation has flushed *and* images have loaded.
|
||||
// It's acceptable because the data is already being fetched + it usually takes longer anyway.
|
||||
// TODO: Figure out why avatar/cover don't always show instantly from cache.
|
||||
await timeout(200)
|
||||
closeModal()
|
||||
}
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
bottomSheetRef.current?.close()
|
||||
closeModal()
|
||||
@@ -91,7 +65,6 @@ export function ModalsContainer() {
|
||||
}
|
||||
}, [isModalActive, bottomSheetRef, activeModal?.name])
|
||||
|
||||
let needsSafeTopInset = false
|
||||
let snapPoints: (string | number)[] = DEFAULT_SNAPPOINTS
|
||||
let element
|
||||
if (activeModal?.name === 'confirm') {
|
||||
@@ -100,13 +73,6 @@ export function ModalsContainer() {
|
||||
} else if (activeModal?.name === 'edit-profile') {
|
||||
snapPoints = EditProfileModal.snapPoints
|
||||
element = <EditProfileModal.Component {...activeModal} />
|
||||
} else if (activeModal?.name === 'profile-preview') {
|
||||
snapPoints = ProfilePreviewModal.snapPoints
|
||||
element = <ProfilePreviewModal.Component {...activeModal} />
|
||||
needsSafeTopInset = true // Need to align with the target profile screen.
|
||||
} else if (activeModal?.name === 'server-input') {
|
||||
snapPoints = ServerInputModal.snapPoints
|
||||
element = <ServerInputModal.Component {...activeModal} />
|
||||
} else if (activeModal?.name === 'report') {
|
||||
snapPoints = ReportModal.snapPoints
|
||||
element = <ReportModal.Component {...activeModal} />
|
||||
@@ -200,12 +166,10 @@ export function ModalsContainer() {
|
||||
)
|
||||
}
|
||||
|
||||
const topInset = needsSafeTopInset ? safeAreaInsets.top - HANDLE_HEIGHT : 0
|
||||
return (
|
||||
<BottomSheet
|
||||
ref={bottomSheetRef}
|
||||
snapPoints={snapPoints}
|
||||
topInset={topInset}
|
||||
handleHeight={HANDLE_HEIGHT}
|
||||
index={isModalActive ? 0 : -1}
|
||||
enablePanDownToClose
|
||||
@@ -216,7 +180,6 @@ export function ModalsContainer() {
|
||||
}
|
||||
handleIndicatorStyle={{backgroundColor: pal.text.color}}
|
||||
handleStyle={[styles.handle, pal.view]}
|
||||
onAnimate={onBottomSheetAnimate}
|
||||
onChange={onBottomSheetChange}>
|
||||
{element}
|
||||
</BottomSheet>
|
||||
|
||||
@@ -9,8 +9,6 @@ import {useModals, useModalControls} from '#/state/modals'
|
||||
import type {Modal as ModalIface} from '#/state/modals'
|
||||
import * as ConfirmModal from './Confirm'
|
||||
import * as EditProfileModal from './EditProfile'
|
||||
import * as ProfilePreviewModal from './ProfilePreview'
|
||||
import * as ServerInputModal from './ServerInput'
|
||||
import * as ReportModal from './report/Modal'
|
||||
import * as AppealLabelModal from './AppealLabel'
|
||||
import * as CreateOrEditListModal from './CreateOrEditList'
|
||||
@@ -85,10 +83,6 @@ function Modal({modal}: {modal: ModalIface}) {
|
||||
element = <ConfirmModal.Component {...modal} />
|
||||
} else if (modal.name === 'edit-profile') {
|
||||
element = <EditProfileModal.Component {...modal} />
|
||||
} else if (modal.name === 'profile-preview') {
|
||||
element = <ProfilePreviewModal.Component {...modal} />
|
||||
} else if (modal.name === 'server-input') {
|
||||
element = <ServerInputModal.Component {...modal} />
|
||||
} else if (modal.name === 'report') {
|
||||
element = <ReportModal.Component {...modal} />
|
||||
} else if (modal.name === 'appeal-label') {
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
import React, {useState, useEffect} from 'react'
|
||||
import {ActivityIndicator, StyleSheet, View} from 'react-native'
|
||||
import {AppBskyActorDefs, ModerationOpts, moderateProfile} from '@atproto/api'
|
||||
import {ThemedText} from '../util/text/ThemedText'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {ProfileHeader} from '../profile/ProfileHeader'
|
||||
import {InfoCircleIcon} from 'lib/icons'
|
||||
import {useNavigationState} from '@react-navigation/native'
|
||||
import {s} from 'lib/styles'
|
||||
import {useModerationOpts} from '#/state/queries/preferences'
|
||||
import {useProfileQuery} from '#/state/queries/profile'
|
||||
import {ErrorScreen} from '../util/error/ErrorScreen'
|
||||
import {CenteredView} from '../util/Views'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
export const snapPoints = [520, '100%']
|
||||
|
||||
export function Component({did}: {did: string}) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const moderationOpts = useModerationOpts()
|
||||
const {
|
||||
data: profile,
|
||||
error: profileError,
|
||||
refetch: refetchProfile,
|
||||
isLoading: isLoadingProfile,
|
||||
} = useProfileQuery({
|
||||
did: did,
|
||||
})
|
||||
|
||||
if (isLoadingProfile || !moderationOpts) {
|
||||
return (
|
||||
<CenteredView style={[pal.view, s.flex1]}>
|
||||
<ProfileHeader
|
||||
profile={null}
|
||||
moderation={null}
|
||||
isProfilePreview={true}
|
||||
/>
|
||||
</CenteredView>
|
||||
)
|
||||
}
|
||||
if (profileError) {
|
||||
return (
|
||||
<ErrorScreen
|
||||
title={_(msg`Oops!`)}
|
||||
message={cleanError(profileError)}
|
||||
onPressTryAgain={refetchProfile}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (profile && moderationOpts) {
|
||||
return <ComponentLoaded profile={profile} moderationOpts={moderationOpts} />
|
||||
}
|
||||
// should never happen
|
||||
return (
|
||||
<ErrorScreen
|
||||
title={_(msg`Oops!`)}
|
||||
message={_(msg`Something went wrong and we're not sure what.`)}
|
||||
onPressTryAgain={refetchProfile}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function ComponentLoaded({
|
||||
profile: profileUnshadowed,
|
||||
moderationOpts,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||
moderationOpts: ModerationOpts
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const profile = useProfileShadow(profileUnshadowed)
|
||||
const {screen} = useAnalytics()
|
||||
const moderation = React.useMemo(
|
||||
() => moderateProfile(profile, moderationOpts),
|
||||
[profile, moderationOpts],
|
||||
)
|
||||
|
||||
// track the navigator state to detect if a page-load occurred
|
||||
const navState = useNavigationState(state => state)
|
||||
const [initNavState] = useState(navState)
|
||||
const isLoading = initNavState !== navState
|
||||
|
||||
useEffect(() => {
|
||||
screen('Profile:Preview')
|
||||
}, [screen])
|
||||
|
||||
return (
|
||||
<View testID="profilePreview" style={[pal.view, s.flex1]}>
|
||||
<View style={[styles.headerWrapper]}>
|
||||
<ProfileHeader
|
||||
profile={profile}
|
||||
moderation={moderation}
|
||||
hideBackButton
|
||||
isProfilePreview
|
||||
/>
|
||||
</View>
|
||||
<View style={[styles.hintWrapper, pal.view]}>
|
||||
<View style={styles.hint}>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator />
|
||||
) : (
|
||||
<>
|
||||
<InfoCircleIcon size={21} style={pal.textLight} />
|
||||
<ThemedText type="xl" fg="light">
|
||||
<Trans>Swipe up to see more</Trans>
|
||||
</ThemedText>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
headerWrapper: {
|
||||
height: 440,
|
||||
},
|
||||
hintWrapper: {
|
||||
height: 80,
|
||||
},
|
||||
hint: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
paddingHorizontal: 14,
|
||||
borderRadius: 6,
|
||||
},
|
||||
})
|
||||
@@ -1,189 +0,0 @@
|
||||
import React, {useState} from 'react'
|
||||
import {Platform, StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconStyle,
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import {ScrollView, TextInput} from './util'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {s, colors} from 'lib/styles'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {LOCAL_DEV_SERVICE, STAGING_SERVICE, PROD_SERVICE} from 'lib/constants'
|
||||
import {LOGIN_INCLUDE_DEV_SERVERS} from 'lib/build-flags'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
||||
export const snapPoints = ['80%']
|
||||
|
||||
export function Component({onSelect}: {onSelect: (url: string) => void}) {
|
||||
const theme = useTheme()
|
||||
const pal = usePalette('default')
|
||||
const [customUrl, setCustomUrl] = useState<string>('')
|
||||
const {_} = useLingui()
|
||||
const {closeModal} = useModalControls()
|
||||
|
||||
const doSelect = (url: string) => {
|
||||
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||||
url = `https://${url}`
|
||||
}
|
||||
closeModal()
|
||||
onSelect(url)
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[pal.view, s.flex1]} testID="serverInputModal">
|
||||
<Text type="2xl-bold" style={[pal.text, s.textCenter]}>
|
||||
<Trans>Choose Service</Trans>
|
||||
</Text>
|
||||
<ScrollView style={styles.inner}>
|
||||
<View style={styles.group}>
|
||||
{LOGIN_INCLUDE_DEV_SERVERS ? (
|
||||
<>
|
||||
<TouchableOpacity
|
||||
testID="localDevServerButton"
|
||||
style={styles.btn}
|
||||
onPress={() => doSelect(LOCAL_DEV_SERVICE)}
|
||||
accessibilityRole="button">
|
||||
<Text style={styles.btnText}>
|
||||
<Trans>Local dev server</Trans>
|
||||
</Text>
|
||||
<FontAwesomeIcon
|
||||
icon="arrow-right"
|
||||
style={s.white as FontAwesomeIconStyle}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={styles.btn}
|
||||
onPress={() => doSelect(STAGING_SERVICE)}
|
||||
accessibilityRole="button">
|
||||
<Text style={styles.btnText}>
|
||||
<Trans>Staging</Trans>
|
||||
</Text>
|
||||
<FontAwesomeIcon
|
||||
icon="arrow-right"
|
||||
style={s.white as FontAwesomeIconStyle}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
) : undefined}
|
||||
<TouchableOpacity
|
||||
style={styles.btn}
|
||||
onPress={() => doSelect(PROD_SERVICE)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Select Bluesky Social`)}
|
||||
accessibilityHint="Sets Bluesky Social as your service provider">
|
||||
<Text style={styles.btnText}>
|
||||
<Trans>Bluesky.Social</Trans>
|
||||
</Text>
|
||||
<FontAwesomeIcon
|
||||
icon="arrow-right"
|
||||
style={s.white as FontAwesomeIconStyle}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.group}>
|
||||
<Text style={[pal.text, styles.label]}>
|
||||
<Trans>Other service</Trans>
|
||||
</Text>
|
||||
<View style={s.flexRow}>
|
||||
<TextInput
|
||||
testID="customServerTextInput"
|
||||
style={[pal.borderDark, pal.text, styles.textInput]}
|
||||
placeholder="e.g. https://bsky.app"
|
||||
placeholderTextColor={colors.gray4}
|
||||
autoCapitalize="none"
|
||||
autoComplete="off"
|
||||
autoCorrect={false}
|
||||
keyboardAppearance={theme.colorScheme}
|
||||
value={customUrl}
|
||||
onChangeText={setCustomUrl}
|
||||
accessibilityLabel={_(msg`Custom domain`)}
|
||||
// TODO: Simplify this wording further to be understandable by everyone
|
||||
accessibilityHint={_(
|
||||
msg`Use your domain as your Bluesky client service provider`,
|
||||
)}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
testID="customServerSelectBtn"
|
||||
style={[pal.borderDark, pal.text, styles.textInputBtn]}
|
||||
onPress={() => doSelect(customUrl)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`Confirm service. ${
|
||||
customUrl === ''
|
||||
? _(msg`Button disabled. Input custom domain to proceed.`)
|
||||
: ''
|
||||
}`}
|
||||
accessibilityHint=""
|
||||
// TODO - accessibility: Need to inform state change on failure
|
||||
disabled={customUrl === ''}>
|
||||
<FontAwesomeIcon
|
||||
icon="check"
|
||||
style={[pal.text as FontAwesomeIconStyle, styles.checkIcon]}
|
||||
size={18}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
inner: {
|
||||
padding: 14,
|
||||
},
|
||||
group: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
label: {
|
||||
fontWeight: 'bold',
|
||||
paddingHorizontal: 4,
|
||||
paddingBottom: 4,
|
||||
},
|
||||
textInput: {
|
||||
flex: 1,
|
||||
borderWidth: 1,
|
||||
borderTopLeftRadius: 6,
|
||||
borderBottomLeftRadius: 6,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 12,
|
||||
fontSize: 16,
|
||||
},
|
||||
textInputBtn: {
|
||||
borderWidth: 1,
|
||||
borderLeftWidth: 0,
|
||||
borderTopRightRadius: 6,
|
||||
borderBottomRightRadius: 6,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
btn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.blue3,
|
||||
borderRadius: 6,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 10,
|
||||
marginBottom: 6,
|
||||
},
|
||||
btnText: {
|
||||
flex: 1,
|
||||
fontSize: 18,
|
||||
fontWeight: '500',
|
||||
color: colors.white,
|
||||
},
|
||||
checkIcon: {
|
||||
position: 'relative',
|
||||
...Platform.select({
|
||||
android: {
|
||||
top: 8,
|
||||
},
|
||||
ios: {
|
||||
top: 2,
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
@@ -18,7 +18,7 @@ import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {getAgent} from '#/state/session'
|
||||
|
||||
const DMCA_LINK = 'https://blueskyweb.xyz/support/copyright'
|
||||
const DMCA_LINK = 'https://bsky.social/about/support/copyright'
|
||||
|
||||
export const snapPoints = [575]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user