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 {NavigationModel} from '../src/state/models/navigation'
|
||||||
import {ShellUiModel} from '../src/state/models/shell-ui'
|
import {ShellUiModel} from '../src/state/models/shell-ui'
|
||||||
import {MeModel} from '../src/state/models/me'
|
import {MeModel} from '../src/state/models/me'
|
||||||
|
import {MyFollowsModel} from '../src/state/models/my-follows'
|
||||||
import {OnboardModel} from '../src/state/models/onboard'
|
import {OnboardModel} from '../src/state/models/onboard'
|
||||||
import {ProfilesViewModel} from '../src/state/models/profiles-view'
|
import {ProfilesViewModel} from '../src/state/models/profiles-view'
|
||||||
import {LinkMetasViewModel} from '../src/state/models/link-metas-view'
|
import {LinkMetasViewModel} from '../src/state/models/link-metas-view'
|
||||||
@@ -53,9 +54,8 @@ export const mockedProfileStore = {
|
|||||||
followsCount: 0,
|
followsCount: 0,
|
||||||
membersCount: 0,
|
membersCount: 0,
|
||||||
postsCount: 0,
|
postsCount: 0,
|
||||||
myState: {
|
viewer: {
|
||||||
follow: '',
|
following: '',
|
||||||
member: '',
|
|
||||||
},
|
},
|
||||||
rootStore: {} as RootStoreModel,
|
rootStore: {} as RootStoreModel,
|
||||||
hasContent: true,
|
hasContent: true,
|
||||||
@@ -572,6 +572,10 @@ export const mockedShellStore = {
|
|||||||
openLightbox: jest.fn(),
|
openLightbox: jest.fn(),
|
||||||
} as ShellUiModel
|
} as ShellUiModel
|
||||||
|
|
||||||
|
export const mockedFollowsStore = {
|
||||||
|
isFollowing: jest.fn().mockReturnValue(false),
|
||||||
|
} as MyFollowsModel
|
||||||
|
|
||||||
export const mockedMeStore = {
|
export const mockedMeStore = {
|
||||||
serialize: jest.fn(),
|
serialize: jest.fn(),
|
||||||
hydrate: jest.fn(),
|
hydrate: jest.fn(),
|
||||||
@@ -585,6 +589,7 @@ export const mockedMeStore = {
|
|||||||
memberships: mockedMembershipsStore,
|
memberships: mockedMembershipsStore,
|
||||||
mainFeed: mockedFeedStore,
|
mainFeed: mockedFeedStore,
|
||||||
notifications: mockedNotificationsStore,
|
notifications: mockedNotificationsStore,
|
||||||
|
follows: mockedFollowsStore,
|
||||||
clear: jest.fn(),
|
clear: jest.fn(),
|
||||||
load: jest.fn(),
|
load: jest.fn(),
|
||||||
clearNotificationCount: jest.fn(),
|
clearNotificationCount: jest.fn(),
|
||||||
|
|||||||
+2
-2
@@ -16,7 +16,7 @@
|
|||||||
"e2e": "detox test --configuration ios.sim.debug --take-screenshots all"
|
"e2e": "detox test --configuration ios.sim.debug --take-screenshots all"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atproto/api": "^0.1.1",
|
"@atproto/api": "^0.1.2",
|
||||||
"@atproto/lexicon": "^0.0.4",
|
"@atproto/lexicon": "^0.0.4",
|
||||||
"@atproto/xrpc": "^0.0.4",
|
"@atproto/xrpc": "^0.0.4",
|
||||||
"@bam.tech/react-native-image-resizer": "^3.0.4",
|
"@bam.tech/react-native-image-resizer": "^3.0.4",
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
"zod": "^3.20.2"
|
"zod": "^3.20.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@atproto/pds": "^0.0.2",
|
"@atproto/pds": "^0.0.3",
|
||||||
"@babel/core": "^7.20.0",
|
"@babel/core": "^7.20.0",
|
||||||
"@babel/preset-env": "^7.20.0",
|
"@babel/preset-env": "^7.20.0",
|
||||||
"@babel/runtime": "^7.20.0",
|
"@babel/runtime": "^7.20.0",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import notifee from '@notifee/react-native'
|
|||||||
import {RootStoreModel} from './root-store'
|
import {RootStoreModel} from './root-store'
|
||||||
import {FeedModel} from './feed-view'
|
import {FeedModel} from './feed-view'
|
||||||
import {NotificationsViewModel} from './notifications-view'
|
import {NotificationsViewModel} from './notifications-view'
|
||||||
|
import {MyFollowsModel} from './my-follows'
|
||||||
import {isObj, hasProp} from '../lib/type-guards'
|
import {isObj, hasProp} from '../lib/type-guards'
|
||||||
import {displayNotificationFromModel} from '../../view/lib/notifee'
|
import {displayNotificationFromModel} from '../../view/lib/notifee'
|
||||||
|
|
||||||
@@ -15,6 +16,7 @@ export class MeModel {
|
|||||||
notificationCount: number = 0
|
notificationCount: number = 0
|
||||||
mainFeed: FeedModel
|
mainFeed: FeedModel
|
||||||
notifications: NotificationsViewModel
|
notifications: NotificationsViewModel
|
||||||
|
follows: MyFollowsModel
|
||||||
|
|
||||||
constructor(public rootStore: RootStoreModel) {
|
constructor(public rootStore: RootStoreModel) {
|
||||||
makeAutoObservable(
|
makeAutoObservable(
|
||||||
@@ -26,6 +28,7 @@ export class MeModel {
|
|||||||
algorithm: 'reverse-chronological',
|
algorithm: 'reverse-chronological',
|
||||||
})
|
})
|
||||||
this.notifications = new NotificationsViewModel(this.rootStore, {})
|
this.notifications = new NotificationsViewModel(this.rootStore, {})
|
||||||
|
this.follows = new MyFollowsModel(this.rootStore)
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
@@ -104,6 +107,9 @@ export class MeModel {
|
|||||||
this.notifications.setup().catch(e => {
|
this.notifications.setup().catch(e => {
|
||||||
this.rootStore.log.error('Failed to setup notifications model', 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
|
// 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 const ACTOR_TYPE_USER = 'app.bsky.system.actorUser'
|
||||||
|
|
||||||
export class ProfileViewMyStateModel {
|
export class ProfileViewViewerModel {
|
||||||
follow?: string
|
|
||||||
muted?: boolean
|
muted?: boolean
|
||||||
|
following?: string
|
||||||
|
followedBy?: string
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
makeAutoObservable(this)
|
makeAutoObservable(this)
|
||||||
@@ -47,7 +48,7 @@ export class ProfileViewModel {
|
|||||||
followersCount: number = 0
|
followersCount: number = 0
|
||||||
followsCount: number = 0
|
followsCount: number = 0
|
||||||
postsCount: number = 0
|
postsCount: number = 0
|
||||||
myState = new ProfileViewMyStateModel()
|
viewer = new ProfileViewViewerModel()
|
||||||
|
|
||||||
// added data
|
// added data
|
||||||
descriptionEntities?: Entity[]
|
descriptionEntities?: Entity[]
|
||||||
@@ -98,11 +99,24 @@ export class ProfileViewModel {
|
|||||||
if (!this.rootStore.me.did) {
|
if (!this.rootStore.me.did) {
|
||||||
throw new Error('Not logged in')
|
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(() => {
|
runInAction(() => {
|
||||||
this.followersCount--
|
this.followersCount--
|
||||||
this.myState.follow = undefined
|
this.viewer.following = undefined
|
||||||
|
this.rootStore.me.follows.removeFollow(this.did)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const res = await apilib.follow(
|
const res = await apilib.follow(
|
||||||
@@ -112,7 +126,8 @@ export class ProfileViewModel {
|
|||||||
)
|
)
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.followersCount++
|
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() {
|
async muteAccount() {
|
||||||
await this.rootStore.api.app.bsky.graph.mute({user: this.did})
|
await this.rootStore.api.app.bsky.graph.mute({user: this.did})
|
||||||
this.myState.muted = true
|
this.viewer.muted = true
|
||||||
await this.refresh()
|
await this.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
async unmuteAccount() {
|
async unmuteAccount() {
|
||||||
await this.rootStore.api.app.bsky.graph.unmute({user: this.did})
|
await this.rootStore.api.app.bsky.graph.unmute({user: this.did})
|
||||||
this.myState.muted = false
|
this.viewer.muted = false
|
||||||
await this.refresh()
|
await this.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,8 +226,9 @@ export class ProfileViewModel {
|
|||||||
this.followersCount = res.data.followersCount
|
this.followersCount = res.data.followersCount
|
||||||
this.followsCount = res.data.followsCount
|
this.followsCount = res.data.followsCount
|
||||||
this.postsCount = res.data.postsCount
|
this.postsCount = res.data.postsCount
|
||||||
if (res.data.myState) {
|
if (res.data.viewer) {
|
||||||
Object.assign(this.myState, res.data.myState)
|
Object.assign(this.viewer, res.data.viewer)
|
||||||
|
this.rootStore.me.follows.hydrate(this.did, res.data.viewer.following)
|
||||||
}
|
}
|
||||||
this.descriptionEntities = extractEntities(this.description || '')
|
this.descriptionEntities = extractEntities(this.description || '')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import {makeAutoObservable, runInAction} from 'mobx'
|
import {makeAutoObservable, runInAction} from 'mobx'
|
||||||
import {AtUri} from '../../third-party/uri'
|
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 {RootStoreModel} from './root-store'
|
||||||
import {bundleAsync} from '../../lib/async/bundle'
|
import {bundleAsync} from '../../lib/async/bundle'
|
||||||
import {cleanError} from '../../lib/strings'
|
import {cleanError} from '../../lib/strings'
|
||||||
@@ -8,7 +11,7 @@ import * as apilib from '../lib/api'
|
|||||||
|
|
||||||
const PAGE_SIZE = 30
|
const PAGE_SIZE = 30
|
||||||
|
|
||||||
export type RepostedByItem = GetRepostedBy.RepostedBy
|
export type RepostedByItem = ActorRef.WithInfo
|
||||||
|
|
||||||
export class RepostedByViewModel {
|
export class RepostedByViewModel {
|
||||||
// state
|
// state
|
||||||
@@ -127,5 +130,6 @@ export class RepostedByViewModel {
|
|||||||
this.loadMoreCursor = res.data.cursor
|
this.loadMoreCursor = res.data.cursor
|
||||||
this.hasMore = !!this.loadMoreCursor
|
this.hasMore = !!this.loadMoreCursor
|
||||||
this.repostedBy = this.repostedBy.concat(res.data.repostedBy)
|
this.repostedBy = this.repostedBy.concat(res.data.repostedBy)
|
||||||
|
this.rootStore.me.follows.hydrateProfiles(res.data.repostedBy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ export class RootStoreModel {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await this.me.fetchNotifications()
|
await this.me.fetchNotifications()
|
||||||
|
await this.me.follows.fetchIfNeeded()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.log.error('Failed to fetch latest state', e)
|
this.log.error('Failed to fetch latest state', e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import {makeAutoObservable, runInAction} from 'mobx'
|
import {makeAutoObservable, runInAction} from 'mobx'
|
||||||
import {
|
import {AppBskyActorProfile as Profile} from '@atproto/api'
|
||||||
AppBskyActorGetSuggestions as GetSuggestions,
|
|
||||||
AppBskyActorProfile as Profile,
|
|
||||||
} from '@atproto/api'
|
|
||||||
import {RootStoreModel} from './root-store'
|
import {RootStoreModel} from './root-store'
|
||||||
import {cleanError} from '../../lib/strings'
|
import {cleanError} from '../../lib/strings'
|
||||||
import {bundleAsync} from '../../lib/async/bundle'
|
import {bundleAsync} from '../../lib/async/bundle'
|
||||||
@@ -14,7 +11,7 @@ import {
|
|||||||
|
|
||||||
const PAGE_SIZE = 30
|
const PAGE_SIZE = 30
|
||||||
|
|
||||||
export type SuggestedActor = GetSuggestions.Actor | Profile.View
|
export type SuggestedActor = Profile.ViewBasic | Profile.View
|
||||||
|
|
||||||
const getSuggestionList = ({serviceUrl}: {serviceUrl: string}) => {
|
const getSuggestionList = ({serviceUrl}: {serviceUrl: string}) => {
|
||||||
if (serviceUrl.includes('localhost')) {
|
if (serviceUrl.includes('localhost')) {
|
||||||
@@ -142,10 +139,15 @@ export class SuggestedActorsViewModel {
|
|||||||
} while (actors.length)
|
} while (actors.length)
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.hardCodedSuggestions = profiles.filter(
|
this.hardCodedSuggestions = profiles.filter(profile => {
|
||||||
profile =>
|
if (this.rootStore.me.follows.isFollowing(profile.did)) {
|
||||||
!profile.myState?.follow && profile.did !== this.rootStore.me.did,
|
return false
|
||||||
)
|
}
|
||||||
|
if (profile.did === this.rootStore.me.did) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.rootStore.log.error(
|
this.rootStore.log.error(
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {bundleAsync} from '../../lib/async/bundle'
|
|||||||
|
|
||||||
const PAGE_SIZE = 30
|
const PAGE_SIZE = 30
|
||||||
|
|
||||||
export type FollowerItem = GetFollowers.Follower
|
export type FollowerItem = ActorRef.WithInfo
|
||||||
|
|
||||||
export class UserFollowersViewModel {
|
export class UserFollowersViewModel {
|
||||||
// state
|
// state
|
||||||
@@ -116,5 +116,6 @@ export class UserFollowersViewModel {
|
|||||||
this.loadMoreCursor = res.data.cursor
|
this.loadMoreCursor = res.data.cursor
|
||||||
this.hasMore = !!this.loadMoreCursor
|
this.hasMore = !!this.loadMoreCursor
|
||||||
this.followers = this.followers.concat(res.data.followers)
|
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
|
const PAGE_SIZE = 30
|
||||||
|
|
||||||
export type FollowItem = GetFollows.Follow
|
export type FollowItem = ActorRef.WithInfo
|
||||||
|
|
||||||
export class UserFollowsViewModel {
|
export class UserFollowsViewModel {
|
||||||
// state
|
// state
|
||||||
@@ -116,5 +116,6 @@ export class UserFollowsViewModel {
|
|||||||
this.loadMoreCursor = res.data.cursor
|
this.loadMoreCursor = res.data.cursor
|
||||||
this.hasMore = !!this.loadMoreCursor
|
this.hasMore = !!this.loadMoreCursor
|
||||||
this.follows = this.follows.concat(res.data.follows)
|
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 React from 'react'
|
||||||
import {
|
import {ActivityIndicator, FlatList, StyleSheet, View} from 'react-native'
|
||||||
ActivityIndicator,
|
|
||||||
FlatList,
|
|
||||||
StyleSheet,
|
|
||||||
TouchableOpacity,
|
|
||||||
View,
|
|
||||||
} from 'react-native'
|
|
||||||
import LinearGradient from 'react-native-linear-gradient'
|
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import _omit from 'lodash.omit'
|
|
||||||
import {ErrorScreen} from '../util/error/ErrorScreen'
|
import {ErrorScreen} from '../util/error/ErrorScreen'
|
||||||
import {Link} from '../util/Link'
|
import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
|
||||||
import {Text} from '../util/text/Text'
|
|
||||||
import {UserAvatar} from '../util/UserAvatar'
|
|
||||||
import * as Toast from '../util/Toast'
|
|
||||||
import {useStores} from '../../../state'
|
import {useStores} from '../../../state'
|
||||||
import * as apilib from '../../../state/lib/api'
|
|
||||||
import {
|
import {
|
||||||
SuggestedActorsViewModel,
|
SuggestedActorsViewModel,
|
||||||
SuggestedActor,
|
SuggestedActor,
|
||||||
} from '../../../state/models/suggested-actors-view'
|
} from '../../../state/models/suggested-actors-view'
|
||||||
import {s, gradients} from '../../lib/styles'
|
import {s} from '../../lib/styles'
|
||||||
import {usePalette} from '../../lib/hooks/usePalette'
|
import {usePalette} from '../../lib/hooks/usePalette'
|
||||||
|
|
||||||
export const SuggestedFollows = observer(
|
export const SuggestedFollows = observer(
|
||||||
({
|
({onNoSuggestions}: {onNoSuggestions?: () => void}) => {
|
||||||
onNoSuggestions,
|
|
||||||
asLinks,
|
|
||||||
}: {
|
|
||||||
onNoSuggestions?: () => void
|
|
||||||
asLinks?: boolean
|
|
||||||
}) => {
|
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const [follows, setFollows] = useState<Record<string, string>>({})
|
|
||||||
|
|
||||||
const view = React.useMemo<SuggestedActorsViewModel>(
|
const view = React.useMemo<SuggestedActorsViewModel>(
|
||||||
() => new SuggestedActorsViewModel(store),
|
() => new SuggestedActorsViewModel(store),
|
||||||
[store],
|
[store],
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
React.useEffect(() => {
|
||||||
view
|
view
|
||||||
.loadMore()
|
.loadMore()
|
||||||
.catch((err: any) =>
|
.catch((err: any) =>
|
||||||
@@ -49,7 +29,7 @@ export const SuggestedFollows = observer(
|
|||||||
)
|
)
|
||||||
}, [view, store.log])
|
}, [view, store.log])
|
||||||
|
|
||||||
useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!view.isLoading && !view.hasError && !view.hasContent) {
|
if (!view.isLoading && !view.hasError && !view.hasContent) {
|
||||||
onNoSuggestions?.()
|
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}) => {
|
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 (
|
return (
|
||||||
<User
|
<ProfileCardWithFollowBtn
|
||||||
key={item.did}
|
key={item.did}
|
||||||
item={item}
|
did={item.did}
|
||||||
follow={follows[item.did]}
|
declarationCid={item.declaration.cid}
|
||||||
onPressFollow={onPressFollow}
|
handle={item.handle}
|
||||||
onPressUnfollow={onPressUnfollow}
|
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({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
height: '100%',
|
height: '100%',
|
||||||
@@ -231,48 +110,4 @@ const styles = StyleSheet.create({
|
|||||||
height: 200,
|
height: 200,
|
||||||
paddingTop: 20,
|
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,
|
View,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import _omit from 'lodash.omit'
|
|
||||||
import {useStores} from '../../../state'
|
import {useStores} from '../../../state'
|
||||||
import {
|
import {SuggestedActorsViewModel} from '../../../state/models/suggested-actors-view'
|
||||||
SuggestedActorsViewModel,
|
import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
|
||||||
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 {Text} from '../util/text/Text'
|
import {Text} from '../util/text/Text'
|
||||||
|
import {s} from '../../lib/styles'
|
||||||
import {usePalette} from '../../lib/hooks/usePalette'
|
import {usePalette} from '../../lib/hooks/usePalette'
|
||||||
|
|
||||||
export const WhoToFollow = observer(() => {
|
export const WhoToFollow = observer(() => {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const [follows, setFollows] = React.useState<Record<string, string>>({})
|
|
||||||
const suggestedActorsView = React.useMemo<SuggestedActorsViewModel>(
|
const suggestedActorsView = React.useMemo<SuggestedActorsViewModel>(
|
||||||
() => new SuggestedActorsViewModel(store, {pageSize: 5}),
|
() => new SuggestedActorsViewModel(store, {pageSize: 5}),
|
||||||
[store],
|
[store],
|
||||||
@@ -35,25 +28,6 @@ export const WhoToFollow = observer(() => {
|
|||||||
const onPressLoadMoreSuggestedActors = () => {
|
const onPressLoadMoreSuggestedActors = () => {
|
||||||
suggestedActorsView.loadMore()
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{(suggestedActorsView.hasContent || suggestedActorsView.isLoading) && (
|
{(suggestedActorsView.hasContent || suggestedActorsView.isLoading) && (
|
||||||
@@ -65,18 +39,14 @@ export const WhoToFollow = observer(() => {
|
|||||||
<>
|
<>
|
||||||
<View style={[pal.border, styles.bottomBorder]}>
|
<View style={[pal.border, styles.bottomBorder]}>
|
||||||
{suggestedActorsView.suggestions.map(item => (
|
{suggestedActorsView.suggestions.map(item => (
|
||||||
<ProfileCard
|
<ProfileCardWithFollowBtn
|
||||||
key={item.did}
|
key={item.did}
|
||||||
|
did={item.did}
|
||||||
|
declarationCid={item.declaration.cid}
|
||||||
handle={item.handle}
|
handle={item.handle}
|
||||||
displayName={item.displayName}
|
displayName={item.displayName}
|
||||||
avatar={item.avatar}
|
avatar={item.avatar}
|
||||||
description={item.description}
|
description={item.description}
|
||||||
renderButton={() => (
|
|
||||||
<FollowBtn
|
|
||||||
isFollowing={!!follows[item.did]}
|
|
||||||
onPress={() => onToggleFollow(item)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</View>
|
</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({
|
const styles = StyleSheet.create({
|
||||||
heading: {
|
heading: {
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
@@ -135,14 +86,4 @@ const styles = StyleSheet.create({
|
|||||||
paddingLeft: 16,
|
paddingLeft: 16,
|
||||||
paddingVertical: 12,
|
paddingVertical: 12,
|
||||||
},
|
},
|
||||||
|
|
||||||
btn: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
paddingVertical: 7,
|
|
||||||
borderRadius: 50,
|
|
||||||
marginLeft: 6,
|
|
||||||
paddingHorizontal: 14,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,12 +5,9 @@ import {
|
|||||||
RepostedByViewModel,
|
RepostedByViewModel,
|
||||||
RepostedByItem,
|
RepostedByItem,
|
||||||
} from '../../../state/models/reposted-by-view'
|
} from '../../../state/models/reposted-by-view'
|
||||||
import {UserAvatar} from '../util/UserAvatar'
|
import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
|
||||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||||
import {Link} from '../util/Link'
|
|
||||||
import {Text} from '../util/text/Text'
|
|
||||||
import {useStores} from '../../../state'
|
import {useStores} from '../../../state'
|
||||||
import {s, colors} from '../../lib/styles'
|
|
||||||
|
|
||||||
export const PostRepostedBy = observer(function PostRepostedBy({
|
export const PostRepostedBy = observer(function PostRepostedBy({
|
||||||
uri,
|
uri,
|
||||||
@@ -61,7 +58,15 @@ export const PostRepostedBy = observer(function PostRepostedBy({
|
|||||||
// loaded
|
// loaded
|
||||||
// =
|
// =
|
||||||
const renderItem = ({item}: {item: RepostedByItem}) => (
|
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 (
|
return (
|
||||||
<FlatList
|
<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({
|
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: {
|
footer: {
|
||||||
height: 200,
|
height: 200,
|
||||||
paddingTop: 20,
|
paddingTop: 20,
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
import React, {useEffect} from 'react'
|
import React, {useEffect} from 'react'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {ActivityIndicator, FlatList, StyleSheet, View} from 'react-native'
|
import {ActivityIndicator, FlatList, StyleSheet, View} from 'react-native'
|
||||||
import {VotesViewModel, VotesItem} from '../../../state/models/votes-view'
|
import {VotesViewModel, VoteItem} from '../../../state/models/votes-view'
|
||||||
import {Link} from '../util/Link'
|
|
||||||
import {Text} from '../util/text/Text'
|
|
||||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||||
import {UserAvatar} from '../util/UserAvatar'
|
import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
|
||||||
import {useStores} from '../../../state'
|
import {useStores} from '../../../state'
|
||||||
import {s} from '../../lib/styles'
|
|
||||||
import {usePalette} from '../../lib/hooks/usePalette'
|
|
||||||
|
|
||||||
export const PostVotedBy = observer(function PostVotedBy({
|
export const PostVotedBy = observer(function PostVotedBy({
|
||||||
uri,
|
uri,
|
||||||
@@ -56,7 +52,17 @@ export const PostVotedBy = observer(function PostVotedBy({
|
|||||||
|
|
||||||
// loaded
|
// 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 (
|
return (
|
||||||
<FlatList
|
<FlatList
|
||||||
data={view.votes}
|
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({
|
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: {
|
footer: {
|
||||||
height: 200,
|
height: 200,
|
||||||
paddingTop: 20,
|
paddingTop: 20,
|
||||||
|
|||||||
@@ -1,22 +1,28 @@
|
|||||||
import React from 'react'
|
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 {Link} from '../util/Link'
|
||||||
import {Text} from '../util/text/Text'
|
import {Text} from '../util/text/Text'
|
||||||
import {UserAvatar} from '../util/UserAvatar'
|
import {UserAvatar} from '../util/UserAvatar'
|
||||||
|
import * as Toast from '../util/Toast'
|
||||||
import {s} from '../../lib/styles'
|
import {s} from '../../lib/styles'
|
||||||
import {usePalette} from '../../lib/hooks/usePalette'
|
import {usePalette} from '../../lib/hooks/usePalette'
|
||||||
|
import {useStores} from '../../../state'
|
||||||
|
import * as apilib from '../../../state/lib/api'
|
||||||
|
|
||||||
export function ProfileCard({
|
export function ProfileCard({
|
||||||
handle,
|
handle,
|
||||||
displayName,
|
displayName,
|
||||||
avatar,
|
avatar,
|
||||||
description,
|
description,
|
||||||
|
isFollowedBy,
|
||||||
renderButton,
|
renderButton,
|
||||||
}: {
|
}: {
|
||||||
handle: string
|
handle: string
|
||||||
displayName?: string
|
displayName?: string
|
||||||
avatar?: string
|
avatar?: string
|
||||||
description?: string
|
description?: string
|
||||||
|
isFollowedBy?: boolean
|
||||||
renderButton?: () => JSX.Element
|
renderButton?: () => JSX.Element
|
||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
@@ -36,12 +42,19 @@ export function ProfileCard({
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.layoutContent}>
|
<View style={styles.layoutContent}>
|
||||||
<Text style={[s.bold, pal.text]} numberOfLines={1}>
|
<Text type="lg" style={[s.bold, pal.text]} numberOfLines={1}>
|
||||||
{displayName || handle}
|
{displayName || handle}
|
||||||
</Text>
|
</Text>
|
||||||
<Text type="sm" style={[pal.textLight]} numberOfLines={1}>
|
<Text type="md" style={[pal.textLight]} numberOfLines={1}>
|
||||||
@{handle}
|
@{handle}
|
||||||
</Text>
|
</Text>
|
||||||
|
{isFollowedBy && (
|
||||||
|
<View style={s.flexRow}>
|
||||||
|
<View style={[s.mt5, pal.btn, styles.pill]}>
|
||||||
|
<Text type="xs">Follows You</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
{renderButton ? (
|
{renderButton ? (
|
||||||
<View style={styles.layoutButton}>{renderButton()}</View>
|
<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({
|
const styles = StyleSheet.create({
|
||||||
outer: {
|
outer: {
|
||||||
borderTopWidth: 1,
|
borderTopWidth: 1,
|
||||||
@@ -93,4 +184,15 @@ const styles = StyleSheet.create({
|
|||||||
paddingRight: 10,
|
paddingRight: 10,
|
||||||
paddingBottom: 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,
|
UserFollowersViewModel,
|
||||||
FollowerItem,
|
FollowerItem,
|
||||||
} from '../../../state/models/user-followers-view'
|
} from '../../../state/models/user-followers-view'
|
||||||
import {Link} from '../util/Link'
|
|
||||||
import {Text} from '../util/text/Text'
|
|
||||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||||
import {UserAvatar} from '../util/UserAvatar'
|
import {ProfileCardWithFollowBtn} from './ProfileCard'
|
||||||
import {useStores} from '../../../state'
|
import {useStores} from '../../../state'
|
||||||
import {s} from '../../lib/styles'
|
|
||||||
import {usePalette} from '../../lib/hooks/usePalette'
|
|
||||||
|
|
||||||
export const ProfileFollowers = observer(function ProfileFollowers({
|
export const ProfileFollowers = observer(function ProfileFollowers({
|
||||||
name,
|
name,
|
||||||
@@ -62,7 +58,15 @@ export const ProfileFollowers = observer(function ProfileFollowers({
|
|||||||
// loaded
|
// loaded
|
||||||
// =
|
// =
|
||||||
const renderItem = ({item}: {item: FollowerItem}) => (
|
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 (
|
return (
|
||||||
<FlatList
|
<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({
|
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: {
|
footer: {
|
||||||
height: 200,
|
height: 200,
|
||||||
paddingTop: 20,
|
paddingTop: 20,
|
||||||
|
|||||||
@@ -5,13 +5,9 @@ import {
|
|||||||
UserFollowsViewModel,
|
UserFollowsViewModel,
|
||||||
FollowItem,
|
FollowItem,
|
||||||
} from '../../../state/models/user-follows-view'
|
} from '../../../state/models/user-follows-view'
|
||||||
import {Link} from '../util/Link'
|
|
||||||
import {Text} from '../util/text/Text'
|
|
||||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||||
import {UserAvatar} from '../util/UserAvatar'
|
import {ProfileCardWithFollowBtn} from './ProfileCard'
|
||||||
import {useStores} from '../../../state'
|
import {useStores} from '../../../state'
|
||||||
import {s} from '../../lib/styles'
|
|
||||||
import {usePalette} from '../../lib/hooks/usePalette'
|
|
||||||
|
|
||||||
export const ProfileFollows = observer(function ProfileFollows({
|
export const ProfileFollows = observer(function ProfileFollows({
|
||||||
name,
|
name,
|
||||||
@@ -62,7 +58,15 @@ export const ProfileFollows = observer(function ProfileFollows({
|
|||||||
// loaded
|
// loaded
|
||||||
// =
|
// =
|
||||||
const renderItem = ({item}: {item: FollowItem}) => (
|
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 (
|
return (
|
||||||
<FlatList
|
<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({
|
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: {
|
footer: {
|
||||||
height: 200,
|
height: 200,
|
||||||
paddingTop: 20,
|
paddingTop: 20,
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export const ProfileHeader = observer(function ProfileHeader({
|
|||||||
view?.toggleFollowing().then(
|
view?.toggleFollowing().then(
|
||||||
() => {
|
() => {
|
||||||
Toast.show(
|
Toast.show(
|
||||||
`${view.myState.follow ? 'Following' : 'No longer following'} ${
|
`${view.viewer.following ? 'Following' : 'No longer following'} ${
|
||||||
view.displayName || view.handle
|
view.displayName || view.handle
|
||||||
}`,
|
}`,
|
||||||
)
|
)
|
||||||
@@ -140,8 +140,8 @@ export const ProfileHeader = observer(function ProfileHeader({
|
|||||||
let dropdownItems: DropdownItem[] = [{label: 'Share', onPress: onPressShare}]
|
let dropdownItems: DropdownItem[] = [{label: 'Share', onPress: onPressShare}]
|
||||||
if (!isMe) {
|
if (!isMe) {
|
||||||
dropdownItems.push({
|
dropdownItems.push({
|
||||||
label: view.myState.muted ? 'Unmute Account' : 'Mute Account',
|
label: view.viewer.muted ? 'Unmute Account' : 'Mute Account',
|
||||||
onPress: view.myState.muted ? onPressUnmuteAccount : onPressMuteAccount,
|
onPress: view.viewer.muted ? onPressUnmuteAccount : onPressMuteAccount,
|
||||||
})
|
})
|
||||||
dropdownItems.push({
|
dropdownItems.push({
|
||||||
label: 'Report Account',
|
label: 'Report Account',
|
||||||
@@ -164,7 +164,7 @@ export const ProfileHeader = observer(function ProfileHeader({
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{view.myState.follow ? (
|
{store.me.follows.isFollowing(view.did) ? (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={onPressToggleFollow}
|
onPress={onPressToggleFollow}
|
||||||
style={[styles.btn, styles.mainBtn, pal.btn]}>
|
style={[styles.btn, styles.mainBtn, pal.btn]}>
|
||||||
@@ -213,6 +213,13 @@ export const ProfileHeader = observer(function ProfileHeader({
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.handleLine}>
|
<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>
|
<Text style={pal.textLight}>@{view.handle}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.metricsLine}>
|
<View style={styles.metricsLine}>
|
||||||
@@ -257,14 +264,14 @@ export const ProfileHeader = observer(function ProfileHeader({
|
|||||||
entities={view.descriptionEntities}
|
entities={view.descriptionEntities}
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
{view.myState.muted ? (
|
{view.viewer.muted ? (
|
||||||
<View style={[styles.detailLine, pal.btn, s.p5]}>
|
<View style={[styles.detailLine, pal.btn, s.p5]}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={['far', 'eye-slash']}
|
icon={['far', 'eye-slash']}
|
||||||
style={[pal.text, s.mr5]}
|
style={[pal.text, s.mr5]}
|
||||||
/>
|
/>
|
||||||
<Text type="md" style={[s.mr2, pal.text]}>
|
<Text type="md" style={[s.mr2, pal.text]}>
|
||||||
Account muted.
|
Account muted
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
@@ -371,6 +378,12 @@ const styles = StyleSheet.create({
|
|||||||
marginBottom: 5,
|
marginBottom: 5,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
pill: {
|
||||||
|
borderRadius: 4,
|
||||||
|
paddingHorizontal: 6,
|
||||||
|
paddingVertical: 2,
|
||||||
|
},
|
||||||
|
|
||||||
br40: {borderRadius: 40},
|
br40: {borderRadius: 40},
|
||||||
br50: {borderRadius: 50},
|
br50: {borderRadius: 50},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -19,10 +19,10 @@
|
|||||||
jsonpointer "^5.0.0"
|
jsonpointer "^5.0.0"
|
||||||
leven "^3.1.0"
|
leven "^3.1.0"
|
||||||
|
|
||||||
"@atproto/api@^0.1.1":
|
"@atproto/api@^0.1.2":
|
||||||
version "0.1.1"
|
version "0.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.1.1.tgz#0215dd80b98b8698f9e08932d4cdaf52676cb56c"
|
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.1.2.tgz#66102f9203ba499432bc5aeb30cd19313ab2e4fc"
|
||||||
integrity sha512-OMKLXuWxsdaHmmZ8XzwZ1uzWpq1sr9cuzZJLAbWJz0HRDQ3a0tg1uI80XvDYHFep0qgVoT2OKJNtB23Wp7nKoQ==
|
integrity sha512-lDcFGkrk0J7rkIPSie18xS7sO3IL6DsosX8GgoeqCNeVaDuphBRaFCcpBUWf0q4fHrpmdgghGo4ulefyKHTIFQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@atproto/xrpc" "*"
|
"@atproto/xrpc" "*"
|
||||||
typed-emitter "^2.1.0"
|
typed-emitter "^2.1.0"
|
||||||
@@ -90,10 +90,10 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@atproto/nsid/-/nsid-0.0.1.tgz#0cdc00cefe8f0b1385f352b9f57b3ad37fff09a4"
|
resolved "https://registry.yarnpkg.com/@atproto/nsid/-/nsid-0.0.1.tgz#0cdc00cefe8f0b1385f352b9f57b3ad37fff09a4"
|
||||||
integrity sha512-t5M6/CzWBVYoBbIvfKDpqPj/+ZmyoK9ydZSStcTXosJ27XXwOPhz0VDUGKK2SM9G5Y7TPes8S5KTAU0UdVYFCw==
|
integrity sha512-t5M6/CzWBVYoBbIvfKDpqPj/+ZmyoK9ydZSStcTXosJ27XXwOPhz0VDUGKK2SM9G5Y7TPes8S5KTAU0UdVYFCw==
|
||||||
|
|
||||||
"@atproto/pds@^0.0.2":
|
"@atproto/pds@^0.0.3":
|
||||||
version "0.0.2"
|
version "0.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.0.2.tgz#3665a24ce1f3a5696e46fea5448c5f849e38b7e0"
|
resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.0.3.tgz#118a1d51687664f085f8e1c19ae3ac1646dc69b2"
|
||||||
integrity sha512-TCTVKJWaUxF6EQJ6hobhO19bLaoW2Nk8xn/mySnQMN7ZHlYD9Hv+eRVY+PWLpnAuGetCkTbZAspg84eG2lKGTA==
|
integrity sha512-l5iGJNyQs73V/mQWkcg4NXNGbnfXfv+Yg3g8nqwtun409iAAeYZimA1Tt0JV/hyq+Oz3antG0VvLQQEtNVUpVQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@atproto/common" "*"
|
"@atproto/common" "*"
|
||||||
"@atproto/crypto" "*"
|
"@atproto/crypto" "*"
|
||||||
@@ -124,6 +124,7 @@
|
|||||||
pino "^8.6.1"
|
pino "^8.6.1"
|
||||||
pino-http "^8.2.1"
|
pino-http "^8.2.1"
|
||||||
sharp "^0.31.2"
|
sharp "^0.31.2"
|
||||||
|
typed-emitter "^2.1.0"
|
||||||
uint8arrays "3.0.0"
|
uint8arrays "3.0.0"
|
||||||
|
|
||||||
"@atproto/plc@*":
|
"@atproto/plc@*":
|
||||||
|
|||||||
Reference in New Issue
Block a user