From 49356700c31a1cb34c252e3aecf18561114916b9 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 27 Jul 2023 10:50:12 -0500 Subject: [PATCH] [APP-782] Support invalid handles correctly (#1049) * Update profile link construction to support handle.invalid * Update list links to support using handles * Use did for isMe check to ensure invalid handles dont distort the check * Shift the red (error) colors away from the pink spectrum * Add ThemedText helper component * Add sanitizedHandle() helper to render invalid handles well * Fix regression: only show avatar in PostMeta when needed * Restore the color of likes * Remove users with invalid handles from default autosuggests --- src/lib/link-meta/bsky.ts | 6 +- src/lib/routes/links.ts | 15 ++++ src/lib/strings/handles.ts | 8 ++ src/lib/styles.ts | 16 ++-- src/state/models/content/list.ts | 23 +++++- .../models/discovery/user-autocomplete.ts | 3 +- src/state/models/feeds/custom-feed.ts | 3 +- src/state/models/feeds/multi-feed.ts | 3 +- src/state/models/ui/shell.ts | 1 + src/view/com/composer/Composer.tsx | 4 +- src/view/com/feeds/CustomFeed.tsx | 3 +- src/view/com/lists/ListCard.tsx | 6 +- src/view/com/lists/ListItems.tsx | 6 +- src/view/com/modals/ListAddRemoveUser.tsx | 3 +- src/view/com/notifications/FeedItem.tsx | 12 +-- src/view/com/notifications/InvitedUsers.tsx | 5 +- src/view/com/post-thread/PostThreadItem.tsx | 36 ++++----- src/view/com/post/Post.tsx | 12 +-- src/view/com/posts/FeedItem.tsx | 22 +++-- src/view/com/posts/FeedSlice.tsx | 5 +- src/view/com/profile/ProfileCard.tsx | 12 ++- src/view/com/profile/ProfileHeader.tsx | 43 ++++++++-- src/view/com/search/Suggestions.tsx | 3 +- src/view/com/util/PostMeta.tsx | 31 ++++--- src/view/com/util/UserInfoText.tsx | 6 +- src/view/com/util/UserPreviewLink.tsx | 3 +- src/view/com/util/post-ctrls/PostCtrls.tsx | 7 +- src/view/com/util/post-embeds/QuoteEmbed.tsx | 8 +- src/view/com/util/text/ThemedText.tsx | 80 +++++++++++++++++++ src/view/screens/CustomFeed.tsx | 9 ++- src/view/screens/Settings.tsx | 3 +- src/view/shell/bottom-bar/BottomBarWeb.tsx | 3 +- src/view/shell/desktop/LeftNav.tsx | 8 +- 33 files changed, 291 insertions(+), 117 deletions(-) create mode 100644 src/lib/routes/links.ts create mode 100644 src/view/com/util/text/ThemedText.tsx diff --git a/src/lib/link-meta/bsky.ts b/src/lib/link-meta/bsky.ts index aed10389..b052ed04 100644 --- a/src/lib/link-meta/bsky.ts +++ b/src/lib/link-meta/bsky.ts @@ -122,11 +122,7 @@ export async function getPostAsQuote( cid: threadView.thread.post.cid, text: threadView.thread.postRecord?.text || '', indexedAt: threadView.thread.post.indexedAt, - author: { - handle: threadView.thread.post.author.handle, - displayName: threadView.thread.post.author.displayName, - avatar: threadView.thread.post.author.avatar, - }, + author: threadView.thread.post.author, } } diff --git a/src/lib/routes/links.ts b/src/lib/routes/links.ts new file mode 100644 index 00000000..cc543b6b --- /dev/null +++ b/src/lib/routes/links.ts @@ -0,0 +1,15 @@ +import {isInvalidHandle} from 'lib/strings/handles' + +export function makeProfileLink( + info: { + did: string + handle: string + }, + ...segments: string[] +) { + return [ + `/profile`, + `${isInvalidHandle(info.handle) ? info.did : info.handle}`, + ...segments, + ].join('/') +} diff --git a/src/lib/strings/handles.ts b/src/lib/strings/handles.ts index 3409a031..3c01d934 100644 --- a/src/lib/strings/handles.ts +++ b/src/lib/strings/handles.ts @@ -11,3 +11,11 @@ export function createFullHandle(name: string, domain: string): string { domain = (domain || '').replace(/^[.]+/, '') return `${name}.${domain}` } + +export function isInvalidHandle(handle: string): boolean { + return handle === 'handle.invalid' +} + +export function sanitizeHandle(handle: string, prefix = ''): string { + return isInvalidHandle(handle) ? '⚠Invalid Handle' : `${prefix}${handle}` +} diff --git a/src/lib/styles.ts b/src/lib/styles.ts index c5a710ff..8ee6e596 100644 --- a/src/lib/styles.ts +++ b/src/lib/styles.ts @@ -25,13 +25,13 @@ export const colors = { blue6: '#012561', blue7: '#001040', - red1: '#ffe6f2', - red2: '#fba2ce', - red3: '#ec4899', - red4: '#d1106f', - red5: '#97074e', - red6: '#690436', - red7: '#4F0328', + red1: '#ffe6eb', + red2: '#fba2b2', + red3: '#ec4868', + red4: '#d11043', + red5: '#970721', + red6: '#690419', + red7: '#4F0314', pink1: '#f8ccff', pink2: '#e966ff', @@ -53,6 +53,7 @@ export const colors = { unreadNotifBg: '#ebf6ff', brandBlue: '#0066FF', + like: '#ec4899', } export const gradients = { @@ -224,6 +225,7 @@ export const s = StyleSheet.create({ green5: {color: colors.green5}, brandBlue: {color: colors.brandBlue}, + likeColor: {color: colors.like}, }) export function lh( diff --git a/src/state/models/content/list.ts b/src/state/models/content/list.ts index d5c9e649..c5ac72e4 100644 --- a/src/state/models/content/list.ts +++ b/src/state/models/content/list.ts @@ -1,4 +1,4 @@ -import {makeAutoObservable} from 'mobx' +import {makeAutoObservable, runInAction} from 'mobx' import { AtUri, AppBskyGraphGetList as GetList, @@ -115,6 +115,7 @@ export class ListModel { } this._xLoading(replace) try { + await this._resolveUri() const res = await this.rootStore.agent.app.bsky.graph.getList({ list: this.uri, limit: PAGE_SIZE, @@ -146,6 +147,7 @@ export class ListModel { if (!this.isOwner) { throw new Error('Cannot edit this list') } + await this._resolveUri() // get the current record const {rkey} = new AtUri(this.uri) @@ -179,6 +181,7 @@ export class ListModel { if (!this.list) { return } + await this._resolveUri() // fetch all the listitem records that belong to this list let cursor @@ -220,6 +223,7 @@ export class ListModel { if (!this.list) { return } + await this._resolveUri() await this.rootStore.agent.app.bsky.graph.muteActorList({ list: this.list.uri, }) @@ -231,6 +235,7 @@ export class ListModel { if (!this.list) { return } + await this._resolveUri() await this.rootStore.agent.app.bsky.graph.unmuteActorList({ list: this.list.uri, }) @@ -273,6 +278,22 @@ export class ListModel { // helper functions // = + async _resolveUri() { + const urip = new AtUri(this.uri) + if (!urip.host.startsWith('did:')) { + try { + urip.host = await apilib.resolveName(this.rootStore, urip.host) + } catch (e: any) { + runInAction(() => { + this.error = e.toString() + }) + } + } + runInAction(() => { + this.uri = urip.toString() + }) + } + _replaceAll(res: GetList.Response) { this.items = [] this._appendAll(res) diff --git a/src/state/models/discovery/user-autocomplete.ts b/src/state/models/discovery/user-autocomplete.ts index 601e10ea..461073e4 100644 --- a/src/state/models/discovery/user-autocomplete.ts +++ b/src/state/models/discovery/user-autocomplete.ts @@ -2,6 +2,7 @@ import {makeAutoObservable, runInAction} from 'mobx' import {AppBskyActorDefs} from '@atproto/api' import AwaitLock from 'await-lock' import {RootStoreModel} from '../root-store' +import {isInvalidHandle} from 'lib/strings/handles' export class UserAutocompleteModel { // state @@ -81,7 +82,7 @@ export class UserAutocompleteModel { actor: this.rootStore.me.did || '', }) runInAction(() => { - this.follows = res.data.follows + this.follows = res.data.follows.filter(f => !isInvalidHandle(f.handle)) for (const f of this.follows) { this.knownHandles.add(f.handle) } diff --git a/src/state/models/feeds/custom-feed.ts b/src/state/models/feeds/custom-feed.ts index 1303952e..3c6d5275 100644 --- a/src/state/models/feeds/custom-feed.ts +++ b/src/state/models/feeds/custom-feed.ts @@ -2,6 +2,7 @@ import {AppBskyFeedDefs} from '@atproto/api' import {makeAutoObservable, runInAction} from 'mobx' import {RootStoreModel} from 'state/models/root-store' import {sanitizeDisplayName} from 'lib/strings/display-names' +import {sanitizeHandle} from 'lib/strings/handles' import {updateDataOptimistically} from 'lib/async/revertible' import {track} from 'lib/analytics/analytics' @@ -42,7 +43,7 @@ export class CustomFeedModel { if (this.data.displayName) { return sanitizeDisplayName(this.data.displayName) } - return `Feed by @${this.data.creator.handle}` + return `Feed by ${sanitizeHandle(this.data.creator.handle, '@')}` } get isSaved() { diff --git a/src/state/models/feeds/multi-feed.ts b/src/state/models/feeds/multi-feed.ts index 1fc57a86..fdcd208c 100644 --- a/src/state/models/feeds/multi-feed.ts +++ b/src/state/models/feeds/multi-feed.ts @@ -5,6 +5,7 @@ import {RootStoreModel} from '../root-store' import {CustomFeedModel} from './custom-feed' import {PostsFeedModel} from './posts' import {PostsFeedSliceModel} from './posts-slice' +import {makeProfileLink} from 'lib/routes/links' const FEED_PAGE_SIZE = 10 const FEEDS_PAGE_SIZE = 3 @@ -107,7 +108,7 @@ export class PostsMultiFeedModel { _reactKey: `__feed_footer_${i}__`, type: 'feed-footer', title: feedInfo.displayName, - uri: `/profile/${feedInfo.data.creator.did}/feed/${urip.rkey}`, + uri: makeProfileLink(feedInfo.data.creator, 'feed', urip.rkey), }) } if (!this.hasMore) { diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts index 17740a77..e33a34ac 100644 --- a/src/state/models/ui/shell.ts +++ b/src/state/models/ui/shell.ts @@ -208,6 +208,7 @@ export interface ComposerOptsQuote { text: string indexedAt: string author: { + did: string handle: string displayName?: string avatar?: string diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 66722ab2..0fae996f 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({ {sanitizeDisplayName( - replyTo.author.displayName || replyTo.author.handle, + replyTo.author.displayName || + sanitizeHandle(replyTo.author.handle), )} diff --git a/src/view/com/feeds/CustomFeed.tsx b/src/view/com/feeds/CustomFeed.tsx index ef8de8b8..79f1dd74 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} - by @{item.data.creator.handle} + by {sanitizeHandle(item.data.creator.handle, '@')} {showSaveBtn && ( diff --git a/src/view/com/lists/ListCard.tsx b/src/view/com/lists/ListCard.tsx index b70fa377..159d966e 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, '@')} {!!list.viewer?.muted && ( diff --git a/src/view/com/lists/ListItems.tsx b/src/view/com/lists/ListItems.tsx index 289ba000..188518ea 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' ) : ( )} diff --git a/src/view/com/modals/ListAddRemoveUser.tsx b/src/view/com/modals/ListAddRemoveUser.tsx index c2d63ef6..49f46e74 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, '@')} { 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)}   - {author.handle} + {sanitizeHandle(author.handle)} diff --git a/src/view/com/notifications/InvitedUsers.tsx b/src/view/com/notifications/InvitedUsers.tsx index 73469d2a..1bdb42a9 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({ /> - + {' '} joined using your invite code! diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index efc9fe69..0680bbc0 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), )} @@ -223,7 +226,7 @@ export const PostThreadItem = observer(function PostThreadItem({ href={authorHref} title={authorTitle}> - @{item.post.author.handle} + {sanitizeHandle(item.post.author.handle, '@')} @@ -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({ { 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 && ( @@ -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)} /> @@ -221,8 +224,7 @@ export const FeedItem = observer(function ({ { 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 ( diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx index 2dfc7ad3..946e0f2a 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), + )} - @{profile.handle} + {sanitizeHandle(profile.handle, '@')} {!!profile.viewer?.followedBy && ( @@ -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 ( - {sanitizeDisplayName(view.displayName || view.handle)} + {sanitizeDisplayName( + view.displayName || sanitizeHandle(view.handle), + )} @@ -104,6 +110,7 @@ const ProfileHeaderLoaded = observer( const store = useStores() const navigation = useNavigation() 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`}> + accessibilityHint={`Shows posts from ${view.handle} in your feed`}> - {sanitizeDisplayName(view.displayName || view.handle)} + {sanitizeDisplayName( + view.displayName || sanitizeHandle(view.handle), + )} @@ -393,7 +406,16 @@ const ProfileHeaderLoaded = observer( ) : undefined} - @{view.handle} + + {invalidHandle ? '⚠Invalid Handle' : `@${view.handle}`} + {!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 c8941e24..440d912a 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 5df6b398..2ce49976 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 ( - {typeof opts.authorAvatar !== 'undefined' && ( + {opts.showAvatar && typeof opts.author.avatar !== 'undefined' && ( @@ -43,17 +49,17 @@ export const PostMeta = observer(function (opts: PostMetaOpts) { lineHeight={1.2} text={ <> - {sanitizeDisplayName(displayName)} + {sanitizeDisplayName(displayName)}  -  @{handle} + lineHeight={1.2} + style={pal.textLight}> + {sanitizeHandle(handle, '@')} } - href={`/profile/${opts.authorHandle}`} + href={makeProfileLink(opts.author)} /> {!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 b737b2b1..695711b2 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 ae49301f..7eedbc2d 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 ( diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index cd6db408..c544f640 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 3836132d..4995562a 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}> ) { + 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 ( + + {children} + + ) +} diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx index c0dcd798..61550c68 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 export const CustomFeedScreen = withAuthRequired( @@ -216,8 +218,11 @@ export const CustomFeedScreen = withAuthRequired( 'you' ) : ( )} diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx index 7356db54..dd456c35 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( ) : ( diff --git a/src/view/shell/bottom-bar/BottomBarWeb.tsx b/src/view/shell/bottom-bar/BottomBarWeb.tsx index cbaafd1f..50cfa057 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(() => { ) }} - + {() => ( { const store = useStores() return ( - + ) @@ -252,7 +250,7 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() { /> {store.session.hasSession && ( } iconFilled={