Lex refactor (#362)

* Remove the hackcheck for upgrades

* Rename the PostEmbeds folder to match the codebase style

* Updates to latest lex refactor

* Update to use new bsky agent

* Update to use api package's richtext library

* Switch to upsertProfile

* Add TextEncoder/TextDecoder polyfill

* Add Intl.Segmenter polyfill

* Update composer to calculate lengths by grapheme

* Fix detox

* Fix login in e2e

* Create account e2e passing

* Implement an e2e mocking framework

* Don't use private methods on mobx models as mobx can't track them

* Add tooling for e2e-specific builds and add e2e media-picker mock

* Add some tests and fix some bugs around profile editing

* Add shell tests

* Add home screen tests

* Add thread screen tests

* Add tests for other user profile screens

* Add search screen tests

* Implement profile imagery change tools and tests

* Update to new embed behaviors

* Add post tests

* Fix to profile-screen test

* Fix session resumption

* Update web composer to new api

* 1.11.0

* Fix pagination cursor parameters

* Add quote posts to notifications

* Fix embed layouts

* Remove youtube inline player and improve tap handling on link cards

* Reset minimal shell mode on all screen loads and feed swipes (close #299)

* Update podfile.lock

* Improve post notfound UI (close #366)

* Bump atproto packages
This commit is contained in:
Paul Frazee 2023-03-31 13:17:26 -05:00 committed by GitHub
parent 19f3a2fa92
commit a3334a01a2
133 changed files with 3103 additions and 2839 deletions

View file

@ -2,19 +2,16 @@ import React from 'react'
import {observer} from 'mobx-react-lite'
import {Button, ButtonType} from '../util/forms/Button'
import {useStores} from 'state/index'
import * as apilib from 'lib/api/index'
import * as Toast from '../util/Toast'
const FollowButton = observer(
({
type = 'inverted',
did,
declarationCid,
onToggleFollow,
}: {
type?: ButtonType
did: string
declarationCid: string
onToggleFollow?: (v: boolean) => void
}) => {
const store = useStores()
@ -23,7 +20,7 @@ const FollowButton = observer(
const onToggleFollowInner = async () => {
if (store.me.follows.isFollowing(did)) {
try {
await apilib.unfollow(store, store.me.follows.getFollowUri(did))
await store.agent.deleteFollow(store.me.follows.getFollowUri(did))
store.me.follows.removeFollow(did)
onToggleFollow?.(false)
} catch (e: any) {
@ -32,7 +29,7 @@ const FollowButton = observer(
}
} else {
try {
const res = await apilib.follow(store, did, declarationCid)
const res = await store.agent.follow(did)
store.me.follows.addFollow(did, res.uri)
onToggleFollow?.(true)
} catch (e: any) {

View file

@ -1,7 +1,7 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import {observer} from 'mobx-react-lite'
import {AppBskyActorProfile} from '@atproto/api'
import {AppBskyActorDefs} from '@atproto/api'
import {Link} from '../util/Link'
import {Text} from '../util/text/Text'
import {UserAvatar} from '../util/UserAvatar'
@ -11,6 +11,7 @@ import {useStores} from 'state/index'
import FollowButton from './FollowButton'
export function ProfileCard({
testID,
handle,
displayName,
avatar,
@ -21,6 +22,7 @@ export function ProfileCard({
followers,
renderButton,
}: {
testID?: string
handle: string
displayName?: string
avatar?: string
@ -28,12 +30,13 @@ export function ProfileCard({
isFollowedBy?: boolean
noBg?: boolean
noBorder?: boolean
followers?: AppBskyActorProfile.View[] | undefined
followers?: AppBskyActorDefs.ProfileView[] | undefined
renderButton?: () => JSX.Element
}) {
const pal = usePalette('default')
return (
<Link
testID={testID}
style={[
styles.outer,
pal.border,
@ -106,7 +109,6 @@ export function ProfileCard({
export const ProfileCardWithFollowBtn = observer(
({
did,
declarationCid,
handle,
displayName,
avatar,
@ -117,7 +119,6 @@ export const ProfileCardWithFollowBtn = observer(
followers,
}: {
did: string
declarationCid: string
handle: string
displayName?: string
avatar?: string
@ -125,7 +126,7 @@ export const ProfileCardWithFollowBtn = observer(
isFollowedBy?: boolean
noBg?: boolean
noBorder?: boolean
followers?: AppBskyActorProfile.View[] | undefined
followers?: AppBskyActorDefs.ProfileView[] | undefined
}) => {
const store = useStores()
const isMe = store.me.handle === handle
@ -140,11 +141,7 @@ export const ProfileCardWithFollowBtn = observer(
noBg={noBg}
noBorder={noBorder}
followers={followers}
renderButton={
isMe
? undefined
: () => <FollowButton did={did} declarationCid={declarationCid} />
}
renderButton={isMe ? undefined : () => <FollowButton did={did} />}
/>
)
},

View file

@ -19,7 +19,7 @@ export const ProfileFollowers = observer(function ProfileFollowers({
const pal = usePalette('default')
const store = useStores()
const view = React.useMemo(
() => new UserFollowersViewModel(store, {user: name}),
() => new UserFollowersViewModel(store, {actor: name}),
[store, name],
)
@ -64,7 +64,6 @@ export const ProfileFollowers = observer(function ProfileFollowers({
<ProfileCardWithFollowBtn
key={item.did}
did={item.did}
declarationCid={item.declaration.cid}
handle={item.handle}
displayName={item.displayName}
avatar={item.avatar}

View file

@ -16,7 +16,7 @@ export const ProfileFollows = observer(function ProfileFollows({
const pal = usePalette('default')
const store = useStores()
const view = React.useMemo(
() => new UserFollowsViewModel(store, {user: name}),
() => new UserFollowsViewModel(store, {actor: name}),
[store, name],
)
@ -61,7 +61,6 @@ export const ProfileFollows = observer(function ProfileFollows({
<ProfileCardWithFollowBtn
key={item.did}
did={item.did}
declarationCid={item.declaration.cid}
handle={item.handle}
displayName={item.displayName}
avatar={item.avatar}

View file

@ -33,7 +33,61 @@ import {isDesktopWeb} from 'platform/detection'
const BACK_HITSLOP = {left: 30, top: 30, right: 30, bottom: 30}
export const ProfileHeader = observer(function ProfileHeader({
export const ProfileHeader = observer(
({
view,
onRefreshAll,
}: {
view: ProfileViewModel
onRefreshAll: () => void
}) => {
const pal = usePalette('default')
// loading
// =
if (!view || !view.hasLoaded) {
return (
<View style={pal.view}>
<LoadingPlaceholder width="100%" height={120} />
<View
style={[
pal.view,
{borderColor: pal.colors.background},
styles.avi,
]}>
<LoadingPlaceholder width={80} height={80} style={styles.br40} />
</View>
<View style={styles.content}>
<View style={[styles.buttonsLine]}>
<LoadingPlaceholder width={100} height={31} style={styles.br50} />
</View>
<View style={styles.displayNameLine}>
<Text type="title-2xl" style={[pal.text, styles.title]}>
{view.displayName || view.handle}
</Text>
</View>
</View>
</View>
)
}
// error
// =
if (view.hasError) {
return (
<View testID="profileHeaderHasError">
<Text>{view.error}</Text>
</View>
)
}
// loaded
// =
return <ProfileHeaderLoaded view={view} onRefreshAll={onRefreshAll} />
},
)
const ProfileHeaderLoaded = observer(function ProfileHeaderLoaded({
view,
onRefreshAll,
}: {
@ -44,14 +98,17 @@ export const ProfileHeader = observer(function ProfileHeader({
const store = useStores()
const navigation = useNavigation<NavigationProp>()
const {track} = useAnalytics()
const onPressBack = React.useCallback(() => {
navigation.goBack()
}, [navigation])
const onPressAvi = React.useCallback(() => {
if (view.avatar) {
store.shell.openLightbox(new ProfileImageLightbox(view))
}
}, [store, view])
const onPressToggleFollow = React.useCallback(() => {
view?.toggleFollowing().then(
() => {
@ -64,6 +121,7 @@ export const ProfileHeader = observer(function ProfileHeader({
err => store.log.error('Failed to toggle follow', err),
)
}, [view, store])
const onPressEditProfile = React.useCallback(() => {
track('ProfileHeader:EditProfileButtonClicked')
store.shell.openModal({
@ -72,18 +130,22 @@ export const ProfileHeader = observer(function ProfileHeader({
onUpdate: onRefreshAll,
})
}, [track, store, view, onRefreshAll])
const onPressFollowers = React.useCallback(() => {
track('ProfileHeader:FollowersButtonClicked')
navigation.push('ProfileFollowers', {name: view.handle})
}, [track, navigation, view])
const onPressFollows = React.useCallback(() => {
track('ProfileHeader:FollowsButtonClicked')
navigation.push('ProfileFollows', {name: view.handle})
}, [track, navigation, view])
const onPressShare = React.useCallback(() => {
track('ProfileHeader:ShareButtonClicked')
Share.share({url: toShareUrl(`/profile/${view.handle}`)})
}, [track, view])
const onPressMuteAccount = React.useCallback(async () => {
track('ProfileHeader:MuteAccountButtonClicked')
try {
@ -94,6 +156,7 @@ export const ProfileHeader = observer(function ProfileHeader({
Toast.show(`There was an issue! ${e.toString()}`)
}
}, [track, view, store])
const onPressUnmuteAccount = React.useCallback(async () => {
track('ProfileHeader:UnmuteAccountButtonClicked')
try {
@ -104,6 +167,7 @@ export const ProfileHeader = observer(function ProfileHeader({
Toast.show(`There was an issue! ${e.toString()}`)
}
}, [track, view, store])
const onPressReportAccount = React.useCallback(() => {
track('ProfileHeader:ReportAccountButtonClicked')
store.shell.openModal({
@ -112,54 +176,39 @@ export const ProfileHeader = observer(function ProfileHeader({
})
}, [track, store, view])
// loading
// =
if (!view || !view.hasLoaded) {
return (
<View style={pal.view}>
<LoadingPlaceholder width="100%" height={120} />
<View
style={[pal.view, {borderColor: pal.colors.background}, styles.avi]}>
<LoadingPlaceholder width={80} height={80} style={styles.br40} />
</View>
<View style={styles.content}>
<View style={[styles.buttonsLine]}>
<LoadingPlaceholder width={100} height={31} style={styles.br50} />
</View>
<View style={styles.displayNameLine}>
<Text type="title-2xl" style={[pal.text, styles.title]}>
{view.displayName || view.handle}
</Text>
</View>
</View>
</View>
)
}
// error
// =
if (view.hasError) {
return (
<View testID="profileHeaderHasError">
<Text>{view.error}</Text>
</View>
)
}
// loaded
// =
const isMe = store.me.did === view.did
let dropdownItems: DropdownItem[] = [{label: 'Share', onPress: onPressShare}]
if (!isMe) {
dropdownItems.push({
label: view.viewer.muted ? 'Unmute Account' : 'Mute Account',
onPress: view.viewer.muted ? onPressUnmuteAccount : onPressMuteAccount,
})
dropdownItems.push({
label: 'Report Account',
onPress: onPressReportAccount,
})
}
const isMe = React.useMemo(
() => store.me.did === view.did,
[store.me.did, view.did],
)
const dropdownItems: DropdownItem[] = React.useMemo(() => {
let items: DropdownItem[] = [
{
testID: 'profileHeaderDropdownSahreBtn',
label: 'Share',
onPress: onPressShare,
},
]
if (!isMe) {
items.push({
testID: 'profileHeaderDropdownMuteBtn',
label: view.viewer.muted ? 'Unmute Account' : 'Mute Account',
onPress: view.viewer.muted ? onPressUnmuteAccount : onPressMuteAccount,
})
items.push({
testID: 'profileHeaderDropdownReportBtn',
label: 'Report Account',
onPress: onPressReportAccount,
})
}
return items
}, [
isMe,
view.viewer.muted,
onPressShare,
onPressUnmuteAccount,
onPressMuteAccount,
onPressReportAccount,
])
return (
<View style={pal.view}>
<UserBanner banner={view.banner} />
@ -178,6 +227,7 @@ export const ProfileHeader = observer(function ProfileHeader({
<>
{store.me.follows.isFollowing(view.did) ? (
<TouchableOpacity
testID="unfollowBtn"
onPress={onPressToggleFollow}
style={[styles.btn, styles.mainBtn, pal.btn]}>
<FontAwesomeIcon
@ -191,7 +241,7 @@ export const ProfileHeader = observer(function ProfileHeader({
</TouchableOpacity>
) : (
<TouchableOpacity
testID="profileHeaderToggleFollowButton"
testID="followBtn"
onPress={onPressToggleFollow}
style={[styles.btn, styles.primaryBtn]}>
<FontAwesomeIcon
@ -207,6 +257,7 @@ export const ProfileHeader = observer(function ProfileHeader({
)}
{dropdownItems?.length ? (
<DropdownButton
testID="profileHeaderDropdownBtn"
type="bare"
items={dropdownItems}
style={[styles.btn, styles.secondaryBtn, pal.btn]}>
@ -215,7 +266,10 @@ export const ProfileHeader = observer(function ProfileHeader({
) : undefined}
</View>
<View style={styles.displayNameLine}>
<Text type="title-2xl" style={[pal.text, styles.title]}>
<Text
testID="profileHeaderDisplayName"
type="title-2xl"
style={[pal.text, styles.title]}>
{view.displayName || view.handle}
</Text>
</View>
@ -241,19 +295,17 @@ export const ProfileHeader = observer(function ProfileHeader({
{pluralize(view.followersCount, 'follower')}
</Text>
</TouchableOpacity>
{view.isUser ? (
<TouchableOpacity
testID="profileHeaderFollowsButton"
style={[s.flexRow, s.mr10]}
onPress={onPressFollows}>
<Text type="md" style={[s.bold, s.mr2, pal.text]}>
{view.followsCount}
</Text>
<Text type="md" style={[pal.textLight]}>
following
</Text>
</TouchableOpacity>
) : undefined}
<TouchableOpacity
testID="profileHeaderFollowsButton"
style={[s.flexRow, s.mr10]}
onPress={onPressFollows}>
<Text type="md" style={[s.bold, s.mr2, pal.text]}>
{view.followsCount}
</Text>
<Text type="md" style={[pal.textLight]}>
following
</Text>
</TouchableOpacity>
<View style={[s.flexRow, s.mr10]}>
<Text type="md" style={[s.bold, s.mr2, pal.text]}>
{view.postsCount}
@ -265,13 +317,16 @@ export const ProfileHeader = observer(function ProfileHeader({
</View>
{view.descriptionRichText ? (
<RichText
testID="profileHeaderDescription"
style={[styles.description, pal.text]}
numberOfLines={15}
richText={view.descriptionRichText}
/>
) : undefined}
{view.viewer.muted ? (
<View style={[styles.detailLine, pal.btn, s.p5]}>
<View
testID="profileHeaderMutedNotice"
style={[styles.detailLine, pal.btn, s.p5]}>
<FontAwesomeIcon
icon={['far', 'eye-slash']}
style={[pal.text, s.mr5]}