diff options
Diffstat (limited to 'src/view/com')
-rw-r--r-- | src/view/com/auth/create/Step2.tsx | 14 | ||||
-rw-r--r-- | src/view/com/modals/CreateOrEditMuteList.tsx | 4 | ||||
-rw-r--r-- | src/view/com/modals/EditProfile.tsx | 4 | ||||
-rw-r--r-- | src/view/com/modals/Waitlist.tsx | 2 | ||||
-rw-r--r-- | src/view/com/modals/crop-image/CropImage.web.tsx | 6 | ||||
-rw-r--r-- | src/view/com/posts/FeedItem.tsx | 6 | ||||
-rw-r--r-- | src/view/com/profile/ProfileHeader.tsx | 11 | ||||
-rw-r--r-- | src/view/com/profile/ProfileHeaderSuggestedFollows.tsx | 12 | ||||
-rw-r--r-- | src/view/com/util/Link.tsx | 20 | ||||
-rw-r--r-- | src/view/com/util/PostMeta.tsx | 6 | ||||
-rw-r--r-- | src/view/com/util/UserAvatar.tsx | 108 | ||||
-rw-r--r-- | src/view/com/util/UserInfoText.tsx | 4 | ||||
-rw-r--r-- | src/view/com/util/images/AutoSizedImage.tsx | 12 | ||||
-rw-r--r-- | src/view/com/util/images/ImageLayoutGrid.tsx | 8 |
14 files changed, 126 insertions, 91 deletions
diff --git a/src/view/com/auth/create/Step2.tsx b/src/view/com/auth/create/Step2.tsx index 83b0aee40..60e197564 100644 --- a/src/view/com/auth/create/Step2.tsx +++ b/src/view/com/auth/create/Step2.tsx @@ -11,6 +11,7 @@ import {TextInput} from '../util/TextInput' import {Policies} from './Policies' import {ErrorMessage} from 'view/com/util/error/ErrorMessage' import {useStores} from 'state/index' +import {isWeb} from 'platform/detection' /** STEP 2: Your account * @field Invite code or waitlist @@ -60,10 +61,11 @@ export const Step2 = observer(function Step2Impl({ Don't have an invite code?{' '} <TouchableWithoutFeedback onPress={onPressWaitlist} - accessibilityRole="button" - accessibilityLabel="Waitlist" - accessibilityHint="Opens Bluesky waitlist form"> - <Text style={pal.link}>Join the waitlist.</Text> + accessibilityLabel="Join the waitlist." + accessibilityHint=""> + <View style={styles.touchable}> + <Text style={pal.link}>Join the waitlist.</Text> + </View> </TouchableWithoutFeedback> </Text> ) : ( @@ -151,4 +153,8 @@ const styles = StyleSheet.create({ borderRadius: 6, paddingVertical: 14, }, + // @ts-expect-error: Suppressing error due to incomplete `ViewStyle` type definition in react-native-web, missing `cursor` prop as discussed in https://github.com/necolas/react-native-web/issues/832. + touchable: { + ...(isWeb && {cursor: 'pointer'}), + }, }) diff --git a/src/view/com/modals/CreateOrEditMuteList.tsx b/src/view/com/modals/CreateOrEditMuteList.tsx index 3f3cfc5f0..4a440afeb 100644 --- a/src/view/com/modals/CreateOrEditMuteList.tsx +++ b/src/view/com/modals/CreateOrEditMuteList.tsx @@ -18,7 +18,7 @@ import {ListModel} from 'state/models/content/list' import {s, colors, gradients} from 'lib/styles' import {enforceLen} from 'lib/strings/helpers' import {compressIfNeeded} from 'lib/media/manip' -import {UserAvatar} from '../util/UserAvatar' +import {EditableUserAvatar} from '../util/UserAvatar' import {usePalette} from 'lib/hooks/usePalette' import {useTheme} from 'lib/ThemeContext' import {useAnalytics} from 'lib/analytics/analytics' @@ -148,7 +148,7 @@ export function Component({ )} <Text style={[styles.label, pal.text]}>List Avatar</Text> <View style={[styles.avi, {borderColor: pal.colors.background}]}> - <UserAvatar + <EditableUserAvatar type="list" size={80} avatar={avatar} diff --git a/src/view/com/modals/EditProfile.tsx b/src/view/com/modals/EditProfile.tsx index 620aad9fc..58d0857ad 100644 --- a/src/view/com/modals/EditProfile.tsx +++ b/src/view/com/modals/EditProfile.tsx @@ -20,7 +20,7 @@ import {enforceLen} from 'lib/strings/helpers' import {MAX_DISPLAY_NAME, MAX_DESCRIPTION} from 'lib/constants' import {compressIfNeeded} from 'lib/media/manip' import {UserBanner} from '../util/UserBanner' -import {UserAvatar} from '../util/UserAvatar' +import {EditableUserAvatar} from '../util/UserAvatar' import {usePalette} from 'lib/hooks/usePalette' import {useTheme} from 'lib/ThemeContext' import {useAnalytics} from 'lib/analytics/analytics' @@ -153,7 +153,7 @@ export function Component({ onSelectNewBanner={onSelectNewBanner} /> <View style={[styles.avi, {borderColor: pal.colors.background}]}> - <UserAvatar + <EditableUserAvatar size={80} avatar={userAvatar} onSelectNewAvatar={onSelectNewAvatar} diff --git a/src/view/com/modals/Waitlist.tsx b/src/view/com/modals/Waitlist.tsx index 1104c0a39..0fb371fe4 100644 --- a/src/view/com/modals/Waitlist.tsx +++ b/src/view/com/modals/Waitlist.tsx @@ -77,6 +77,8 @@ export function Component({}: {}) { keyboardAppearance={theme.colorScheme} value={email} onChangeText={setEmail} + onSubmitEditing={onPressSignup} + enterKeyHint="done" accessible={true} accessibilityLabel="Email" accessibilityHint="Input your email to get on the Bluesky waitlist" diff --git a/src/view/com/modals/crop-image/CropImage.web.tsx b/src/view/com/modals/crop-image/CropImage.web.tsx index c5959cf4c..8e35201d1 100644 --- a/src/view/com/modals/crop-image/CropImage.web.tsx +++ b/src/view/com/modals/crop-image/CropImage.web.tsx @@ -100,7 +100,7 @@ export function Component({ accessibilityHint="Sets image aspect ratio to wide"> <RectWideIcon size={24} - style={as === AspectRatio.Wide ? s.blue3 : undefined} + style={as === AspectRatio.Wide ? s.blue3 : pal.text} /> </TouchableOpacity> <TouchableOpacity @@ -110,7 +110,7 @@ export function Component({ accessibilityHint="Sets image aspect ratio to tall"> <RectTallIcon size={24} - style={as === AspectRatio.Tall ? s.blue3 : undefined} + style={as === AspectRatio.Tall ? s.blue3 : pal.text} /> </TouchableOpacity> <TouchableOpacity @@ -120,7 +120,7 @@ export function Component({ accessibilityHint="Sets image aspect ratio to square"> <SquareIcon size={24} - style={as === AspectRatio.Square ? s.blue3 : undefined} + style={as === AspectRatio.Square ? s.blue3 : pal.text} /> </TouchableOpacity> </View> diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index 1ceae80ae..6aac450ff 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -9,7 +9,7 @@ import { } from '@fortawesome/react-native-fontawesome' import {PostsFeedItemModel} from 'state/models/feeds/post' import {FeedSourceInfo} from 'lib/api/feed/types' -import {Link, DesktopWebTextLink} from '../util/Link' +import {Link, TextLinkOnWebOnly} from '../util/Link' import {Text} from '../util/text/Text' import {UserInfoText} from '../util/UserInfoText' import {PostMeta} from '../util/PostMeta' @@ -189,7 +189,7 @@ export const FeedItem = observer(function FeedItemImpl({ lineHeight={1.2} numberOfLines={1}> From{' '} - <DesktopWebTextLink + <TextLinkOnWebOnly type="sm-bold" style={pal.textLight} lineHeight={1.2} @@ -220,7 +220,7 @@ export const FeedItem = observer(function FeedItemImpl({ lineHeight={1.2} numberOfLines={1}> Reposted by{' '} - <DesktopWebTextLink + <TextLinkOnWebOnly type="sm-bold" style={pal.textLight} lineHeight={1.2} diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx index df19ecad5..5514bf98e 100644 --- a/src/view/com/profile/ProfileHeader.tsx +++ b/src/view/com/profile/ProfileHeader.tsx @@ -132,20 +132,19 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({ }, [store, view]) const onPressToggleFollow = React.useCallback(() => { - track( - view.viewer.following - ? 'ProfileHeader:FollowButtonClicked' - : 'ProfileHeader:UnfollowButtonClicked', - ) view?.toggleFollowing().then( () => { setShowSuggestedFollows(Boolean(view.viewer.following)) - Toast.show( `${ view.viewer.following ? 'Following' : 'No longer following' } ${sanitizeDisplayName(view.displayName || view.handle)}`, ) + track( + view.viewer.following + ? 'ProfileHeader:FollowButtonClicked' + : 'ProfileHeader:UnfollowButtonClicked', + ) }, err => store.log.error('Failed to toggle follow', err), ) diff --git a/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx b/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx index c5b187fb3..cf759ddd1 100644 --- a/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx +++ b/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {View, StyleSheet, ScrollView, Pressable} from 'react-native' +import {View, StyleSheet, Pressable, ScrollView} from 'react-native' import Animated, { useSharedValue, withTiming, @@ -26,6 +26,7 @@ import {sanitizeHandle} from 'lib/strings/handles' import {makeProfileLink} from 'lib/routes/links' import {Link} from 'view/com/util/Link' import {useAnalytics} from 'lib/analytics/analytics' +import {isWeb} from 'platform/detection' const OUTER_PADDING = 10 const INNER_PADDING = 14 @@ -100,7 +101,6 @@ export function ProfileHeaderSuggestedFollows({ backgroundColor: pal.viewLight.backgroundColor, height: '100%', paddingTop: INNER_PADDING / 2, - paddingBottom: INNER_PADDING, }}> <View style={{ @@ -130,11 +130,15 @@ export function ProfileHeaderSuggestedFollows({ </View> <ScrollView - horizontal - showsHorizontalScrollIndicator={false} + horizontal={true} + showsHorizontalScrollIndicator={isWeb} + persistentScrollbar={true} + scrollIndicatorInsets={{bottom: 0}} + scrollEnabled={true} contentContainerStyle={{ alignItems: 'flex-start', paddingLeft: INNER_PADDING / 2, + paddingBottom: INNER_PADDING, }}> {isLoading ? ( <> diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx index 6915d3e08..1777f6659 100644 --- a/src/view/com/util/Link.tsx +++ b/src/view/com/util/Link.tsx @@ -1,5 +1,4 @@ -import React, {ComponentProps, useMemo} from 'react' -import {observer} from 'mobx-react-lite' +import React, {ComponentProps, memo, useMemo} from 'react' import { Linking, GestureResponderEvent, @@ -28,11 +27,10 @@ import { isExternalUrl, linkRequiresWarning, } from 'lib/strings/url-helpers' -import {isAndroid} from 'platform/detection' +import {isAndroid, isWeb} from 'platform/detection' import {sanitizeUrl} from '@braintree/sanitize-url' import {PressableWithHover} from './PressableWithHover' import FixedTouchableHighlight from '../pager/FixedTouchableHighlight' -import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' type Event = | React.MouseEvent<HTMLAnchorElement, MouseEvent> @@ -50,7 +48,7 @@ interface Props extends ComponentProps<typeof TouchableOpacity> { anchorNoUnderline?: boolean } -export const Link = observer(function Link({ +export const Link = memo(function Link({ testID, style, href, @@ -136,7 +134,7 @@ export const Link = observer(function Link({ ) }) -export const TextLink = observer(function TextLink({ +export const TextLink = memo(function TextLink({ testID, type = 'md', style, @@ -223,7 +221,7 @@ export const TextLink = observer(function TextLink({ /** * Only acts as a link on desktop web */ -interface DesktopWebTextLinkProps extends TextProps { +interface TextLinkOnWebOnlyProps extends TextProps { testID?: string type?: TypographyVariant style?: StyleProp<TextStyle> @@ -236,7 +234,7 @@ interface DesktopWebTextLinkProps extends TextProps { accessibilityHint?: string title?: string } -export const DesktopWebTextLink = observer(function DesktopWebTextLink({ +export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({ testID, type = 'md', style, @@ -245,10 +243,8 @@ export const DesktopWebTextLink = observer(function DesktopWebTextLink({ numberOfLines, lineHeight, ...props -}: DesktopWebTextLinkProps) { - const {isDesktop} = useWebMediaQueries() - - if (isDesktop) { +}: TextLinkOnWebOnlyProps) { + if (isWeb) { return ( <TextLink testID={testID} diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx index b5c47dea5..c5e438f8d 100644 --- a/src/view/com/util/PostMeta.tsx +++ b/src/view/com/util/PostMeta.tsx @@ -1,7 +1,7 @@ import React from 'react' import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native' import {Text} from './text/Text' -import {DesktopWebTextLink} from './Link' +import {TextLinkOnWebOnly} from './Link' import {niceDate} from 'lib/strings/time' import {usePalette} from 'lib/hooks/usePalette' import {TypographyVariant} from 'lib/ThemeContext' @@ -47,7 +47,7 @@ export const PostMeta = observer(function PostMetaImpl(opts: PostMetaOpts) { </View> )} <View style={styles.maxWidth}> - <DesktopWebTextLink + <TextLinkOnWebOnly type={opts.displayNameType || 'lg-bold'} style={[pal.text, opts.displayNameStyle]} numberOfLines={1} @@ -78,7 +78,7 @@ export const PostMeta = observer(function PostMetaImpl(opts: PostMetaOpts) { )} <TimeElapsed timestamp={opts.timestamp}> {({timeElapsed}) => ( - <DesktopWebTextLink + <TextLinkOnWebOnly type="md" style={pal.textLight} lineHeight={1.2} diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index d24e47499..fbc0b5e11 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -23,14 +23,18 @@ interface BaseUserAvatarProps { type?: Type size: number avatar?: string | null - moderation?: ModerationUI } interface UserAvatarProps extends BaseUserAvatarProps { - onSelectNewAvatar?: (img: RNImage | null) => void + moderation?: ModerationUI +} + +interface EditableUserAvatarProps extends BaseUserAvatarProps { + onSelectNewAvatar: (img: RNImage | null) => void } interface PreviewableUserAvatarProps extends BaseUserAvatarProps { + moderation?: ModerationUI did: string handle: string } @@ -106,8 +110,65 @@ export function UserAvatar({ size, avatar, moderation, - onSelectNewAvatar, }: UserAvatarProps) { + const pal = usePalette('default') + + const aviStyle = useMemo(() => { + if (type === 'algo' || type === 'list') { + return { + width: size, + height: size, + borderRadius: size > 32 ? 8 : 3, + } + } + return { + width: size, + height: size, + borderRadius: Math.floor(size / 2), + } + }, [type, size]) + + const alert = useMemo(() => { + if (!moderation?.alert) { + return null + } + return ( + <View style={[styles.alertIconContainer, pal.view]}> + <FontAwesomeIcon + icon="exclamation-circle" + style={styles.alertIcon} + size={Math.floor(size / 3)} + /> + </View> + ) + }, [moderation?.alert, size, pal]) + + return avatar && + !((moderation?.blur && isAndroid) /* android crashes with blur */) ? ( + <View style={{width: size, height: size}}> + <HighPriorityImage + testID="userAvatarImage" + style={aviStyle} + contentFit="cover" + source={{uri: avatar}} + blurRadius={moderation?.blur ? BLUR_AMOUNT : 0} + /> + {alert} + </View> + ) : ( + <View style={{width: size, height: size}}> + <DefaultAvatar type={type} size={size} /> + {alert} + </View> + ) +} + +export function EditableUserAvatar({ + type = 'user', + size, + avatar, + onSelectNewAvatar, +}: EditableUserAvatarProps) { const store = useStores() const pal = usePalette('default') const {requestCameraAccessIfNeeded} = useCameraPermission() @@ -146,7 +207,7 @@ export function UserAvatar({ return } - onSelectNewAvatar?.( + onSelectNewAvatar( await openCamera(store, { width: 1000, height: 1000, @@ -186,7 +247,7 @@ export function UserAvatar({ path: item.path, }) - onSelectNewAvatar?.(croppedImage) + onSelectNewAvatar(croppedImage) }, }, !!avatar && { @@ -203,7 +264,7 @@ export function UserAvatar({ web: 'trash', }, onPress: async () => { - onSelectNewAvatar?.(null) + onSelectNewAvatar(null) }, }, ].filter(Boolean) as DropdownItem[], @@ -216,23 +277,7 @@ export function UserAvatar({ ], ) - const alert = useMemo(() => { - if (!moderation?.alert) { - return null - } - return ( - <View style={[styles.alertIconContainer, pal.view]}> - <FontAwesomeIcon - icon="exclamation-circle" - style={styles.alertIcon} - size={Math.floor(size / 3)} - /> - </View> - ) - }, [moderation?.alert, size, pal]) - - // onSelectNewAvatar is only passed as prop on the EditProfile component - return onSelectNewAvatar ? ( + return ( <NativeDropdown testID="changeAvatarBtn" items={dropdownItems} @@ -256,23 +301,6 @@ export function UserAvatar({ /> </View> </NativeDropdown> - ) : avatar && - !((moderation?.blur && isAndroid) /* android crashes with blur */) ? ( - <View style={{width: size, height: size}}> - <HighPriorityImage - testID="userAvatarImage" - style={aviStyle} - contentFit="cover" - source={{uri: avatar}} - blurRadius={moderation?.blur ? BLUR_AMOUNT : 0} - /> - {alert} - </View> - ) : ( - <View style={{width: size, height: size}}> - <DefaultAvatar type={type} size={size} /> - {alert} - </View> ) } diff --git a/src/view/com/util/UserInfoText.tsx b/src/view/com/util/UserInfoText.tsx index 695711b2a..e4ca981d9 100644 --- a/src/view/com/util/UserInfoText.tsx +++ b/src/view/com/util/UserInfoText.tsx @@ -1,7 +1,7 @@ import React, {useState, useEffect} from 'react' import {AppBskyActorGetProfile as GetProfile} from '@atproto/api' import {StyleProp, StyleSheet, TextStyle} from 'react-native' -import {DesktopWebTextLink} from './Link' +import {TextLinkOnWebOnly} from './Link' import {Text} from './text/Text' import {LoadingPlaceholder} from './LoadingPlaceholder' import {useStores} from 'state/index' @@ -65,7 +65,7 @@ export function UserInfoText({ ) } else if (profile) { inner = ( - <DesktopWebTextLink + <TextLinkOnWebOnly type={type} style={style} lineHeight={1.2} diff --git a/src/view/com/util/images/AutoSizedImage.tsx b/src/view/com/util/images/AutoSizedImage.tsx index 035e29c25..6cbcddc32 100644 --- a/src/view/com/util/images/AutoSizedImage.tsx +++ b/src/view/com/util/images/AutoSizedImage.tsx @@ -52,20 +52,20 @@ export function AutoSizedImage({ if (onPress || onLongPress || onPressIn) { return ( + // disable a11y rule because in this case we want the tags on the image (#1640) + // eslint-disable-next-line react-native-a11y/has-valid-accessibility-descriptors <Pressable onPress={onPress} onLongPress={onLongPress} onPressIn={onPressIn} - style={[styles.container, style]} - accessible={true} - accessibilityRole="button" - accessibilityLabel={alt || 'Image'} - accessibilityHint="Tap to view fully"> + style={[styles.container, style]}> <Image style={[styles.image, {aspectRatio}]} source={uri} - accessible={false} // Must set for `accessibilityLabel` to work + accessible={true} // Must set for `accessibilityLabel` to work accessibilityIgnoresInvertColors + accessibilityLabel={alt} + accessibilityHint="Tap to view fully" /> {children} </Pressable> diff --git a/src/view/com/util/images/ImageLayoutGrid.tsx b/src/view/com/util/images/ImageLayoutGrid.tsx index 2e352d086..4aa6f28de 100644 --- a/src/view/com/util/images/ImageLayoutGrid.tsx +++ b/src/view/com/util/images/ImageLayoutGrid.tsx @@ -63,8 +63,8 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) { case 4: return ( - <View style={styles.flexRow}> - <View style={{flex: 1}}> + <> + <View style={styles.flexRow}> <View style={styles.smallItem}> <GalleryItem {...props} index={0} imageStyle={styles.image} /> </View> @@ -72,7 +72,7 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) { <GalleryItem {...props} index={2} imageStyle={styles.image} /> </View> </View> - <View style={{flex: 1}}> + <View style={styles.flexRow}> <View style={styles.smallItem}> <GalleryItem {...props} index={1} imageStyle={styles.image} /> </View> @@ -80,7 +80,7 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) { <GalleryItem {...props} index={3} imageStyle={styles.image} /> </View> </View> - </View> + </> ) default: |