diff options
Diffstat (limited to 'src/view')
24 files changed, 229 insertions, 101 deletions
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 66722ab2f..0fae996ff 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -30,6 +30,7 @@ import * as apilib from 'lib/api/index' import {ComposerOpts} from 'state/models/ui/shell' import {s, colors, gradients} from 'lib/styles' import {sanitizeDisplayName} from 'lib/strings/display-names' +import {sanitizeHandle} from 'lib/strings/handles' import {cleanError} from 'lib/strings/errors' import {SelectPhotoBtn} from './photos/SelectPhotoBtn' import {OpenCameraBtn} from './photos/OpenCameraBtn' @@ -319,7 +320,8 @@ export const ComposePost = observer(function ComposePost({ <View style={styles.replyToPost}> <Text type="xl-medium" style={[pal.text]}> {sanitizeDisplayName( - replyTo.author.displayName || replyTo.author.handle, + replyTo.author.displayName || + sanitizeHandle(replyTo.author.handle), )} </Text> <Text type="post-text" style={pal.text} numberOfLines={6}> diff --git a/src/view/com/feeds/CustomFeed.tsx b/src/view/com/feeds/CustomFeed.tsx index ef8de8b85..79f1dd74d 100644 --- a/src/view/com/feeds/CustomFeed.tsx +++ b/src/view/com/feeds/CustomFeed.tsx @@ -20,6 +20,7 @@ import {useStores} from 'state/index' import {pluralize} from 'lib/strings/helpers' import {AtUri} from '@atproto/api' import * as Toast from 'view/com/util/Toast' +import {sanitizeHandle} from 'lib/strings/handles' export const CustomFeed = observer( ({ @@ -86,7 +87,7 @@ export const CustomFeed = observer( {item.displayName} </Text> <Text style={[pal.textLight]} numberOfLines={3}> - by @{item.data.creator.handle} + by {sanitizeHandle(item.data.creator.handle, '@')} </Text> </View> {showSaveBtn && ( diff --git a/src/view/com/lists/ListCard.tsx b/src/view/com/lists/ListCard.tsx index b70fa3773..159d966eb 100644 --- a/src/view/com/lists/ListCard.tsx +++ b/src/view/com/lists/ListCard.tsx @@ -9,6 +9,8 @@ import {s} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' import {useStores} from 'state/index' import {sanitizeDisplayName} from 'lib/strings/display-names' +import {sanitizeHandle} from 'lib/strings/handles' +import {makeProfileLink} from 'lib/routes/links' export const ListCard = ({ testID, @@ -57,7 +59,7 @@ export const ListCard = ({ !noBg && pal.view, style, ]} - href={`/profile/${list.creator.did}/lists/${rkey}`} + href={makeProfileLink(list.creator, 'lists', rkey)} title={list.name} asAnchor anchorNoUnderline> @@ -77,7 +79,7 @@ export const ListCard = ({ {list.purpose === 'app.bsky.graph.defs#modlist' && 'Mute list'} by{' '} {list.creator.did === store.me.did ? 'you' - : `@${list.creator.handle}`} + : sanitizeHandle(list.creator.handle, '@')} </Text> {!!list.viewer?.muted && ( <View style={s.flexRow}> diff --git a/src/view/com/lists/ListItems.tsx b/src/view/com/lists/ListItems.tsx index 289ba000b..188518ea5 100644 --- a/src/view/com/lists/ListItems.tsx +++ b/src/view/com/lists/ListItems.tsx @@ -26,6 +26,8 @@ import {useStores} from 'state/index' import {s} from 'lib/styles' import {isDesktopWeb} from 'platform/detection' import {ListActions} from './ListActions' +import {makeProfileLink} from 'lib/routes/links' +import {sanitizeHandle} from 'lib/strings/handles' const LOADING_ITEM = {_reactKey: '__loading__'} const HEADER_ITEM = {_reactKey: '__header__'} @@ -296,8 +298,8 @@ const ListHeader = observer( 'you' ) : ( <TextLink - text={`@${list.creator.handle}`} - href={`/profile/${list.creator.did}`} + text={sanitizeHandle(list.creator.handle, '@')} + href={makeProfileLink(list.creator)} /> )} </Text> diff --git a/src/view/com/modals/ListAddRemoveUser.tsx b/src/view/com/modals/ListAddRemoveUser.tsx index c2d63ef6e..49f46e741 100644 --- a/src/view/com/modals/ListAddRemoveUser.tsx +++ b/src/view/com/modals/ListAddRemoveUser.tsx @@ -16,6 +16,7 @@ import {Button} from '../util/forms/Button' import * as Toast from '../util/Toast' import {useStores} from 'state/index' import {sanitizeDisplayName} from 'lib/strings/display-names' +import {sanitizeHandle} from 'lib/strings/handles' import {s} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' import {isDesktopWeb, isAndroid} from 'platform/detection' @@ -122,7 +123,7 @@ export const Component = observer( by{' '} {list.creator.did === store.me.did ? 'you' - : `@${list.creator.handle}`} + : sanitizeHandle(list.creator.handle, '@')} </Text> </View> <View diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx index d5acf2305..7b9f0715b 100644 --- a/src/view/com/notifications/FeedItem.tsx +++ b/src/view/com/notifications/FeedItem.tsx @@ -19,6 +19,7 @@ import {PostThreadModel} from 'state/models/content/post-thread' import {s, colors} from 'lib/styles' import {ago} from 'lib/strings/time' import {sanitizeDisplayName} from 'lib/strings/display-names' +import {sanitizeHandle} from 'lib/strings/handles' import {pluralize} from 'lib/strings/helpers' import {HeartIconSolid} from 'lib/icons' import {Text} from '../util/text/Text' @@ -36,6 +37,7 @@ import { } from 'lib/labeling/helpers' import {ProfileModeration} from 'lib/labeling/types' import {formatCount} from '../util/numeric/format' +import {makeProfileLink} from 'lib/routes/links' const MAX_AUTHORS = 5 @@ -63,7 +65,7 @@ export const FeedItem = observer(function ({ const urip = new AtUri(item.subjectUri) return `/profile/${urip.host}/post/${urip.rkey}` } else if (item.isFollow) { - return `/profile/${item.author.handle}` + return makeProfileLink(item.author) } else if (item.isReply) { const urip = new AtUri(item.uri) return `/profile/${urip.host}/post/${urip.rkey}` @@ -92,7 +94,7 @@ export const FeedItem = observer(function ({ const authors: Author[] = useMemo(() => { return [ { - href: `/profile/${item.author.handle}`, + href: makeProfileLink(item.author), did: item.author.did, handle: item.author.handle, displayName: item.author.displayName, @@ -104,7 +106,7 @@ export const FeedItem = observer(function ({ }, ...(item.additional?.map(({author}) => { return { - href: `/profile/${author.handle}`, + href: makeProfileLink(author), did: author.did, handle: author.handle, displayName: author.displayName, @@ -158,7 +160,7 @@ export const FeedItem = observer(function ({ action = 'liked your post' icon = 'HeartIconSolid' iconStyle = [ - s.red3 as FontAwesomeIconStyle, + s.likeColor as FontAwesomeIconStyle, {position: 'relative', top: -4}, ] } else if (item.isRepost) { @@ -377,7 +379,7 @@ function ExpandedAuthorsList({ {sanitizeDisplayName(author.displayName || author.handle)} <Text style={[pal.textLight]} lineHeight={1.2}> - {author.handle} + {sanitizeHandle(author.handle)} </Text> </Text> </View> diff --git a/src/view/com/notifications/InvitedUsers.tsx b/src/view/com/notifications/InvitedUsers.tsx index 73469d2ad..1bdb42a9c 100644 --- a/src/view/com/notifications/InvitedUsers.tsx +++ b/src/view/com/notifications/InvitedUsers.tsx @@ -16,6 +16,7 @@ import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' import {s} from 'lib/styles' import {sanitizeDisplayName} from 'lib/strings/display-names' +import {makeProfileLink} from 'lib/routes/links' export const InvitedUsers = observer(() => { const store = useStores() @@ -58,14 +59,14 @@ function InvitedUser({ /> </View> <View style={s.flex1}> - <Link href={`/profile/${profile.handle}`}> + <Link href={makeProfileLink(profile)}> <UserAvatar avatar={profile.avatar} size={35} /> </Link> <Text style={[styles.desc, pal.text]}> <TextLink type="md-bold" style={pal.text} - href={`/profile/${profile.handle}`} + href={makeProfileLink(profile)} text={sanitizeDisplayName(profile.displayName || profile.handle)} />{' '} joined using your invite code! diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index efc9fe694..0680bbc06 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -17,6 +17,7 @@ import {PreviewableUserAvatar} from '../util/UserAvatar' import {s} from 'lib/styles' import {niceDate} from 'lib/strings/time' import {sanitizeDisplayName} from 'lib/strings/display-names' +import {sanitizeHandle} from 'lib/strings/handles' import {pluralize} from 'lib/strings/helpers' import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers' import {useStores} from 'state/index' @@ -31,6 +32,7 @@ import {ErrorMessage} from '../util/error/ErrorMessage' import {usePalette} from 'lib/hooks/usePalette' import {formatCount} from '../util/numeric/format' import {TimeElapsed} from 'view/com/util/TimeElapsed' +import {makeProfileLink} from 'lib/routes/links' const PARENT_REPLY_LINE_LENGTH = 8 @@ -51,20 +53,20 @@ export const PostThreadItem = observer(function PostThreadItem({ const itemCid = item.post.cid const itemHref = React.useMemo(() => { const urip = new AtUri(item.post.uri) - return `/profile/${item.post.author.handle}/post/${urip.rkey}` - }, [item.post.uri, item.post.author.handle]) + return makeProfileLink(item.post.author, 'post', urip.rkey) + }, [item.post.uri, item.post.author]) const itemTitle = `Post by ${item.post.author.handle}` - const authorHref = `/profile/${item.post.author.handle}` + const authorHref = makeProfileLink(item.post.author) const authorTitle = item.post.author.handle const likesHref = React.useMemo(() => { const urip = new AtUri(item.post.uri) - return `/profile/${item.post.author.handle}/post/${urip.rkey}/liked-by` - }, [item.post.uri, item.post.author.handle]) + return makeProfileLink(item.post.author, 'post', urip.rkey, 'liked-by') + }, [item.post.uri, item.post.author]) const likesTitle = 'Likes on this post' const repostsHref = React.useMemo(() => { const urip = new AtUri(item.post.uri) - return `/profile/${item.post.author.handle}/post/${urip.rkey}/reposted-by` - }, [item.post.uri, item.post.author.handle]) + return makeProfileLink(item.post.author, 'post', urip.rkey, 'reposted-by') + }, [item.post.uri, item.post.author]) const repostsTitle = 'Reposts of this post' const primaryLanguage = store.preferences.contentLanguages[0] || 'en' @@ -185,7 +187,8 @@ export const PostThreadItem = observer(function PostThreadItem({ numberOfLines={1} lineHeight={1.2}> {sanitizeDisplayName( - item.post.author.displayName || item.post.author.handle, + item.post.author.displayName || + sanitizeHandle(item.post.author.handle), )} </Text> </Link> @@ -223,7 +226,7 @@ export const PostThreadItem = observer(function PostThreadItem({ href={authorHref} title={authorTitle}> <Text type="md" style={[pal.textLight]} numberOfLines={1}> - @{item.post.author.handle} + {sanitizeHandle(item.post.author.handle, '@')} </Text> </Link> </View> @@ -297,11 +300,7 @@ export const PostThreadItem = observer(function PostThreadItem({ itemCid={itemCid} itemHref={itemHref} itemTitle={itemTitle} - author={{ - avatar: item.post.author.avatar!, - handle: item.post.author.handle, - displayName: item.post.author.displayName!, - }} + author={item.post.author} text={item.richText?.text || record.text} indexedAt={item.post.indexedAt} isAuthor={item.post.author.did === store.me.did} @@ -362,8 +361,7 @@ export const PostThreadItem = observer(function PostThreadItem({ </View> <View style={styles.layoutContent}> <PostMeta - authorHandle={item.post.author.handle} - authorDisplayName={item.post.author.displayName} + author={item.post.author} authorHasWarning={!!item.post.author.labels?.length} timestamp={item.post.indexedAt} postHref={itemHref} @@ -399,11 +397,7 @@ export const PostThreadItem = observer(function PostThreadItem({ itemCid={itemCid} itemHref={itemHref} itemTitle={itemTitle} - author={{ - avatar: item.post.author.avatar!, - handle: item.post.author.handle, - displayName: item.post.author.displayName!, - }} + author={item.post.author} text={item.richText?.text || record.text} indexedAt={item.post.indexedAt} isAuthor={item.post.author.did === store.me.did} diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index bdc84b62f..ac5e7d20b 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -30,6 +30,7 @@ import {useStores} from 'state/index' import {s, colors} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers' +import {makeProfileLink} from 'lib/routes/links' export const Post = observer(function Post({ uri, @@ -125,7 +126,7 @@ const PostLoaded = observer( const itemUri = item.post.uri const itemCid = item.post.cid const itemUrip = new AtUri(item.post.uri) - const itemHref = `/profile/${item.post.author.handle}/post/${itemUrip.rkey}` + const itemHref = makeProfileLink(item.post.author, 'post', itemUrip.rkey) const itemTitle = `Post by ${item.post.author.handle}` let replyAuthorDid = '' if (record.reply) { @@ -222,8 +223,7 @@ const PostLoaded = observer( </View> <View style={styles.layoutContent}> <PostMeta - authorHandle={item.post.author.handle} - authorDisplayName={item.post.author.displayName} + author={item.post.author} authorHasWarning={!!item.post.author.labels?.length} timestamp={item.post.indexedAt} postHref={itemHref} @@ -282,11 +282,7 @@ const PostLoaded = observer( itemCid={itemCid} itemHref={itemHref} itemTitle={itemTitle} - author={{ - avatar: item.post.author.avatar!, - handle: item.post.author.handle, - displayName: item.post.author.displayName!, - }} + author={item.post.author} indexedAt={item.post.indexedAt} text={item.richText?.text || record.text} isAuthor={item.post.author.did === store.me.did} diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index e1b160dcb..75c321145 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -27,7 +27,9 @@ import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' import {useAnalytics} from 'lib/analytics/analytics' import {sanitizeDisplayName} from 'lib/strings/display-names' +import {sanitizeHandle} from 'lib/strings/handles' import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers' +import {makeProfileLink} from 'lib/routes/links' export const FeedItem = observer(function ({ item, @@ -50,8 +52,8 @@ export const FeedItem = observer(function ({ const itemCid = item.post.cid const itemHref = useMemo(() => { const urip = new AtUri(item.post.uri) - return `/profile/${item.post.author.handle}/post/${urip.rkey}` - }, [item.post.uri, item.post.author.handle]) + return makeProfileLink(item.post.author, 'post', urip.rkey) + }, [item.post.uri, item.post.author]) const itemTitle = `Post by ${item.post.author.handle}` const replyAuthorDid = useMemo(() => { if (!record?.reply) { @@ -178,7 +180,7 @@ export const FeedItem = observer(function ({ {item.reasonRepost && ( <Link style={styles.includeReason} - href={`/profile/${item.reasonRepost.by.handle}`} + href={makeProfileLink(item.reasonRepost.by)} title={sanitizeDisplayName( item.reasonRepost.by.displayName || item.reasonRepost.by.handle, )}> @@ -201,9 +203,10 @@ export const FeedItem = observer(function ({ lineHeight={1.2} numberOfLines={1} text={sanitizeDisplayName( - item.reasonRepost.by.displayName || item.reasonRepost.by.handle, + item.reasonRepost.by.displayName || + sanitizeHandle(item.reasonRepost.by.handle), )} - href={`/profile/${item.reasonRepost.by.handle}`} + href={makeProfileLink(item.reasonRepost.by)} /> </Text> </Link> @@ -221,8 +224,7 @@ export const FeedItem = observer(function ({ </View> <View style={styles.layoutContent}> <PostMeta - authorHandle={item.post.author.handle} - authorDisplayName={item.post.author.displayName} + author={item.post.author} authorHasWarning={!!item.post.author.labels?.length} timestamp={item.post.indexedAt} postHref={itemHref} @@ -284,11 +286,7 @@ export const FeedItem = observer(function ({ itemCid={itemCid} itemHref={itemHref} itemTitle={itemTitle} - author={{ - avatar: item.post.author.avatar!, - handle: item.post.author.handle, - displayName: item.post.author.displayName!, - }} + author={item.post.author} text={item.richText?.text || record.text} indexedAt={item.post.indexedAt} isAuthor={item.post.author.did === store.me.did} diff --git a/src/view/com/posts/FeedSlice.tsx b/src/view/com/posts/FeedSlice.tsx index 8ac813b92..b73d4a99d 100644 --- a/src/view/com/posts/FeedSlice.tsx +++ b/src/view/com/posts/FeedSlice.tsx @@ -8,6 +8,7 @@ import Svg, {Circle, Line} from 'react-native-svg' import {FeedItem} from './FeedItem' import {usePalette} from 'lib/hooks/usePalette' import {ModerationBehaviorCode} from 'lib/labeling/types' +import {makeProfileLink} from 'lib/routes/links' export function FeedSlice({ slice, @@ -70,8 +71,8 @@ function ViewFullThread({slice}: {slice: PostsFeedSliceModel}) { const pal = usePalette('default') const itemHref = React.useMemo(() => { const urip = new AtUri(slice.rootItem.post.uri) - return `/profile/${slice.rootItem.post.author.handle}/post/${urip.rkey}` - }, [slice.rootItem.post.uri, slice.rootItem.post.author.handle]) + return makeProfileLink(slice.rootItem.post.author, 'post', urip.rkey) + }, [slice.rootItem.post.uri, slice.rootItem.post.author]) return ( <Link style={[pal.view, styles.viewFullThread]} href={itemHref} noFeedback> diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx index 2dfc7ad30..946e0f2ab 100644 --- a/src/view/com/profile/ProfileCard.tsx +++ b/src/view/com/profile/ProfileCard.tsx @@ -10,11 +10,13 @@ import {usePalette} from 'lib/hooks/usePalette' import {useStores} from 'state/index' import {FollowButton} from './FollowButton' import {sanitizeDisplayName} from 'lib/strings/display-names' +import {sanitizeHandle} from 'lib/strings/handles' import { getProfileViewBasicLabelInfo, getProfileModeration, } from 'lib/labeling/helpers' import {ModerationBehaviorCode} from 'lib/labeling/types' +import {makeProfileLink} from 'lib/routes/links' export const ProfileCard = observer( ({ @@ -60,7 +62,7 @@ export const ProfileCard = observer( noBorder && styles.outerNoBorder, !noBg && pal.view, ]} - href={`/profile/${profile.handle}`} + href={makeProfileLink(profile)} title={profile.handle} asAnchor anchorNoUnderline> @@ -78,10 +80,12 @@ export const ProfileCard = observer( style={[s.bold, pal.text]} numberOfLines={1} lineHeight={1.2}> - {sanitizeDisplayName(profile.displayName || profile.handle)} + {sanitizeDisplayName( + profile.displayName || sanitizeHandle(profile.handle), + )} </Text> <Text type="md" style={[pal.textLight]} numberOfLines={1}> - @{profile.handle} + {sanitizeHandle(profile.handle, '@')} </Text> {!!profile.viewer?.followedBy && ( <View style={s.flexRow}> @@ -160,7 +164,7 @@ export const ProfileCardWithFollowBtn = observer( followers?: AppBskyActorDefs.ProfileView[] | undefined }) => { const store = useStores() - const isMe = store.me.handle === profile.handle + const isMe = store.me.did === profile.did return ( <ProfileCard diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx index 320d8a778..11e9e74c3 100644 --- a/src/view/com/profile/ProfileHeader.tsx +++ b/src/view/com/profile/ProfileHeader.tsx @@ -15,11 +15,13 @@ import {ProfileImageLightbox} from 'state/models/ui/shell' import {pluralize} from 'lib/strings/helpers' import {toShareUrl} from 'lib/strings/url-helpers' import {sanitizeDisplayName} from 'lib/strings/display-names' +import {sanitizeHandle} from 'lib/strings/handles' import {s, colors} from 'lib/styles' import {DropdownButton, DropdownItem} from '../util/forms/DropdownButton' import * as Toast from '../util/Toast' import {LoadingPlaceholder} from '../util/LoadingPlaceholder' import {Text} from '../util/text/Text' +import {ThemedText} from '../util/text/ThemedText' import {TextLink} from '../util/Link' import {RichText} from '../util/text/RichText' import {UserAvatar} from '../util/UserAvatar' @@ -34,6 +36,8 @@ import {FollowState} from 'state/models/cache/my-follows' import {shareUrl} from 'lib/sharing' import {formatCount} from '../util/numeric/format' import {navigate} from '../../../Navigation' +import {isInvalidHandle} from 'lib/strings/handles' +import {makeProfileLink} from 'lib/routes/links' const BACK_HITSLOP = {left: 30, top: 30, right: 30, bottom: 30} @@ -67,7 +71,9 @@ export const ProfileHeader = observer( </View> <View> <Text type="title-2xl" style={[pal.text, styles.title]}> - {sanitizeDisplayName(view.displayName || view.handle)} + {sanitizeDisplayName( + view.displayName || sanitizeHandle(view.handle), + )} </Text> </View> </View> @@ -104,6 +110,7 @@ const ProfileHeaderLoaded = observer( const store = useStores() const navigation = useNavigation<NavigationProp>() const {track} = useAnalytics() + const invalidHandle = isInvalidHandle(view.handle) const onPressBack = React.useCallback(() => { navigation.goBack() @@ -144,19 +151,23 @@ const ProfileHeaderLoaded = observer( const onPressFollowers = React.useCallback(() => { track('ProfileHeader:FollowersButtonClicked') - navigate('ProfileFollowers', {name: view.handle}) + navigate('ProfileFollowers', { + name: isInvalidHandle(view.handle) ? view.did : view.handle, + }) store.shell.closeAllActiveElements() // for when used in the profile preview modal }, [track, view, store.shell]) const onPressFollows = React.useCallback(() => { track('ProfileHeader:FollowsButtonClicked') - navigate('ProfileFollows', {name: view.handle}) + navigate('ProfileFollows', { + name: isInvalidHandle(view.handle) ? view.did : view.handle, + }) store.shell.closeAllActiveElements() // for when used in the profile preview modal }, [track, view, store.shell]) const onPressShare = React.useCallback(() => { track('ProfileHeader:ShareButtonClicked') - const url = toShareUrl(`/profile/${view.handle}`) + const url = toShareUrl(makeProfileLink(view)) shareUrl(url) }, [track, view]) @@ -338,7 +349,7 @@ const ProfileHeaderLoaded = observer( style={[styles.btn, styles.mainBtn, pal.btn]} accessibilityRole="button" accessibilityLabel={`Unfollow ${view.handle}`} - accessibilityHint={`Hides direct posts from ${view.handle} in your feed`}> + accessibilityHint={`Hides posts from ${view.handle} in your feed`}> <FontAwesomeIcon icon="check" style={[pal.text, s.mr5]} @@ -355,7 +366,7 @@ const ProfileHeaderLoaded = observer( style={[styles.btn, styles.mainBtn, palInverted.view]} accessibilityRole="button" accessibilityLabel={`Follow ${view.handle}`} - accessibilityHint={`Shows direct posts from ${view.handle} in your feed`}> + accessibilityHint={`Shows posts from ${view.handle} in your feed`}> <FontAwesomeIcon icon="plus" style={[palInverted.text, s.mr5]} @@ -382,7 +393,9 @@ const ProfileHeaderLoaded = observer( testID="profileHeaderDisplayName" type="title-2xl" style={[pal.text, styles.title]}> - {sanitizeDisplayName(view.displayName || view.handle)} + {sanitizeDisplayName( + view.displayName || sanitizeHandle(view.handle), + )} </Text> </View> <View style={styles.handleLine}> @@ -393,7 +406,16 @@ const ProfileHeaderLoaded = observer( </Text> </View> ) : undefined} - <Text style={[pal.textLight, styles.handle]}>@{view.handle}</Text> + <ThemedText + type={invalidHandle ? 'xs' : 'md'} + fg={invalidHandle ? 'error' : 'light'} + border={invalidHandle ? 'error' : undefined} + style={[ + invalidHandle ? styles.invalidHandle : undefined, + styles.handle, + ]}> + {invalidHandle ? 'ā Invalid Handle' : `@${view.handle}`} + </ThemedText> </View> {!blockHide && ( <> @@ -600,6 +622,11 @@ const styles = StyleSheet.create({ // @ts-ignore web only -prf wordBreak: 'break-all', }, + invalidHandle: { + borderWidth: 1, + borderRadius: 4, + paddingHorizontal: 4, + }, handleLine: { flexDirection: 'row', diff --git a/src/view/com/search/Suggestions.tsx b/src/view/com/search/Suggestions.tsx index c8941e24d..440d912af 100644 --- a/src/view/com/search/Suggestions.tsx +++ b/src/view/com/search/Suggestions.tsx @@ -12,6 +12,7 @@ import {Text} from '../util/text/Text' import {ProfileCardWithFollowBtn} from '../profile/ProfileCard' import {ProfileCardFeedLoadingPlaceholder} from 'view/com/util/LoadingPlaceholder' import {sanitizeDisplayName} from 'lib/strings/display-names' +import {sanitizeHandle} from 'lib/strings/handles' import {RefWithInfoAndFollowers} from 'state/models/discovery/foafs' import {usePalette} from 'lib/hooks/usePalette' @@ -99,7 +100,7 @@ export const Suggestions = observer( _reactKey: `__${item.did}_heading__`, type: 'heading', title: `Followed by ${sanitizeDisplayName( - item.displayName || item.handle, + item.displayName || sanitizeHandle(item.handle), )}`, }, ]) diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx index 5df6b3983..2ce499765 100644 --- a/src/view/com/util/PostMeta.tsx +++ b/src/view/com/util/PostMeta.tsx @@ -7,13 +7,19 @@ import {usePalette} from 'lib/hooks/usePalette' import {UserAvatar} from './UserAvatar' import {observer} from 'mobx-react-lite' import {sanitizeDisplayName} from 'lib/strings/display-names' +import {sanitizeHandle} from 'lib/strings/handles' import {isAndroid} from 'platform/detection' import {TimeElapsed} from './TimeElapsed' +import {makeProfileLink} from 'lib/routes/links' interface PostMetaOpts { - authorAvatar?: string - authorHandle: string - authorDisplayName: string | undefined + author: { + avatar?: string + did: string + handle: string + displayName?: string | undefined + } + showAvatar?: boolean authorHasWarning: boolean postHref: string timestamp: string @@ -21,15 +27,15 @@ interface PostMetaOpts { export const PostMeta = observer(function (opts: PostMetaOpts) { const pal = usePalette('default') - const displayName = opts.authorDisplayName || opts.authorHandle - const handle = opts.authorHandle + const displayName = opts.author.displayName || opts.author.handle + const handle = opts.author.handle return ( <View style={styles.metaOneLine}> - {typeof opts.authorAvatar !== 'undefined' && ( + {opts.showAvatar && typeof opts.author.avatar !== 'undefined' && ( <View style={styles.avatar}> <UserAvatar - avatar={opts.authorAvatar} + avatar={opts.author.avatar} size={16} // TODO moderation /> @@ -43,17 +49,17 @@ export const PostMeta = observer(function (opts: PostMetaOpts) { lineHeight={1.2} text={ <> - {sanitizeDisplayName(displayName)} + {sanitizeDisplayName(displayName)} <Text type="md" - style={[pal.textLight]} numberOfLines={1} - lineHeight={1.2}> - @{handle} + lineHeight={1.2} + style={pal.textLight}> + {sanitizeHandle(handle, '@')} </Text> </> } - href={`/profile/${opts.authorHandle}`} + href={makeProfileLink(opts.author)} /> </View> {!isAndroid && ( @@ -85,6 +91,7 @@ export const PostMeta = observer(function (opts: PostMetaOpts) { const styles = StyleSheet.create({ metaOneLine: { flexDirection: 'row', + alignItems: 'baseline', paddingBottom: 2, gap: 4, }, diff --git a/src/view/com/util/UserInfoText.tsx b/src/view/com/util/UserInfoText.tsx index b737b2b1e..695711b2a 100644 --- a/src/view/com/util/UserInfoText.tsx +++ b/src/view/com/util/UserInfoText.tsx @@ -7,6 +7,8 @@ import {LoadingPlaceholder} from './LoadingPlaceholder' import {useStores} from 'state/index' import {TypographyVariant} from 'lib/ThemeContext' import {sanitizeDisplayName} from 'lib/strings/display-names' +import {sanitizeHandle} from 'lib/strings/handles' +import {makeProfileLink} from 'lib/routes/links' export function UserInfoText({ type = 'md', @@ -68,11 +70,11 @@ export function UserInfoText({ style={style} lineHeight={1.2} numberOfLines={1} - href={`/profile/${profile.handle}`} + href={makeProfileLink(profile)} text={`${prefix || ''}${sanitizeDisplayName( typeof profile[attr] === 'string' && profile[attr] ? (profile[attr] as string) - : profile.handle, + : sanitizeHandle(profile.handle), )}`} /> ) diff --git a/src/view/com/util/UserPreviewLink.tsx b/src/view/com/util/UserPreviewLink.tsx index ae49301fd..7eedbc2d4 100644 --- a/src/view/com/util/UserPreviewLink.tsx +++ b/src/view/com/util/UserPreviewLink.tsx @@ -3,6 +3,7 @@ import {Pressable, StyleProp, ViewStyle} from 'react-native' import {useStores} from 'state/index' import {Link} from './Link' import {isDesktopWeb} from 'platform/detection' +import {makeProfileLink} from 'lib/routes/links' interface UserPreviewLinkProps { did: string @@ -17,7 +18,7 @@ export function UserPreviewLink( if (isDesktopWeb) { return ( <Link - href={`/profile/${props.handle}`} + href={makeProfileLink(props)} title={props.handle} asAnchor style={props.style}> diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index cd6db408c..c544f6409 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -32,9 +32,10 @@ interface PostCtrlsOpts { itemTitle: string isAuthor: boolean author: { + did: string handle: string - displayName: string - avatar: string + displayName?: string | undefined + avatar?: string | undefined } text: string indexedAt: string @@ -269,7 +270,7 @@ const styles = StyleSheet.create({ margin: -5, }, ctrlIconLiked: { - color: colors.red3, + color: colors.like, }, mt1: { marginTop: 1, diff --git a/src/view/com/util/post-embeds/QuoteEmbed.tsx b/src/view/com/util/post-embeds/QuoteEmbed.tsx index 3836132d5..4995562ac 100644 --- a/src/view/com/util/post-embeds/QuoteEmbed.tsx +++ b/src/view/com/util/post-embeds/QuoteEmbed.tsx @@ -8,6 +8,7 @@ import {Text} from '../text/Text' import {usePalette} from 'lib/hooks/usePalette' import {ComposerOptsQuote} from 'state/models/ui/shell' import {PostEmbeds} from '.' +import {makeProfileLink} from 'lib/routes/links' export function QuoteEmbed({ quote, @@ -18,7 +19,7 @@ export function QuoteEmbed({ }) { const pal = usePalette('default') const itemUrip = new AtUri(quote.uri) - const itemHref = `/profile/${quote.author.handle}/post/${itemUrip.rkey}` + const itemHref = makeProfileLink(quote.author, 'post', itemUrip.rkey) const itemTitle = `Post by ${quote.author.handle}` const isEmpty = React.useMemo( () => quote.text.trim().length === 0, @@ -39,9 +40,8 @@ export function QuoteEmbed({ href={itemHref} title={itemTitle}> <PostMeta - authorAvatar={quote.author.avatar} - authorHandle={quote.author.handle} - authorDisplayName={quote.author.displayName} + author={quote.author} + showAvatar authorHasWarning={false} postHref={itemHref} timestamp={quote.indexedAt} diff --git a/src/view/com/util/text/ThemedText.tsx b/src/view/com/util/text/ThemedText.tsx new file mode 100644 index 000000000..2844d273c --- /dev/null +++ b/src/view/com/util/text/ThemedText.tsx @@ -0,0 +1,80 @@ +import React from 'react' +import {CustomTextProps, Text} from './Text' +import {usePalette} from 'lib/hooks/usePalette' +import {addStyle} from 'lib/styles' + +export type ThemedTextProps = CustomTextProps & { + fg?: 'default' | 'light' | 'error' | 'inverted' | 'inverted-light' + bg?: 'default' | 'light' | 'error' | 'inverted' | 'inverted-light' + border?: 'default' | 'dark' | 'error' | 'inverted' | 'inverted-dark' + lineHeight?: number +} + +export function ThemedText({ + fg, + bg, + border, + style, + children, + ...props +}: React.PropsWithChildren<ThemedTextProps>) { + const pal = usePalette('default') + const palInverted = usePalette('inverted') + const palError = usePalette('error') + switch (fg) { + case 'default': + style = addStyle(style, pal.text) + break + case 'light': + style = addStyle(style, pal.textLight) + break + case 'error': + style = addStyle(style, {color: palError.colors.background}) + break + case 'inverted': + style = addStyle(style, palInverted.text) + break + case 'inverted-light': + style = addStyle(style, palInverted.textLight) + break + } + switch (bg) { + case 'default': + style = addStyle(style, pal.view) + break + case 'light': + style = addStyle(style, pal.viewLight) + break + case 'error': + style = addStyle(style, palError.view) + break + case 'inverted': + style = addStyle(style, palInverted.view) + break + case 'inverted-light': + style = addStyle(style, palInverted.viewLight) + break + } + switch (border) { + case 'default': + style = addStyle(style, pal.border) + break + case 'dark': + style = addStyle(style, pal.borderDark) + break + case 'error': + style = addStyle(style, palError.border) + break + case 'inverted': + style = addStyle(style, palInverted.border) + break + case 'inverted-dark': + style = addStyle(style, palInverted.borderDark) + break + } + return ( + <Text style={style} {...props}> + {children} + </Text> + ) +} diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx index c0dcd7980..61550c683 100644 --- a/src/view/screens/CustomFeed.tsx +++ b/src/view/screens/CustomFeed.tsx @@ -14,6 +14,7 @@ import {useCustomFeed} from 'lib/hooks/useCustomFeed' import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {Feed} from 'view/com/posts/Feed' import {pluralize} from 'lib/strings/helpers' +import {sanitizeHandle} from 'lib/strings/handles' import {TextLink} from 'view/com/util/Link' import {UserAvatar} from 'view/com/util/UserAvatar' import {ViewHeader} from 'view/com/util/ViewHeader' @@ -32,6 +33,7 @@ import {DropdownButton, DropdownItem} from 'view/com/util/forms/DropdownButton' import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' import {EmptyState} from 'view/com/util/EmptyState' import {useAnalytics} from 'lib/analytics/analytics' +import {makeProfileLink} from 'lib/routes/links' type Props = NativeStackScreenProps<CommonNavigatorParams, 'CustomFeed'> export const CustomFeedScreen = withAuthRequired( @@ -216,8 +218,11 @@ export const CustomFeedScreen = withAuthRequired( 'you' ) : ( <TextLink - text={`@${currentFeed.data.creator.handle}`} - href={`/profile/${currentFeed.data.creator.did}`} + text={sanitizeHandle( + currentFeed.data.creator.handle, + '@', + )} + href={makeProfileLink(currentFeed.data.creator)} style={[pal.textLight]} /> )} diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx index 7356db54b..dd456c35e 100644 --- a/src/view/screens/Settings.tsx +++ b/src/view/screens/Settings.tsx @@ -43,6 +43,7 @@ import {pluralize} from 'lib/strings/helpers' import {formatCount} from 'view/com/util/numeric/format' import Clipboard from '@react-native-clipboard/clipboard' import {reset as resetNavigation} from '../../Navigation' +import {makeProfileLink} from 'lib/routes/links' // TEMPORARY (APP-700) // remove after backend testing finishes @@ -229,7 +230,7 @@ export const SettingsScreen = withAuthRequired( </View> ) : ( <Link - href={`/profile/${store.me.handle}`} + href={makeProfileLink(store.me)} title="Your profile" noFeedback> <View style={[pal.view, styles.linkCard]}> diff --git a/src/view/shell/bottom-bar/BottomBarWeb.tsx b/src/view/shell/bottom-bar/BottomBarWeb.tsx index cbaafd1fd..50cfa0570 100644 --- a/src/view/shell/bottom-bar/BottomBarWeb.tsx +++ b/src/view/shell/bottom-bar/BottomBarWeb.tsx @@ -21,6 +21,7 @@ import { } from 'lib/icons' import {Link} from 'view/com/util/Link' import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' +import {makeProfileLink} from 'lib/routes/links' export const BottomBarWeb = observer(() => { const store = useStores() @@ -87,7 +88,7 @@ export const BottomBarWeb = observer(() => { ) }} </NavItem> - <NavItem routeName="Profile" href={`/profile/${store.me.handle}`}> + <NavItem routeName="Profile" href={makeProfileLink(store.me)}> {() => ( <UserIcon size={28} diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx index 99fe9fc76..700857bbe 100644 --- a/src/view/shell/desktop/LeftNav.tsx +++ b/src/view/shell/desktop/LeftNav.tsx @@ -36,14 +36,12 @@ import { import {getCurrentRoute, isTab, isStateAtTabRoot} from 'lib/routes/helpers' import {NavigationProp, CommonNavigatorParams} from 'lib/routes/types' import {router} from '../../../routes' +import {makeProfileLink} from 'lib/routes/links' const ProfileCard = observer(() => { const store = useStores() return ( - <Link - href={`/profile/${store.me.handle}`} - style={styles.profileCard} - asAnchor> + <Link href={makeProfileLink(store.me)} style={styles.profileCard} asAnchor> <UserAvatar avatar={store.me.avatar} size={64} /> </Link> ) @@ -252,7 +250,7 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() { /> {store.session.hasSession && ( <NavItem - href={`/profile/${store.me.handle}`} + href={makeProfileLink(store.me)} icon={<UserIcon strokeWidth={1.75} size={28} style={pal.text} />} iconFilled={ <UserIconSolid strokeWidth={1.75} size={28} style={pal.text} /> |