Add "follows you" information and sync follow state between views (#215)
* Bump @atproto/api@0.1.2 and update API usage * Add 'follows you' pill to profile header (close #110) * Add 'follows you' to followers and follows (close #103) * Update reposted-by and liked-by views to use the same components as followers and following * Create a local follows cache MyFollowsModel to keep views in sync (close #205) * Add incremental hydration to the MyFollows model * Fix tests * Update deps * Fix lint * Fix to paginated fetches * Fix reference * Fix potential state-desync issue
This commit is contained in:
@@ -6,6 +6,7 @@ import {SessionModel} from '../src/state/models/session'
|
||||
import {NavigationModel} from '../src/state/models/navigation'
|
||||
import {ShellUiModel} from '../src/state/models/shell-ui'
|
||||
import {MeModel} from '../src/state/models/me'
|
||||
import {MyFollowsModel} from '../src/state/models/my-follows'
|
||||
import {OnboardModel} from '../src/state/models/onboard'
|
||||
import {ProfilesViewModel} from '../src/state/models/profiles-view'
|
||||
import {LinkMetasViewModel} from '../src/state/models/link-metas-view'
|
||||
@@ -53,9 +54,8 @@ export const mockedProfileStore = {
|
||||
followsCount: 0,
|
||||
membersCount: 0,
|
||||
postsCount: 0,
|
||||
myState: {
|
||||
follow: '',
|
||||
member: '',
|
||||
viewer: {
|
||||
following: '',
|
||||
},
|
||||
rootStore: {} as RootStoreModel,
|
||||
hasContent: true,
|
||||
@@ -572,6 +572,10 @@ export const mockedShellStore = {
|
||||
openLightbox: jest.fn(),
|
||||
} as ShellUiModel
|
||||
|
||||
export const mockedFollowsStore = {
|
||||
isFollowing: jest.fn().mockReturnValue(false),
|
||||
} as MyFollowsModel
|
||||
|
||||
export const mockedMeStore = {
|
||||
serialize: jest.fn(),
|
||||
hydrate: jest.fn(),
|
||||
@@ -585,6 +589,7 @@ export const mockedMeStore = {
|
||||
memberships: mockedMembershipsStore,
|
||||
mainFeed: mockedFeedStore,
|
||||
notifications: mockedNotificationsStore,
|
||||
follows: mockedFollowsStore,
|
||||
clear: jest.fn(),
|
||||
load: jest.fn(),
|
||||
clearNotificationCount: jest.fn(),
|
||||
|
||||
+2
-2
@@ -16,7 +16,7 @@
|
||||
"e2e": "detox test --configuration ios.sim.debug --take-screenshots all"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.1.1",
|
||||
"@atproto/api": "^0.1.2",
|
||||
"@atproto/lexicon": "^0.0.4",
|
||||
"@atproto/xrpc": "^0.0.4",
|
||||
"@bam.tech/react-native-image-resizer": "^3.0.4",
|
||||
@@ -80,7 +80,7 @@
|
||||
"zod": "^3.20.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@atproto/pds": "^0.0.2",
|
||||
"@atproto/pds": "^0.0.3",
|
||||
"@babel/core": "^7.20.0",
|
||||
"@babel/preset-env": "^7.20.0",
|
||||
"@babel/runtime": "^7.20.0",
|
||||
|
||||
@@ -3,6 +3,7 @@ import notifee from '@notifee/react-native'
|
||||
import {RootStoreModel} from './root-store'
|
||||
import {FeedModel} from './feed-view'
|
||||
import {NotificationsViewModel} from './notifications-view'
|
||||
import {MyFollowsModel} from './my-follows'
|
||||
import {isObj, hasProp} from '../lib/type-guards'
|
||||
import {displayNotificationFromModel} from '../../view/lib/notifee'
|
||||
|
||||
@@ -15,6 +16,7 @@ export class MeModel {
|
||||
notificationCount: number = 0
|
||||
mainFeed: FeedModel
|
||||
notifications: NotificationsViewModel
|
||||
follows: MyFollowsModel
|
||||
|
||||
constructor(public rootStore: RootStoreModel) {
|
||||
makeAutoObservable(
|
||||
@@ -26,6 +28,7 @@ export class MeModel {
|
||||
algorithm: 'reverse-chronological',
|
||||
})
|
||||
this.notifications = new NotificationsViewModel(this.rootStore, {})
|
||||
this.follows = new MyFollowsModel(this.rootStore)
|
||||
}
|
||||
|
||||
clear() {
|
||||
@@ -104,6 +107,9 @@ export class MeModel {
|
||||
this.notifications.setup().catch(e => {
|
||||
this.rootStore.log.error('Failed to setup notifications model', e)
|
||||
}),
|
||||
this.follows.fetch().catch(e => {
|
||||
this.rootStore.log.error('Failed to load my follows', e)
|
||||
}),
|
||||
])
|
||||
|
||||
// request notifications permission once the user has logged in
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import {makeAutoObservable, runInAction} from 'mobx'
|
||||
import {FollowRecord, AppBskyActorProfile, AppBskyActorRef} from '@atproto/api'
|
||||
import {RootStoreModel} from './root-store'
|
||||
import {bundleAsync} from '../../lib/async/bundle'
|
||||
|
||||
const CACHE_TTL = 1000 * 60 * 60 // hourly
|
||||
type FollowsListResponse = Awaited<ReturnType<FollowRecord['list']>>
|
||||
type FollowsListResponseRecord = FollowsListResponse['records'][0]
|
||||
type Profile =
|
||||
| AppBskyActorProfile.ViewBasic
|
||||
| AppBskyActorProfile.View
|
||||
| AppBskyActorRef.WithInfo
|
||||
|
||||
/**
|
||||
* This model is used to maintain a synced local cache of the user's
|
||||
* follows. It should be periodically refreshed and updated any time
|
||||
* the user makes a change to their follows.
|
||||
*/
|
||||
export class MyFollowsModel {
|
||||
// data
|
||||
followDidToRecordMap: Record<string, string> = {}
|
||||
lastSync = 0
|
||||
|
||||
constructor(public rootStore: RootStoreModel) {
|
||||
makeAutoObservable(
|
||||
this,
|
||||
{
|
||||
rootStore: false,
|
||||
},
|
||||
{autoBind: true},
|
||||
)
|
||||
}
|
||||
|
||||
// public api
|
||||
// =
|
||||
|
||||
fetchIfNeeded = bundleAsync(async () => {
|
||||
if (
|
||||
Object.keys(this.followDidToRecordMap).length === 0 ||
|
||||
Date.now() - this.lastSync > CACHE_TTL
|
||||
) {
|
||||
return await this.fetch()
|
||||
}
|
||||
})
|
||||
|
||||
fetch = bundleAsync(async () => {
|
||||
this.rootStore.log.debug('MyFollowsModel:fetch running full fetch')
|
||||
let before
|
||||
let records: FollowsListResponseRecord[] = []
|
||||
do {
|
||||
const res: FollowsListResponse =
|
||||
await this.rootStore.api.app.bsky.graph.follow.list({
|
||||
user: this.rootStore.me.did,
|
||||
before,
|
||||
})
|
||||
records = records.concat(res.records)
|
||||
before = res.cursor
|
||||
} while (typeof before !== 'undefined')
|
||||
runInAction(() => {
|
||||
this.followDidToRecordMap = {}
|
||||
for (const record of records) {
|
||||
this.followDidToRecordMap[record.value.subject.did] = record.uri
|
||||
}
|
||||
this.lastSync = Date.now()
|
||||
})
|
||||
})
|
||||
|
||||
isFollowing(did: string) {
|
||||
return !!this.followDidToRecordMap[did]
|
||||
}
|
||||
|
||||
getFollowUri(did: string): string {
|
||||
const v = this.followDidToRecordMap[did]
|
||||
if (!v) {
|
||||
throw new Error('Not a followed user')
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
addFollow(did: string, recordUri: string) {
|
||||
this.followDidToRecordMap[did] = recordUri
|
||||
}
|
||||
|
||||
removeFollow(did: string) {
|
||||
delete this.followDidToRecordMap[did]
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this to incrementally update the cache as views provide information
|
||||
*/
|
||||
hydrate(did: string, recordUri: string | undefined) {
|
||||
if (recordUri) {
|
||||
this.followDidToRecordMap[did] = recordUri
|
||||
} else {
|
||||
delete this.followDidToRecordMap[did]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this to incrementally update the cache as views provide information
|
||||
*/
|
||||
hydrateProfiles(profiles: Profile[]) {
|
||||
for (const profile of profiles) {
|
||||
if (profile.viewer) {
|
||||
this.hydrate(profile.did, profile.viewer.following)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,9 +15,10 @@ import {cleanError} from '../../lib/strings'
|
||||
|
||||
export const ACTOR_TYPE_USER = 'app.bsky.system.actorUser'
|
||||
|
||||
export class ProfileViewMyStateModel {
|
||||
follow?: string
|
||||
export class ProfileViewViewerModel {
|
||||
muted?: boolean
|
||||
following?: string
|
||||
followedBy?: string
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this)
|
||||
@@ -47,7 +48,7 @@ export class ProfileViewModel {
|
||||
followersCount: number = 0
|
||||
followsCount: number = 0
|
||||
postsCount: number = 0
|
||||
myState = new ProfileViewMyStateModel()
|
||||
viewer = new ProfileViewViewerModel()
|
||||
|
||||
// added data
|
||||
descriptionEntities?: Entity[]
|
||||
@@ -98,11 +99,24 @@ export class ProfileViewModel {
|
||||
if (!this.rootStore.me.did) {
|
||||
throw new Error('Not logged in')
|
||||
}
|
||||
if (this.myState.follow) {
|
||||
await apilib.unfollow(this.rootStore, this.myState.follow)
|
||||
|
||||
const follows = this.rootStore.me.follows
|
||||
const followUri = follows.isFollowing(this.did)
|
||||
? follows.getFollowUri(this.did)
|
||||
: undefined
|
||||
|
||||
// guard against this view getting out of sync with the follows cache
|
||||
if (followUri !== this.viewer.following) {
|
||||
this.viewer.following = followUri
|
||||
return
|
||||
}
|
||||
|
||||
if (followUri) {
|
||||
await apilib.unfollow(this.rootStore, followUri)
|
||||
runInAction(() => {
|
||||
this.followersCount--
|
||||
this.myState.follow = undefined
|
||||
this.viewer.following = undefined
|
||||
this.rootStore.me.follows.removeFollow(this.did)
|
||||
})
|
||||
} else {
|
||||
const res = await apilib.follow(
|
||||
@@ -112,7 +126,8 @@ export class ProfileViewModel {
|
||||
)
|
||||
runInAction(() => {
|
||||
this.followersCount++
|
||||
this.myState.follow = res.uri
|
||||
this.viewer.following = res.uri
|
||||
this.rootStore.me.follows.addFollow(this.did, res.uri)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -153,13 +168,13 @@ export class ProfileViewModel {
|
||||
|
||||
async muteAccount() {
|
||||
await this.rootStore.api.app.bsky.graph.mute({user: this.did})
|
||||
this.myState.muted = true
|
||||
this.viewer.muted = true
|
||||
await this.refresh()
|
||||
}
|
||||
|
||||
async unmuteAccount() {
|
||||
await this.rootStore.api.app.bsky.graph.unmute({user: this.did})
|
||||
this.myState.muted = false
|
||||
this.viewer.muted = false
|
||||
await this.refresh()
|
||||
}
|
||||
|
||||
@@ -211,8 +226,9 @@ export class ProfileViewModel {
|
||||
this.followersCount = res.data.followersCount
|
||||
this.followsCount = res.data.followsCount
|
||||
this.postsCount = res.data.postsCount
|
||||
if (res.data.myState) {
|
||||
Object.assign(this.myState, res.data.myState)
|
||||
if (res.data.viewer) {
|
||||
Object.assign(this.viewer, res.data.viewer)
|
||||
this.rootStore.me.follows.hydrate(this.did, res.data.viewer.following)
|
||||
}
|
||||
this.descriptionEntities = extractEntities(this.description || '')
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import {makeAutoObservable, runInAction} from 'mobx'
|
||||
import {AtUri} from '../../third-party/uri'
|
||||
import {AppBskyFeedGetRepostedBy as GetRepostedBy} from '@atproto/api'
|
||||
import {
|
||||
AppBskyFeedGetRepostedBy as GetRepostedBy,
|
||||
AppBskyActorRef as ActorRef,
|
||||
} from '@atproto/api'
|
||||
import {RootStoreModel} from './root-store'
|
||||
import {bundleAsync} from '../../lib/async/bundle'
|
||||
import {cleanError} from '../../lib/strings'
|
||||
@@ -8,7 +11,7 @@ import * as apilib from '../lib/api'
|
||||
|
||||
const PAGE_SIZE = 30
|
||||
|
||||
export type RepostedByItem = GetRepostedBy.RepostedBy
|
||||
export type RepostedByItem = ActorRef.WithInfo
|
||||
|
||||
export class RepostedByViewModel {
|
||||
// state
|
||||
@@ -127,5 +130,6 @@ export class RepostedByViewModel {
|
||||
this.loadMoreCursor = res.data.cursor
|
||||
this.hasMore = !!this.loadMoreCursor
|
||||
this.repostedBy = this.repostedBy.concat(res.data.repostedBy)
|
||||
this.rootStore.me.follows.hydrateProfiles(res.data.repostedBy)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,6 +153,7 @@ export class RootStoreModel {
|
||||
}
|
||||
try {
|
||||
await this.me.fetchNotifications()
|
||||
await this.me.follows.fetchIfNeeded()
|
||||
} catch (e: any) {
|
||||
this.log.error('Failed to fetch latest state', e)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import {makeAutoObservable, runInAction} from 'mobx'
|
||||
import {
|
||||
AppBskyActorGetSuggestions as GetSuggestions,
|
||||
AppBskyActorProfile as Profile,
|
||||
} from '@atproto/api'
|
||||
import {AppBskyActorProfile as Profile} from '@atproto/api'
|
||||
import {RootStoreModel} from './root-store'
|
||||
import {cleanError} from '../../lib/strings'
|
||||
import {bundleAsync} from '../../lib/async/bundle'
|
||||
@@ -14,7 +11,7 @@ import {
|
||||
|
||||
const PAGE_SIZE = 30
|
||||
|
||||
export type SuggestedActor = GetSuggestions.Actor | Profile.View
|
||||
export type SuggestedActor = Profile.ViewBasic | Profile.View
|
||||
|
||||
const getSuggestionList = ({serviceUrl}: {serviceUrl: string}) => {
|
||||
if (serviceUrl.includes('localhost')) {
|
||||
@@ -142,10 +139,15 @@ export class SuggestedActorsViewModel {
|
||||
} while (actors.length)
|
||||
|
||||
runInAction(() => {
|
||||
this.hardCodedSuggestions = profiles.filter(
|
||||
profile =>
|
||||
!profile.myState?.follow && profile.did !== this.rootStore.me.did,
|
||||
)
|
||||
this.hardCodedSuggestions = profiles.filter(profile => {
|
||||
if (this.rootStore.me.follows.isFollowing(profile.did)) {
|
||||
return false
|
||||
}
|
||||
if (profile.did === this.rootStore.me.did) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
})
|
||||
} catch (e) {
|
||||
this.rootStore.log.error(
|
||||
|
||||
@@ -9,7 +9,7 @@ import {bundleAsync} from '../../lib/async/bundle'
|
||||
|
||||
const PAGE_SIZE = 30
|
||||
|
||||
export type FollowerItem = GetFollowers.Follower
|
||||
export type FollowerItem = ActorRef.WithInfo
|
||||
|
||||
export class UserFollowersViewModel {
|
||||
// state
|
||||
@@ -116,5 +116,6 @@ export class UserFollowersViewModel {
|
||||
this.loadMoreCursor = res.data.cursor
|
||||
this.hasMore = !!this.loadMoreCursor
|
||||
this.followers = this.followers.concat(res.data.followers)
|
||||
this.rootStore.me.follows.hydrateProfiles(res.data.followers)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import {bundleAsync} from '../../lib/async/bundle'
|
||||
|
||||
const PAGE_SIZE = 30
|
||||
|
||||
export type FollowItem = GetFollows.Follow
|
||||
export type FollowItem = ActorRef.WithInfo
|
||||
|
||||
export class UserFollowsViewModel {
|
||||
// state
|
||||
@@ -116,5 +116,6 @@ export class UserFollowsViewModel {
|
||||
this.loadMoreCursor = res.data.cursor
|
||||
this.hasMore = !!this.loadMoreCursor
|
||||
this.follows = this.follows.concat(res.data.follows)
|
||||
this.rootStore.me.follows.hydrateProfiles(res.data.follows)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +1,27 @@
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
FlatList,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import LinearGradient from 'react-native-linear-gradient'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import React from 'react'
|
||||
import {ActivityIndicator, FlatList, StyleSheet, View} from 'react-native'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import _omit from 'lodash.omit'
|
||||
import {ErrorScreen} from '../util/error/ErrorScreen'
|
||||
import {Link} from '../util/Link'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {UserAvatar} from '../util/UserAvatar'
|
||||
import * as Toast from '../util/Toast'
|
||||
import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
|
||||
import {useStores} from '../../../state'
|
||||
import * as apilib from '../../../state/lib/api'
|
||||
import {
|
||||
SuggestedActorsViewModel,
|
||||
SuggestedActor,
|
||||
} from '../../../state/models/suggested-actors-view'
|
||||
import {s, gradients} from '../../lib/styles'
|
||||
import {s} from '../../lib/styles'
|
||||
import {usePalette} from '../../lib/hooks/usePalette'
|
||||
|
||||
export const SuggestedFollows = observer(
|
||||
({
|
||||
onNoSuggestions,
|
||||
asLinks,
|
||||
}: {
|
||||
onNoSuggestions?: () => void
|
||||
asLinks?: boolean
|
||||
}) => {
|
||||
({onNoSuggestions}: {onNoSuggestions?: () => void}) => {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const [follows, setFollows] = useState<Record<string, string>>({})
|
||||
|
||||
const view = React.useMemo<SuggestedActorsViewModel>(
|
||||
() => new SuggestedActorsViewModel(store),
|
||||
[store],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
React.useEffect(() => {
|
||||
view
|
||||
.loadMore()
|
||||
.catch((err: any) =>
|
||||
@@ -49,7 +29,7 @@ export const SuggestedFollows = observer(
|
||||
)
|
||||
}, [view, store.log])
|
||||
|
||||
useEffect(() => {
|
||||
React.useEffect(() => {
|
||||
if (!view.isLoading && !view.hasError && !view.hasContent) {
|
||||
onNoSuggestions?.()
|
||||
}
|
||||
@@ -70,48 +50,16 @@ export const SuggestedFollows = observer(
|
||||
)
|
||||
}
|
||||
|
||||
const onPressFollow = async (item: SuggestedActor) => {
|
||||
try {
|
||||
const res = await apilib.follow(store, item.did, item.declaration.cid)
|
||||
setFollows({[item.did]: res.uri, ...follows})
|
||||
} catch (e: any) {
|
||||
store.log.error('Failed fo create follow', e)
|
||||
Toast.show('An issue occurred, please try again.')
|
||||
}
|
||||
}
|
||||
const onPressUnfollow = async (item: SuggestedActor) => {
|
||||
try {
|
||||
await apilib.unfollow(store, follows[item.did])
|
||||
setFollows(_omit(follows, [item.did]))
|
||||
} catch (e: any) {
|
||||
store.log.error('Failed fo delete follow', e)
|
||||
Toast.show('An issue occurred, please try again.')
|
||||
}
|
||||
}
|
||||
|
||||
const renderItem = ({item}: {item: SuggestedActor}) => {
|
||||
if (asLinks) {
|
||||
return (
|
||||
<Link
|
||||
key={item.did}
|
||||
href={`/profile/${item.handle}`}
|
||||
title={item.displayName || item.handle}>
|
||||
<User
|
||||
item={item}
|
||||
follow={follows[item.did]}
|
||||
onPressFollow={onPressFollow}
|
||||
onPressUnfollow={onPressUnfollow}
|
||||
/>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<User
|
||||
<ProfileCardWithFollowBtn
|
||||
key={item.did}
|
||||
item={item}
|
||||
follow={follows[item.did]}
|
||||
onPressFollow={onPressFollow}
|
||||
onPressUnfollow={onPressUnfollow}
|
||||
did={item.did}
|
||||
declarationCid={item.declaration.cid}
|
||||
handle={item.handle}
|
||||
displayName={item.displayName}
|
||||
avatar={item.avatar}
|
||||
description={item.description}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -150,75 +98,6 @@ export const SuggestedFollows = observer(
|
||||
},
|
||||
)
|
||||
|
||||
const User = ({
|
||||
item,
|
||||
follow,
|
||||
onPressFollow,
|
||||
onPressUnfollow,
|
||||
}: {
|
||||
item: SuggestedActor
|
||||
follow: string | undefined
|
||||
onPressFollow: (item: SuggestedActor) => void
|
||||
onPressUnfollow: (item: SuggestedActor) => void
|
||||
}) => {
|
||||
const pal = usePalette('default')
|
||||
return (
|
||||
<View style={[styles.actor, pal.view, pal.border]}>
|
||||
<View style={styles.actorMeta}>
|
||||
<View style={styles.actorAvi}>
|
||||
<UserAvatar
|
||||
size={40}
|
||||
displayName={item.displayName}
|
||||
handle={item.handle}
|
||||
avatar={item.avatar}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.actorContent}>
|
||||
<Text type="title-sm" style={pal.text} numberOfLines={1}>
|
||||
{item.displayName || item.handle}
|
||||
</Text>
|
||||
<Text style={pal.textLight} numberOfLines={1}>
|
||||
@{item.handle}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.actorBtn}>
|
||||
{follow ? (
|
||||
<TouchableOpacity onPress={() => onPressUnfollow(item)}>
|
||||
<View style={[styles.btn, styles.secondaryBtn, pal.btn]}>
|
||||
<Text type="button" style={pal.text}>
|
||||
Unfollow
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<TouchableOpacity onPress={() => onPressFollow(item)}>
|
||||
<LinearGradient
|
||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||
start={{x: 0, y: 0}}
|
||||
end={{x: 1, y: 1}}
|
||||
style={[styles.btn, styles.gradientBtn]}>
|
||||
<FontAwesomeIcon
|
||||
icon="plus"
|
||||
style={[s.white, s.mr5]}
|
||||
size={15}
|
||||
/>
|
||||
<Text style={[s.white, s.fw600, s.f15]}>Follow</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
{item.description ? (
|
||||
<View style={styles.actorDetails}>
|
||||
<Text style={pal.text} numberOfLines={4}>
|
||||
{item.description}
|
||||
</Text>
|
||||
</View>
|
||||
) : undefined}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
height: '100%',
|
||||
@@ -231,48 +110,4 @@ const styles = StyleSheet.create({
|
||||
height: 200,
|
||||
paddingTop: 20,
|
||||
},
|
||||
|
||||
actor: {
|
||||
borderTopWidth: 1,
|
||||
paddingHorizontal: 6,
|
||||
},
|
||||
actorMeta: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
actorAvi: {
|
||||
width: 60,
|
||||
paddingLeft: 10,
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
},
|
||||
actorContent: {
|
||||
flex: 1,
|
||||
paddingRight: 10,
|
||||
paddingTop: 10,
|
||||
},
|
||||
actorBtn: {
|
||||
paddingRight: 10,
|
||||
paddingTop: 10,
|
||||
},
|
||||
actorDetails: {
|
||||
paddingLeft: 60,
|
||||
paddingRight: 10,
|
||||
paddingBottom: 10,
|
||||
},
|
||||
|
||||
gradientBtn: {
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 6,
|
||||
},
|
||||
secondaryBtn: {
|
||||
paddingHorizontal: 14,
|
||||
},
|
||||
btn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 7,
|
||||
borderRadius: 50,
|
||||
marginLeft: 6,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -6,23 +6,16 @@ import {
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import _omit from 'lodash.omit'
|
||||
import {useStores} from '../../../state'
|
||||
import {
|
||||
SuggestedActorsViewModel,
|
||||
SuggestedActor,
|
||||
} from '../../../state/models/suggested-actors-view'
|
||||
import * as apilib from '../../../state/lib/api'
|
||||
import {s} from '../../lib/styles'
|
||||
import {ProfileCard} from '../profile/ProfileCard'
|
||||
import * as Toast from '../util/Toast'
|
||||
import {SuggestedActorsViewModel} from '../../../state/models/suggested-actors-view'
|
||||
import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {s} from '../../lib/styles'
|
||||
import {usePalette} from '../../lib/hooks/usePalette'
|
||||
|
||||
export const WhoToFollow = observer(() => {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const [follows, setFollows] = React.useState<Record<string, string>>({})
|
||||
const suggestedActorsView = React.useMemo<SuggestedActorsViewModel>(
|
||||
() => new SuggestedActorsViewModel(store, {pageSize: 5}),
|
||||
[store],
|
||||
@@ -35,25 +28,6 @@ export const WhoToFollow = observer(() => {
|
||||
const onPressLoadMoreSuggestedActors = () => {
|
||||
suggestedActorsView.loadMore()
|
||||
}
|
||||
const onToggleFollow = async (item: SuggestedActor) => {
|
||||
if (follows[item.did]) {
|
||||
try {
|
||||
await apilib.unfollow(store, follows[item.did])
|
||||
setFollows(_omit(follows, [item.did]))
|
||||
} catch (e: any) {
|
||||
store.log.error('Failed fo delete follow', e)
|
||||
Toast.show('An issue occurred, please try again.')
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const res = await apilib.follow(store, item.did, item.declaration.cid)
|
||||
setFollows({[item.did]: res.uri, ...follows})
|
||||
} catch (e: any) {
|
||||
store.log.error('Failed fo create follow', e)
|
||||
Toast.show('An issue occurred, please try again.')
|
||||
}
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{(suggestedActorsView.hasContent || suggestedActorsView.isLoading) && (
|
||||
@@ -65,18 +39,14 @@ export const WhoToFollow = observer(() => {
|
||||
<>
|
||||
<View style={[pal.border, styles.bottomBorder]}>
|
||||
{suggestedActorsView.suggestions.map(item => (
|
||||
<ProfileCard
|
||||
<ProfileCardWithFollowBtn
|
||||
key={item.did}
|
||||
did={item.did}
|
||||
declarationCid={item.declaration.cid}
|
||||
handle={item.handle}
|
||||
displayName={item.displayName}
|
||||
avatar={item.avatar}
|
||||
description={item.description}
|
||||
renderButton={() => (
|
||||
<FollowBtn
|
||||
isFollowing={!!follows[item.did]}
|
||||
onPress={() => onToggleFollow(item)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
@@ -100,25 +70,6 @@ export const WhoToFollow = observer(() => {
|
||||
)
|
||||
})
|
||||
|
||||
function FollowBtn({
|
||||
isFollowing,
|
||||
onPress,
|
||||
}: {
|
||||
isFollowing: boolean
|
||||
onPress: () => void
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress}>
|
||||
<View style={[styles.btn, pal.btn]}>
|
||||
<Text type="button" style={[pal.text]}>
|
||||
{isFollowing ? 'Unfollow' : 'Follow'}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
heading: {
|
||||
fontWeight: 'bold',
|
||||
@@ -135,14 +86,4 @@ const styles = StyleSheet.create({
|
||||
paddingLeft: 16,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
|
||||
btn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 7,
|
||||
borderRadius: 50,
|
||||
marginLeft: 6,
|
||||
paddingHorizontal: 14,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -5,12 +5,9 @@ import {
|
||||
RepostedByViewModel,
|
||||
RepostedByItem,
|
||||
} from '../../../state/models/reposted-by-view'
|
||||
import {UserAvatar} from '../util/UserAvatar'
|
||||
import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
|
||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||
import {Link} from '../util/Link'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {useStores} from '../../../state'
|
||||
import {s, colors} from '../../lib/styles'
|
||||
|
||||
export const PostRepostedBy = observer(function PostRepostedBy({
|
||||
uri,
|
||||
@@ -61,7 +58,15 @@ export const PostRepostedBy = observer(function PostRepostedBy({
|
||||
// loaded
|
||||
// =
|
||||
const renderItem = ({item}: {item: RepostedByItem}) => (
|
||||
<RepostedByItemCom item={item} />
|
||||
<ProfileCardWithFollowBtn
|
||||
key={item.did}
|
||||
did={item.did}
|
||||
declarationCid={item.declaration.cid}
|
||||
handle={item.handle}
|
||||
displayName={item.displayName}
|
||||
avatar={item.avatar}
|
||||
isFollowedBy={!!item.viewer?.followedBy}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<FlatList
|
||||
@@ -82,57 +87,7 @@ export const PostRepostedBy = observer(function PostRepostedBy({
|
||||
)
|
||||
})
|
||||
|
||||
const RepostedByItemCom = ({item}: {item: RepostedByItem}) => {
|
||||
return (
|
||||
<Link
|
||||
style={styles.outer}
|
||||
href={`/profile/${item.handle}`}
|
||||
title={item.handle}
|
||||
noFeedback>
|
||||
<View style={styles.layout}>
|
||||
<View style={styles.layoutAvi}>
|
||||
<UserAvatar
|
||||
size={40}
|
||||
displayName={item.displayName}
|
||||
handle={item.handle}
|
||||
avatar={item.avatar}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.layoutContent}>
|
||||
<Text style={[s.f15, s.bold]}>{item.displayName || item.handle}</Text>
|
||||
<Text style={[s.f14, s.gray5]}>@{item.handle}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
outer: {
|
||||
marginTop: 1,
|
||||
backgroundColor: colors.white,
|
||||
},
|
||||
layout: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
layoutAvi: {
|
||||
width: 60,
|
||||
paddingLeft: 10,
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
},
|
||||
avi: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
resizeMode: 'cover',
|
||||
},
|
||||
layoutContent: {
|
||||
flex: 1,
|
||||
paddingRight: 10,
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
},
|
||||
footer: {
|
||||
height: 200,
|
||||
paddingTop: 20,
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import React, {useEffect} from 'react'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {ActivityIndicator, FlatList, StyleSheet, View} from 'react-native'
|
||||
import {VotesViewModel, VotesItem} from '../../../state/models/votes-view'
|
||||
import {Link} from '../util/Link'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {VotesViewModel, VoteItem} from '../../../state/models/votes-view'
|
||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||
import {UserAvatar} from '../util/UserAvatar'
|
||||
import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
|
||||
import {useStores} from '../../../state'
|
||||
import {s} from '../../lib/styles'
|
||||
import {usePalette} from '../../lib/hooks/usePalette'
|
||||
|
||||
export const PostVotedBy = observer(function PostVotedBy({
|
||||
uri,
|
||||
@@ -56,7 +52,17 @@ export const PostVotedBy = observer(function PostVotedBy({
|
||||
|
||||
// loaded
|
||||
// =
|
||||
const renderItem = ({item}: {item: VotesItem}) => <LikedByItem item={item} />
|
||||
const renderItem = ({item}: {item: VoteItem}) => (
|
||||
<ProfileCardWithFollowBtn
|
||||
key={item.actor.did}
|
||||
did={item.actor.did}
|
||||
declarationCid={item.actor.declaration.cid}
|
||||
handle={item.actor.handle}
|
||||
displayName={item.actor.displayName}
|
||||
avatar={item.actor.avatar}
|
||||
isFollowedBy={!!item.actor.viewer?.followedBy}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<FlatList
|
||||
data={view.votes}
|
||||
@@ -76,62 +82,7 @@ export const PostVotedBy = observer(function PostVotedBy({
|
||||
)
|
||||
})
|
||||
|
||||
const LikedByItem = ({item}: {item: VotesItem}) => {
|
||||
const pal = usePalette('default')
|
||||
|
||||
return (
|
||||
<Link
|
||||
style={[styles.outer, pal.view]}
|
||||
href={`/profile/${item.actor.handle}`}
|
||||
title={item.actor.handle}
|
||||
noFeedback>
|
||||
<View style={styles.layout}>
|
||||
<View style={styles.layoutAvi}>
|
||||
<UserAvatar
|
||||
size={40}
|
||||
displayName={item.actor.displayName}
|
||||
handle={item.actor.handle}
|
||||
avatar={item.actor.avatar}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.layoutContent}>
|
||||
<Text style={[s.f15, s.bold, pal.text]}>
|
||||
{item.actor.displayName || item.actor.handle}
|
||||
</Text>
|
||||
<Text style={[s.f14, s.gray5, pal.textLight]}>
|
||||
@{item.actor.handle}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
outer: {
|
||||
marginTop: 1,
|
||||
},
|
||||
layout: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
layoutAvi: {
|
||||
width: 60,
|
||||
paddingLeft: 10,
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
},
|
||||
avi: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
resizeMode: 'cover',
|
||||
},
|
||||
layoutContent: {
|
||||
flex: 1,
|
||||
paddingRight: 10,
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
},
|
||||
footer: {
|
||||
height: 200,
|
||||
paddingTop: 20,
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
import React from 'react'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {Link} from '../util/Link'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {UserAvatar} from '../util/UserAvatar'
|
||||
import * as Toast from '../util/Toast'
|
||||
import {s} from '../../lib/styles'
|
||||
import {usePalette} from '../../lib/hooks/usePalette'
|
||||
import {useStores} from '../../../state'
|
||||
import * as apilib from '../../../state/lib/api'
|
||||
|
||||
export function ProfileCard({
|
||||
handle,
|
||||
displayName,
|
||||
avatar,
|
||||
description,
|
||||
isFollowedBy,
|
||||
renderButton,
|
||||
}: {
|
||||
handle: string
|
||||
displayName?: string
|
||||
avatar?: string
|
||||
description?: string
|
||||
isFollowedBy?: boolean
|
||||
renderButton?: () => JSX.Element
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
@@ -36,12 +42,19 @@ export function ProfileCard({
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.layoutContent}>
|
||||
<Text style={[s.bold, pal.text]} numberOfLines={1}>
|
||||
<Text type="lg" style={[s.bold, pal.text]} numberOfLines={1}>
|
||||
{displayName || handle}
|
||||
</Text>
|
||||
<Text type="sm" style={[pal.textLight]} numberOfLines={1}>
|
||||
<Text type="md" style={[pal.textLight]} numberOfLines={1}>
|
||||
@{handle}
|
||||
</Text>
|
||||
{isFollowedBy && (
|
||||
<View style={s.flexRow}>
|
||||
<View style={[s.mt5, pal.btn, styles.pill]}>
|
||||
<Text type="xs">Follows You</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
{renderButton ? (
|
||||
<View style={styles.layoutButton}>{renderButton()}</View>
|
||||
@@ -58,6 +71,84 @@ export function ProfileCard({
|
||||
)
|
||||
}
|
||||
|
||||
export const ProfileCardWithFollowBtn = observer(
|
||||
({
|
||||
did,
|
||||
declarationCid,
|
||||
handle,
|
||||
displayName,
|
||||
avatar,
|
||||
description,
|
||||
isFollowedBy,
|
||||
}: {
|
||||
did: string
|
||||
declarationCid: string
|
||||
handle: string
|
||||
displayName?: string
|
||||
avatar?: string
|
||||
description?: string
|
||||
isFollowedBy?: boolean
|
||||
}) => {
|
||||
const store = useStores()
|
||||
const isMe = store.me.handle === handle
|
||||
const isFollowing = store.me.follows.isFollowing(did)
|
||||
const onToggleFollow = async () => {
|
||||
if (store.me.follows.isFollowing(did)) {
|
||||
try {
|
||||
await apilib.unfollow(store, store.me.follows.getFollowUri(did))
|
||||
store.me.follows.removeFollow(did)
|
||||
} catch (e: any) {
|
||||
store.log.error('Failed fo delete follow', e)
|
||||
Toast.show('An issue occurred, please try again.')
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const res = await apilib.follow(store, did, declarationCid)
|
||||
store.me.follows.addFollow(did, res.uri)
|
||||
} catch (e: any) {
|
||||
store.log.error('Failed fo create follow', e)
|
||||
Toast.show('An issue occurred, please try again.')
|
||||
}
|
||||
}
|
||||
}
|
||||
return (
|
||||
<ProfileCard
|
||||
handle={handle}
|
||||
displayName={displayName}
|
||||
avatar={avatar}
|
||||
description={description}
|
||||
isFollowedBy={isFollowedBy}
|
||||
renderButton={
|
||||
isMe
|
||||
? undefined
|
||||
: () => (
|
||||
<FollowBtn isFollowing={isFollowing} onPress={onToggleFollow} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
function FollowBtn({
|
||||
isFollowing,
|
||||
onPress,
|
||||
}: {
|
||||
isFollowing: boolean
|
||||
onPress: () => void
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress}>
|
||||
<View style={[styles.btn, pal.btn]}>
|
||||
<Text type="button" style={[pal.text]}>
|
||||
{isFollowing ? 'Unfollow' : 'Follow'}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
outer: {
|
||||
borderTopWidth: 1,
|
||||
@@ -93,4 +184,15 @@ const styles = StyleSheet.create({
|
||||
paddingRight: 10,
|
||||
paddingBottom: 10,
|
||||
},
|
||||
pill: {
|
||||
borderRadius: 4,
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 2,
|
||||
},
|
||||
btn: {
|
||||
paddingVertical: 7,
|
||||
borderRadius: 50,
|
||||
marginLeft: 6,
|
||||
paddingHorizontal: 14,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -5,13 +5,9 @@ import {
|
||||
UserFollowersViewModel,
|
||||
FollowerItem,
|
||||
} from '../../../state/models/user-followers-view'
|
||||
import {Link} from '../util/Link'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||
import {UserAvatar} from '../util/UserAvatar'
|
||||
import {ProfileCardWithFollowBtn} from './ProfileCard'
|
||||
import {useStores} from '../../../state'
|
||||
import {s} from '../../lib/styles'
|
||||
import {usePalette} from '../../lib/hooks/usePalette'
|
||||
|
||||
export const ProfileFollowers = observer(function ProfileFollowers({
|
||||
name,
|
||||
@@ -62,7 +58,15 @@ export const ProfileFollowers = observer(function ProfileFollowers({
|
||||
// loaded
|
||||
// =
|
||||
const renderItem = ({item}: {item: FollowerItem}) => (
|
||||
<User key={item.did} item={item} />
|
||||
<ProfileCardWithFollowBtn
|
||||
key={item.did}
|
||||
did={item.did}
|
||||
declarationCid={item.declaration.cid}
|
||||
handle={item.handle}
|
||||
displayName={item.displayName}
|
||||
avatar={item.avatar}
|
||||
isFollowedBy={!!item.viewer?.followedBy}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<FlatList
|
||||
@@ -83,55 +87,7 @@ export const ProfileFollowers = observer(function ProfileFollowers({
|
||||
)
|
||||
})
|
||||
|
||||
const User = ({item}: {item: FollowerItem}) => {
|
||||
const pal = usePalette('default')
|
||||
return (
|
||||
<Link
|
||||
style={[styles.outer, pal.view, pal.border]}
|
||||
href={`/profile/${item.handle}`}
|
||||
title={item.handle}
|
||||
noFeedback>
|
||||
<View style={styles.layout}>
|
||||
<View style={styles.layoutAvi}>
|
||||
<UserAvatar
|
||||
size={40}
|
||||
displayName={item.displayName}
|
||||
handle={item.handle}
|
||||
avatar={item.avatar}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.layoutContent}>
|
||||
<Text style={[s.bold, pal.text]}>
|
||||
{item.displayName || item.handle}
|
||||
</Text>
|
||||
<Text type="sm" style={[pal.textLight]}>
|
||||
@{item.handle}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
outer: {
|
||||
borderTopWidth: 1,
|
||||
},
|
||||
layout: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
layoutAvi: {
|
||||
width: 60,
|
||||
paddingLeft: 10,
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
},
|
||||
layoutContent: {
|
||||
flex: 1,
|
||||
paddingRight: 10,
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
},
|
||||
footer: {
|
||||
height: 200,
|
||||
paddingTop: 20,
|
||||
|
||||
@@ -5,13 +5,9 @@ import {
|
||||
UserFollowsViewModel,
|
||||
FollowItem,
|
||||
} from '../../../state/models/user-follows-view'
|
||||
import {Link} from '../util/Link'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||
import {UserAvatar} from '../util/UserAvatar'
|
||||
import {ProfileCardWithFollowBtn} from './ProfileCard'
|
||||
import {useStores} from '../../../state'
|
||||
import {s} from '../../lib/styles'
|
||||
import {usePalette} from '../../lib/hooks/usePalette'
|
||||
|
||||
export const ProfileFollows = observer(function ProfileFollows({
|
||||
name,
|
||||
@@ -62,7 +58,15 @@ export const ProfileFollows = observer(function ProfileFollows({
|
||||
// loaded
|
||||
// =
|
||||
const renderItem = ({item}: {item: FollowItem}) => (
|
||||
<User key={item.did} item={item} />
|
||||
<ProfileCardWithFollowBtn
|
||||
key={item.did}
|
||||
did={item.did}
|
||||
declarationCid={item.declaration.cid}
|
||||
handle={item.handle}
|
||||
displayName={item.displayName}
|
||||
avatar={item.avatar}
|
||||
isFollowedBy={!!item.viewer?.followedBy}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<FlatList
|
||||
@@ -83,55 +87,7 @@ export const ProfileFollows = observer(function ProfileFollows({
|
||||
)
|
||||
})
|
||||
|
||||
const User = ({item}: {item: FollowItem}) => {
|
||||
const pal = usePalette('default')
|
||||
return (
|
||||
<Link
|
||||
style={[styles.outer, pal.view, pal.border]}
|
||||
href={`/profile/${item.handle}`}
|
||||
title={item.handle}
|
||||
noFeedback>
|
||||
<View style={styles.layout}>
|
||||
<View style={styles.layoutAvi}>
|
||||
<UserAvatar
|
||||
size={40}
|
||||
displayName={item.displayName}
|
||||
handle={item.handle}
|
||||
avatar={item.avatar}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.layoutContent}>
|
||||
<Text style={[s.bold, pal.text]}>
|
||||
{item.displayName || item.handle}
|
||||
</Text>
|
||||
<Text type="sm" style={[pal.textLight]}>
|
||||
@{item.handle}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
outer: {
|
||||
borderTopWidth: 1,
|
||||
},
|
||||
layout: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
layoutAvi: {
|
||||
width: 60,
|
||||
paddingLeft: 10,
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
},
|
||||
layoutContent: {
|
||||
flex: 1,
|
||||
paddingRight: 10,
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
},
|
||||
footer: {
|
||||
height: 200,
|
||||
paddingTop: 20,
|
||||
|
||||
@@ -51,7 +51,7 @@ export const ProfileHeader = observer(function ProfileHeader({
|
||||
view?.toggleFollowing().then(
|
||||
() => {
|
||||
Toast.show(
|
||||
`${view.myState.follow ? 'Following' : 'No longer following'} ${
|
||||
`${view.viewer.following ? 'Following' : 'No longer following'} ${
|
||||
view.displayName || view.handle
|
||||
}`,
|
||||
)
|
||||
@@ -140,8 +140,8 @@ export const ProfileHeader = observer(function ProfileHeader({
|
||||
let dropdownItems: DropdownItem[] = [{label: 'Share', onPress: onPressShare}]
|
||||
if (!isMe) {
|
||||
dropdownItems.push({
|
||||
label: view.myState.muted ? 'Unmute Account' : 'Mute Account',
|
||||
onPress: view.myState.muted ? onPressUnmuteAccount : onPressMuteAccount,
|
||||
label: view.viewer.muted ? 'Unmute Account' : 'Mute Account',
|
||||
onPress: view.viewer.muted ? onPressUnmuteAccount : onPressMuteAccount,
|
||||
})
|
||||
dropdownItems.push({
|
||||
label: 'Report Account',
|
||||
@@ -164,7 +164,7 @@ export const ProfileHeader = observer(function ProfileHeader({
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<>
|
||||
{view.myState.follow ? (
|
||||
{store.me.follows.isFollowing(view.did) ? (
|
||||
<TouchableOpacity
|
||||
onPress={onPressToggleFollow}
|
||||
style={[styles.btn, styles.mainBtn, pal.btn]}>
|
||||
@@ -213,6 +213,13 @@ export const ProfileHeader = observer(function ProfileHeader({
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.handleLine}>
|
||||
{view.viewer.followedBy ? (
|
||||
<View style={[styles.pill, pal.btn, s.mr5]}>
|
||||
<Text type="xs" style={[pal.text]}>
|
||||
Follows you
|
||||
</Text>
|
||||
</View>
|
||||
) : undefined}
|
||||
<Text style={pal.textLight}>@{view.handle}</Text>
|
||||
</View>
|
||||
<View style={styles.metricsLine}>
|
||||
@@ -257,14 +264,14 @@ export const ProfileHeader = observer(function ProfileHeader({
|
||||
entities={view.descriptionEntities}
|
||||
/>
|
||||
) : undefined}
|
||||
{view.myState.muted ? (
|
||||
{view.viewer.muted ? (
|
||||
<View style={[styles.detailLine, pal.btn, s.p5]}>
|
||||
<FontAwesomeIcon
|
||||
icon={['far', 'eye-slash']}
|
||||
style={[pal.text, s.mr5]}
|
||||
/>
|
||||
<Text type="md" style={[s.mr2, pal.text]}>
|
||||
Account muted.
|
||||
Account muted
|
||||
</Text>
|
||||
</View>
|
||||
) : undefined}
|
||||
@@ -371,6 +378,12 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 5,
|
||||
},
|
||||
|
||||
pill: {
|
||||
borderRadius: 4,
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 2,
|
||||
},
|
||||
|
||||
br40: {borderRadius: 40},
|
||||
br50: {borderRadius: 50},
|
||||
})
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
jsonpointer "^5.0.0"
|
||||
leven "^3.1.0"
|
||||
|
||||
"@atproto/api@^0.1.1":
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.1.1.tgz#0215dd80b98b8698f9e08932d4cdaf52676cb56c"
|
||||
integrity sha512-OMKLXuWxsdaHmmZ8XzwZ1uzWpq1sr9cuzZJLAbWJz0HRDQ3a0tg1uI80XvDYHFep0qgVoT2OKJNtB23Wp7nKoQ==
|
||||
"@atproto/api@^0.1.2":
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.1.2.tgz#66102f9203ba499432bc5aeb30cd19313ab2e4fc"
|
||||
integrity sha512-lDcFGkrk0J7rkIPSie18xS7sO3IL6DsosX8GgoeqCNeVaDuphBRaFCcpBUWf0q4fHrpmdgghGo4ulefyKHTIFQ==
|
||||
dependencies:
|
||||
"@atproto/xrpc" "*"
|
||||
typed-emitter "^2.1.0"
|
||||
@@ -90,10 +90,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@atproto/nsid/-/nsid-0.0.1.tgz#0cdc00cefe8f0b1385f352b9f57b3ad37fff09a4"
|
||||
integrity sha512-t5M6/CzWBVYoBbIvfKDpqPj/+ZmyoK9ydZSStcTXosJ27XXwOPhz0VDUGKK2SM9G5Y7TPes8S5KTAU0UdVYFCw==
|
||||
|
||||
"@atproto/pds@^0.0.2":
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.0.2.tgz#3665a24ce1f3a5696e46fea5448c5f849e38b7e0"
|
||||
integrity sha512-TCTVKJWaUxF6EQJ6hobhO19bLaoW2Nk8xn/mySnQMN7ZHlYD9Hv+eRVY+PWLpnAuGetCkTbZAspg84eG2lKGTA==
|
||||
"@atproto/pds@^0.0.3":
|
||||
version "0.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.0.3.tgz#118a1d51687664f085f8e1c19ae3ac1646dc69b2"
|
||||
integrity sha512-l5iGJNyQs73V/mQWkcg4NXNGbnfXfv+Yg3g8nqwtun409iAAeYZimA1Tt0JV/hyq+Oz3antG0VvLQQEtNVUpVQ==
|
||||
dependencies:
|
||||
"@atproto/common" "*"
|
||||
"@atproto/crypto" "*"
|
||||
@@ -124,6 +124,7 @@
|
||||
pino "^8.6.1"
|
||||
pino-http "^8.2.1"
|
||||
sharp "^0.31.2"
|
||||
typed-emitter "^2.1.0"
|
||||
uint8arrays "3.0.0"
|
||||
|
||||
"@atproto/plc@*":
|
||||
|
||||
Reference in New Issue
Block a user