Lex refactor (#362)

* Remove the hackcheck for upgrades

* Rename the PostEmbeds folder to match the codebase style

* Updates to latest lex refactor

* Update to use new bsky agent

* Update to use api package's richtext library

* Switch to upsertProfile

* Add TextEncoder/TextDecoder polyfill

* Add Intl.Segmenter polyfill

* Update composer to calculate lengths by grapheme

* Fix detox

* Fix login in e2e

* Create account e2e passing

* Implement an e2e mocking framework

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

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

* Add some tests and fix some bugs around profile editing

* Add shell tests

* Add home screen tests

* Add thread screen tests

* Add tests for other user profile screens

* Add search screen tests

* Implement profile imagery change tools and tests

* Update to new embed behaviors

* Add post tests

* Fix to profile-screen test

* Fix session resumption

* Update web composer to new api

* 1.11.0

* Fix pagination cursor parameters

* Add quote posts to notifications

* Fix embed layouts

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

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

* Update podfile.lock

* Improve post notfound UI (close #366)

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

View file

@ -3,7 +3,7 @@ import {Dim} from 'lib/media/manip'
export class ImageSizesCache {
sizes: Map<string, Dim> = new Map()
private activeRequests: Map<string, Promise<Dim>> = new Map()
activeRequests: Map<string, Promise<Dim>> = new Map()
constructor() {}

View file

@ -1,15 +1,12 @@
import {makeAutoObservable, runInAction} from 'mobx'
import {FollowRecord, AppBskyActorProfile, AppBskyActorRef} from '@atproto/api'
import {FollowRecord, AppBskyActorDefs} 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
type Profile = AppBskyActorDefs.ProfileViewBasic | AppBskyActorDefs.ProfileView
/**
* This model is used to maintain a synced local cache of the user's
@ -53,21 +50,21 @@ export class MyFollowsCache {
fetch = bundleAsync(async () => {
this.rootStore.log.debug('MyFollowsModel:fetch running full fetch')
let before
let rkeyStart
let records: FollowsListResponseRecord[] = []
do {
const res: FollowsListResponse =
await this.rootStore.api.app.bsky.graph.follow.list({
user: this.rootStore.me.did,
before,
await this.rootStore.agent.app.bsky.graph.follow.list({
repo: this.rootStore.me.did,
rkeyStart,
})
records = records.concat(res.records)
before = res.cursor
} while (typeof before !== 'undefined')
rkeyStart = res.cursor
} while (typeof rkeyStart !== 'undefined')
runInAction(() => {
this.followDidToRecordMap = {}
for (const record of records) {
this.followDidToRecordMap[record.value.subject.did] = record.uri
this.followDidToRecordMap[record.value.subject] = record.uri
}
this.lastSync = Date.now()
this.myDid = this.rootStore.me.did

View file

@ -1,15 +1,15 @@
import {AppBskyActorProfile, AppBskyActorRef} from '@atproto/api'
import {AppBskyActorDefs} from '@atproto/api'
import {makeAutoObservable, runInAction} from 'mobx'
import sampleSize from 'lodash.samplesize'
import {bundleAsync} from 'lib/async/bundle'
import {RootStoreModel} from '../root-store'
export type RefWithInfoAndFollowers = AppBskyActorRef.WithInfo & {
followers: AppBskyActorProfile.View[]
export type RefWithInfoAndFollowers = AppBskyActorDefs.ProfileViewBasic & {
followers: AppBskyActorDefs.ProfileView[]
}
export type ProfileViewFollows = AppBskyActorProfile.View & {
follows: AppBskyActorRef.WithInfo[]
export type ProfileViewFollows = AppBskyActorDefs.ProfileView & {
follows: AppBskyActorDefs.ProfileViewBasic[]
}
export class FoafsModel {
@ -51,14 +51,14 @@ export class FoafsModel {
this.popular.length = 0
// fetch their profiles
const profiles = await this.rootStore.api.app.bsky.actor.getProfiles({
const profiles = await this.rootStore.agent.getProfiles({
actors: this.sources,
})
// fetch their follows
const results = await Promise.allSettled(
this.sources.map(source =>
this.rootStore.api.app.bsky.graph.getFollows({user: source}),
this.rootStore.agent.getFollows({actor: source}),
),
)

View file

@ -1,5 +1,5 @@
import {makeAutoObservable, runInAction} from 'mobx'
import {AppBskyActorProfile as Profile} from '@atproto/api'
import {AppBskyActorDefs} from '@atproto/api'
import shuffle from 'lodash.shuffle'
import {RootStoreModel} from '../root-store'
import {cleanError} from 'lib/strings/errors'
@ -8,7 +8,9 @@ import {SUGGESTED_FOLLOWS} from 'lib/constants'
const PAGE_SIZE = 30
export type SuggestedActor = Profile.ViewBasic | Profile.View
export type SuggestedActor =
| AppBskyActorDefs.ProfileViewBasic
| AppBskyActorDefs.ProfileView
export class SuggestedActorsModel {
// state
@ -20,7 +22,7 @@ export class SuggestedActorsModel {
hasMore = true
loadMoreCursor?: string
private hardCodedSuggestions: SuggestedActor[] | undefined
hardCodedSuggestions: SuggestedActor[] | undefined
// data
suggestions: SuggestedActor[] = []
@ -82,7 +84,7 @@ export class SuggestedActorsModel {
this.loadMoreCursor = undefined
} else {
// pull from the PDS' algo
res = await this.rootStore.api.app.bsky.actor.getSuggestions({
res = await this.rootStore.agent.app.bsky.actor.getSuggestions({
limit: this.pageSize,
cursor: this.loadMoreCursor,
})
@ -104,7 +106,7 @@ export class SuggestedActorsModel {
}
})
private async fetchHardcodedSuggestions() {
async fetchHardcodedSuggestions() {
if (this.hardCodedSuggestions) {
return
}
@ -118,9 +120,9 @@ export class SuggestedActorsModel {
]
// fetch the profiles in chunks of 25 (the limit allowed by `getProfiles`)
let profiles: Profile.View[] = []
let profiles: AppBskyActorDefs.ProfileView[] = []
do {
const res = await this.rootStore.api.app.bsky.actor.getProfiles({
const res = await this.rootStore.agent.getProfiles({
actors: actors.splice(0, 25),
})
profiles = profiles.concat(res.data.profiles)
@ -152,13 +154,13 @@ export class SuggestedActorsModel {
// state transitions
// =
private _xLoading(isRefreshing = false) {
_xLoading(isRefreshing = false) {
this.isLoading = true
this.isRefreshing = isRefreshing
this.error = ''
}
private _xIdle(err?: any) {
_xIdle(err?: any) {
this.isLoading = false
this.isRefreshing = false
this.hasLoaded = true

View file

@ -1,32 +1,29 @@
import {makeAutoObservable, runInAction} from 'mobx'
import {
AppBskyFeedGetTimeline as GetTimeline,
AppBskyFeedFeedViewPost,
AppBskyFeedDefs,
AppBskyFeedPost,
AppBskyFeedGetAuthorFeed as GetAuthorFeed,
RichText,
} from '@atproto/api'
import AwaitLock from 'await-lock'
import {bundleAsync} from 'lib/async/bundle'
import sampleSize from 'lodash.samplesize'
type FeedViewPost = AppBskyFeedFeedViewPost.Main
type ReasonRepost = AppBskyFeedFeedViewPost.ReasonRepost
type PostView = AppBskyFeedPost.View
import {AtUri} from '../../third-party/uri'
import {RootStoreModel} from './root-store'
import * as apilib from 'lib/api/index'
import {cleanError} from 'lib/strings/errors'
import {RichText} from 'lib/strings/rich-text'
import {SUGGESTED_FOLLOWS} from 'lib/constants'
import {
getCombinedCursors,
getMultipleAuthorsPosts,
mergePosts,
} from 'lib/api/build-suggested-posts'
import {FeedTuner, FeedViewPostsSlice} from 'lib/api/feed-manip'
const PAGE_SIZE = 30
type FeedViewPost = AppBskyFeedDefs.FeedViewPost
type ReasonRepost = AppBskyFeedDefs.ReasonRepost
type PostView = AppBskyFeedDefs.PostView
const PAGE_SIZE = 30
let _idCounter = 0
export class FeedItemModel {
@ -51,11 +48,7 @@ export class FeedItemModel {
const valid = AppBskyFeedPost.validateRecord(this.post.record)
if (valid.success) {
this.postRecord = this.post.record
this.richText = new RichText(
this.postRecord.text,
this.postRecord.entities,
{cleanNewlines: true},
)
this.richText = new RichText(this.postRecord, {cleanNewlines: true})
} else {
rootStore.log.warn(
'Received an invalid app.bsky.feed.post record',
@ -82,7 +75,7 @@ export class FeedItemModel {
copyMetrics(v: FeedViewPost) {
this.post.replyCount = v.post.replyCount
this.post.repostCount = v.post.repostCount
this.post.upvoteCount = v.post.upvoteCount
this.post.likeCount = v.post.likeCount
this.post.viewer = v.post.viewer
}
@ -92,68 +85,43 @@ export class FeedItemModel {
}
}
async toggleUpvote() {
const wasUpvoted = !!this.post.viewer.upvote
const wasDownvoted = !!this.post.viewer.downvote
const res = await this.rootStore.api.app.bsky.feed.setVote({
subject: {
uri: this.post.uri,
cid: this.post.cid,
},
direction: wasUpvoted ? 'none' : 'up',
})
runInAction(() => {
if (wasDownvoted) {
this.post.downvoteCount--
}
if (wasUpvoted) {
this.post.upvoteCount--
} else {
this.post.upvoteCount++
}
this.post.viewer.upvote = res.data.upvote
this.post.viewer.downvote = res.data.downvote
})
}
async toggleDownvote() {
const wasUpvoted = !!this.post.viewer.upvote
const wasDownvoted = !!this.post.viewer.downvote
const res = await this.rootStore.api.app.bsky.feed.setVote({
subject: {
uri: this.post.uri,
cid: this.post.cid,
},
direction: wasDownvoted ? 'none' : 'down',
})
runInAction(() => {
if (wasUpvoted) {
this.post.upvoteCount--
}
if (wasDownvoted) {
this.post.downvoteCount--
} else {
this.post.downvoteCount++
}
this.post.viewer.upvote = res.data.upvote
this.post.viewer.downvote = res.data.downvote
})
async toggleLike() {
if (this.post.viewer?.like) {
await this.rootStore.agent.deleteLike(this.post.viewer.like)
runInAction(() => {
this.post.likeCount = this.post.likeCount || 0
this.post.viewer = this.post.viewer || {}
this.post.likeCount--
this.post.viewer.like = undefined
})
} else {
const res = await this.rootStore.agent.like(this.post.uri, this.post.cid)
runInAction(() => {
this.post.likeCount = this.post.likeCount || 0
this.post.viewer = this.post.viewer || {}
this.post.likeCount++
this.post.viewer.like = res.uri
})
}
}
async toggleRepost() {
if (this.post.viewer.repost) {
await apilib.unrepost(this.rootStore, this.post.viewer.repost)
if (this.post.viewer?.repost) {
await this.rootStore.agent.deleteRepost(this.post.viewer.repost)
runInAction(() => {
this.post.repostCount = this.post.repostCount || 0
this.post.viewer = this.post.viewer || {}
this.post.repostCount--
this.post.viewer.repost = undefined
})
} else {
const res = await apilib.repost(
this.rootStore,
const res = await this.rootStore.agent.repost(
this.post.uri,
this.post.cid,
)
runInAction(() => {
this.post.repostCount = this.post.repostCount || 0
this.post.viewer = this.post.viewer || {}
this.post.repostCount++
this.post.viewer.repost = res.uri
})
@ -161,10 +129,7 @@ export class FeedItemModel {
}
async delete() {
await this.rootStore.api.app.bsky.feed.post.delete({
did: this.post.author.did,
rkey: new AtUri(this.post.uri).rkey,
})
await this.rootStore.agent.deletePost(this.post.uri)
this.rootStore.emitPostDeleted(this.post.uri)
}
}
@ -250,7 +215,7 @@ export class FeedModel {
tuner = new FeedTuner()
// used to linearize async modifications to state
private lock = new AwaitLock()
lock = new AwaitLock()
// data
slices: FeedSliceModel[] = []
@ -291,8 +256,8 @@ export class FeedModel {
const params = this.params as GetAuthorFeed.QueryParams
const item = slice.rootItem
const isRepost =
item?.reasonRepost?.by?.handle === params.author ||
item?.reasonRepost?.by?.did === params.author
item?.reasonRepost?.by?.handle === params.actor ||
item?.reasonRepost?.by?.did === params.actor
return (
!item.reply || // not a reply
isRepost || // but allow if it's a repost
@ -338,7 +303,7 @@ export class FeedModel {
return this.setup()
}
private get feedTuners() {
get feedTuners() {
if (this.feedType === 'goodstuff') {
return [
FeedTuner.dedupReposts,
@ -406,7 +371,7 @@ export class FeedModel {
this._xLoading()
try {
const res = await this._getFeed({
before: this.loadMoreCursor,
cursor: this.loadMoreCursor,
limit: PAGE_SIZE,
})
await this._appendAll(res)
@ -439,7 +404,7 @@ export class FeedModel {
try {
do {
const res: GetTimeline.Response = await this._getFeed({
before: cursor,
cursor,
limit: Math.min(numToFetch, 100),
})
if (res.data.feed.length === 0) {
@ -478,14 +443,18 @@ export class FeedModel {
new FeedSliceModel(this.rootStore, `item-${_idCounter++}`, slice),
)
if (autoPrepend) {
this.slices = nextSlicesModels.concat(
this.slices.filter(slice1 =>
nextSlicesModels.find(slice2 => slice1.uri === slice2.uri),
),
)
this.setHasNewLatest(false)
runInAction(() => {
this.slices = nextSlicesModels.concat(
this.slices.filter(slice1 =>
nextSlicesModels.find(slice2 => slice1.uri === slice2.uri),
),
)
this.setHasNewLatest(false)
})
} else {
this.nextSlices = nextSlicesModels
runInAction(() => {
this.nextSlices = nextSlicesModels
})
this.setHasNewLatest(true)
}
} else {
@ -519,13 +488,13 @@ export class FeedModel {
// state transitions
// =
private _xLoading(isRefreshing = false) {
_xLoading(isRefreshing = false) {
this.isLoading = true
this.isRefreshing = isRefreshing
this.error = ''
}
private _xIdle(err?: any) {
_xIdle(err?: any) {
this.isLoading = false
this.isRefreshing = false
this.hasLoaded = true
@ -538,14 +507,12 @@ export class FeedModel {
// helper functions
// =
private async _replaceAll(
res: GetTimeline.Response | GetAuthorFeed.Response,
) {
async _replaceAll(res: GetTimeline.Response | GetAuthorFeed.Response) {
this.pollCursor = res.data.feed[0]?.post.uri
return this._appendAll(res, true)
}
private async _appendAll(
async _appendAll(
res: GetTimeline.Response | GetAuthorFeed.Response,
replace = false,
) {
@ -572,7 +539,7 @@ export class FeedModel {
})
}
private _updateAll(res: GetTimeline.Response | GetAuthorFeed.Response) {
_updateAll(res: GetTimeline.Response | GetAuthorFeed.Response) {
for (const item of res.data.feed) {
const existingSlice = this.slices.find(slice =>
slice.containsUri(item.post.uri),
@ -596,7 +563,7 @@ export class FeedModel {
const responses = await getMultipleAuthorsPosts(
this.rootStore,
sampleSize(SUGGESTED_FOLLOWS(String(this.rootStore.agent.service)), 20),
params.before,
params.cursor,
20,
)
const combinedCursor = getCombinedCursors(responses)
@ -611,9 +578,7 @@ export class FeedModel {
headers: lastHeaders,
}
} else if (this.feedType === 'home') {
return this.rootStore.api.app.bsky.feed.getTimeline(
params as GetTimeline.QueryParams,
)
return this.rootStore.agent.getTimeline(params as GetTimeline.QueryParams)
} else if (this.feedType === 'goodstuff') {
const res = await getGoodStuff(
this.rootStore.session.currentSession?.accessJwt || '',
@ -624,7 +589,7 @@ export class FeedModel {
)
return res
} else {
return this.rootStore.api.app.bsky.feed.getAuthorFeed(
return this.rootStore.agent.getAuthorFeed(
params as GetAuthorFeed.QueryParams,
)
}

View file

@ -1,6 +1,6 @@
import {makeAutoObservable, runInAction} from 'mobx'
import {AtUri} from '../../third-party/uri'
import {AppBskyFeedGetVotes as GetVotes} from '@atproto/api'
import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
import {RootStoreModel} from './root-store'
import {cleanError} from 'lib/strings/errors'
import {bundleAsync} from 'lib/async/bundle'
@ -8,24 +8,24 @@ import * as apilib from 'lib/api/index'
const PAGE_SIZE = 30
export type VoteItem = GetVotes.Vote
export type LikeItem = GetLikes.Like
export class VotesViewModel {
export class LikesViewModel {
// state
isLoading = false
isRefreshing = false
hasLoaded = false
error = ''
resolvedUri = ''
params: GetVotes.QueryParams
params: GetLikes.QueryParams
hasMore = true
loadMoreCursor?: string
// data
uri: string = ''
votes: VoteItem[] = []
likes: LikeItem[] = []
constructor(public rootStore: RootStoreModel, params: GetVotes.QueryParams) {
constructor(public rootStore: RootStoreModel, params: GetLikes.QueryParams) {
makeAutoObservable(
this,
{
@ -68,9 +68,9 @@ export class VotesViewModel {
const params = Object.assign({}, this.params, {
uri: this.resolvedUri,
limit: PAGE_SIZE,
before: replace ? undefined : this.loadMoreCursor,
cursor: replace ? undefined : this.loadMoreCursor,
})
const res = await this.rootStore.api.app.bsky.feed.getVotes(params)
const res = await this.rootStore.agent.getLikes(params)
if (replace) {
this._replaceAll(res)
} else {
@ -85,13 +85,13 @@ export class VotesViewModel {
// state transitions
// =
private _xLoading(isRefreshing = false) {
_xLoading(isRefreshing = false) {
this.isLoading = true
this.isRefreshing = isRefreshing
this.error = ''
}
private _xIdle(err?: any) {
_xIdle(err?: any) {
this.isLoading = false
this.isRefreshing = false
this.hasLoaded = true
@ -104,7 +104,7 @@ export class VotesViewModel {
// helper functions
// =
private async _resolveUri() {
async _resolveUri() {
const urip = new AtUri(this.params.uri)
if (!urip.host.startsWith('did:')) {
try {
@ -118,14 +118,14 @@ export class VotesViewModel {
})
}
private _replaceAll(res: GetVotes.Response) {
this.votes = []
_replaceAll(res: GetLikes.Response) {
this.likes = []
this._appendAll(res)
}
private _appendAll(res: GetVotes.Response) {
_appendAll(res: GetLikes.Response) {
this.loadMoreCursor = res.data.cursor
this.hasMore = !!this.loadMoreCursor
this.votes = this.votes.concat(res.data.votes)
this.likes = this.likes.concat(res.data.likes)
}
}

View file

@ -1,5 +1,5 @@
import {makeAutoObservable} from 'mobx'
import {XRPCError, XRPCInvalidResponseError} from '@atproto/xrpc'
// import {XRPCError, XRPCInvalidResponseError} from '@atproto/xrpc' TODO
const MAX_ENTRIES = 300
@ -32,7 +32,7 @@ export class LogModel {
makeAutoObservable(this)
}
private add(entry: LogEntry) {
add(entry: LogEntry) {
this.entries.push(entry)
while (this.entries.length > MAX_ENTRIES) {
this.entries = this.entries.slice(50)
@ -79,14 +79,14 @@ export class LogModel {
function detailsToStr(details?: any) {
if (details && typeof details !== 'string') {
if (
details instanceof XRPCInvalidResponseError ||
// details instanceof XRPCInvalidResponseError || TODO
details.constructor.name === 'XRPCInvalidResponseError'
) {
return `The server gave an ill-formatted response.\nMethod: ${
details.lexiconNsid
}.\nError: ${details.validationError.toString()}`
} else if (
details instanceof XRPCError ||
// details instanceof XRPCError || TODO
details.constructor.name === 'XRPCError'
) {
return `An XRPC error occurred.\nStatus: ${details.status}\nError: ${details.error}\nMessage: ${details.message}`

View file

@ -85,7 +85,7 @@ export class MeModel {
if (sess.hasSession) {
this.did = sess.currentSession?.did || ''
this.handle = sess.currentSession?.handle || ''
const profile = await this.rootStore.api.app.bsky.actor.getProfile({
const profile = await this.rootStore.agent.getProfile({
actor: this.did,
})
runInAction(() => {

View file

@ -1,11 +1,10 @@
import {makeAutoObservable, runInAction} from 'mobx'
import {
AppBskyNotificationList as ListNotifications,
AppBskyActorRef as ActorRef,
AppBskyNotificationListNotifications as ListNotifications,
AppBskyActorDefs,
AppBskyFeedPost,
AppBskyFeedRepost,
AppBskyFeedVote,
AppBskyGraphAssertion,
AppBskyFeedLike,
AppBskyGraphFollow,
} from '@atproto/api'
import AwaitLock from 'await-lock'
@ -28,8 +27,7 @@ export interface GroupedNotification extends ListNotifications.Notification {
type SupportedRecord =
| AppBskyFeedPost.Record
| AppBskyFeedRepost.Record
| AppBskyFeedVote.Record
| AppBskyGraphAssertion.Record
| AppBskyFeedLike.Record
| AppBskyGraphFollow.Record
export class NotificationsViewItemModel {
@ -39,11 +37,10 @@ export class NotificationsViewItemModel {
// data
uri: string = ''
cid: string = ''
author: ActorRef.WithInfo = {
author: AppBskyActorDefs.ProfileViewBasic = {
did: '',
handle: '',
avatar: '',
declaration: {cid: '', actorType: ''},
}
reason: string = ''
reasonSubject?: string
@ -86,8 +83,8 @@ export class NotificationsViewItemModel {
}
}
get isUpvote() {
return this.reason === 'vote'
get isLike() {
return this.reason === 'like'
}
get isRepost() {
@ -102,16 +99,22 @@ export class NotificationsViewItemModel {
return this.reason === 'reply'
}
get isQuote() {
return this.reason === 'quote'
}
get isFollow() {
return this.reason === 'follow'
}
get isAssertion() {
return this.reason === 'assertion'
}
get needsAdditionalData() {
if (this.isUpvote || this.isRepost || this.isReply || this.isMention) {
if (
this.isLike ||
this.isRepost ||
this.isReply ||
this.isQuote ||
this.isMention
) {
return !this.additionalPost
}
return false
@ -124,7 +127,7 @@ export class NotificationsViewItemModel {
const record = this.record
if (
AppBskyFeedRepost.isRecord(record) ||
AppBskyFeedVote.isRecord(record)
AppBskyFeedLike.isRecord(record)
) {
return record.subject.uri
}
@ -135,8 +138,7 @@ export class NotificationsViewItemModel {
for (const ns of [
AppBskyFeedPost,
AppBskyFeedRepost,
AppBskyFeedVote,
AppBskyGraphAssertion,
AppBskyFeedLike,
AppBskyGraphFollow,
]) {
if (ns.isRecord(v)) {
@ -163,9 +165,9 @@ export class NotificationsViewItemModel {
return
}
let postUri
if (this.isReply || this.isMention) {
if (this.isReply || this.isQuote || this.isMention) {
postUri = this.uri
} else if (this.isUpvote || this.isRepost) {
} else if (this.isLike || this.isRepost) {
postUri = this.subjectUri
}
if (postUri) {
@ -194,7 +196,7 @@ export class NotificationsViewModel {
loadMoreCursor?: string
// used to linearize async modifications to state
private lock = new AwaitLock()
lock = new AwaitLock()
// data
notifications: NotificationsViewItemModel[] = []
@ -266,7 +268,7 @@ export class NotificationsViewModel {
const params = Object.assign({}, this.params, {
limit: PAGE_SIZE,
})
const res = await this.rootStore.api.app.bsky.notification.list(params)
const res = await this.rootStore.agent.listNotifications(params)
await this._replaceAll(res)
this._xIdle()
} catch (e: any) {
@ -297,9 +299,9 @@ export class NotificationsViewModel {
try {
const params = Object.assign({}, this.params, {
limit: PAGE_SIZE,
before: this.loadMoreCursor,
cursor: this.loadMoreCursor,
})
const res = await this.rootStore.api.app.bsky.notification.list(params)
const res = await this.rootStore.agent.listNotifications(params)
await this._appendAll(res)
this._xIdle()
} catch (e: any) {
@ -325,7 +327,7 @@ export class NotificationsViewModel {
try {
this._xLoading()
try {
const res = await this.rootStore.api.app.bsky.notification.list({
const res = await this.rootStore.agent.listNotifications({
limit: PAGE_SIZE,
})
await this._prependAll(res)
@ -357,8 +359,8 @@ export class NotificationsViewModel {
try {
do {
const res: ListNotifications.Response =
await this.rootStore.api.app.bsky.notification.list({
before: cursor,
await this.rootStore.agent.listNotifications({
cursor,
limit: Math.min(numToFetch, 100),
})
if (res.data.notifications.length === 0) {
@ -390,7 +392,7 @@ export class NotificationsViewModel {
*/
loadUnreadCount = bundleAsync(async () => {
const old = this.unreadCount
const res = await this.rootStore.api.app.bsky.notification.getCount()
const res = await this.rootStore.agent.countUnreadNotifications()
runInAction(() => {
this.unreadCount = res.data.count
})
@ -408,9 +410,7 @@ export class NotificationsViewModel {
for (const notif of this.notifications) {
notif.isRead = true
}
await this.rootStore.api.app.bsky.notification.updateSeen({
seenAt: new Date().toISOString(),
})
await this.rootStore.agent.updateSeenNotifications()
} catch (e: any) {
this.rootStore.log.warn('Failed to update notifications read state', e)
}
@ -418,7 +418,7 @@ export class NotificationsViewModel {
async getNewMostRecent(): Promise<NotificationsViewItemModel | undefined> {
let old = this.mostRecentNotificationUri
const res = await this.rootStore.api.app.bsky.notification.list({
const res = await this.rootStore.agent.listNotifications({
limit: 1,
})
if (!res.data.notifications[0] || old === res.data.notifications[0].uri) {
@ -437,13 +437,13 @@ export class NotificationsViewModel {
// state transitions
// =
private _xLoading(isRefreshing = false) {
_xLoading(isRefreshing = false) {
this.isLoading = true
this.isRefreshing = isRefreshing
this.error = ''
}
private _xIdle(err?: any) {
_xIdle(err?: any) {
this.isLoading = false
this.isRefreshing = false
this.hasLoaded = true
@ -456,14 +456,14 @@ export class NotificationsViewModel {
// helper functions
// =
private async _replaceAll(res: ListNotifications.Response) {
async _replaceAll(res: ListNotifications.Response) {
if (res.data.notifications[0]) {
this.mostRecentNotificationUri = res.data.notifications[0].uri
}
return this._appendAll(res, true)
}
private async _appendAll(res: ListNotifications.Response, replace = false) {
async _appendAll(res: ListNotifications.Response, replace = false) {
this.loadMoreCursor = res.data.cursor
this.hasMore = !!this.loadMoreCursor
const promises = []
@ -494,7 +494,7 @@ export class NotificationsViewModel {
})
}
private async _prependAll(res: ListNotifications.Response) {
async _prependAll(res: ListNotifications.Response) {
const promises = []
const itemModels: NotificationsViewItemModel[] = []
const dedupedNotifs = res.data.notifications.filter(
@ -525,7 +525,7 @@ export class NotificationsViewModel {
})
}
private _updateAll(res: ListNotifications.Response) {
_updateAll(res: ListNotifications.Response) {
for (const item of res.data.notifications) {
const existingItem = this.notifications.find(item2 => isEq(item, item2))
if (existingItem) {

View file

@ -2,12 +2,13 @@ import {makeAutoObservable, runInAction} from 'mobx'
import {
AppBskyFeedGetPostThread as GetPostThread,
AppBskyFeedPost as FeedPost,
AppBskyFeedDefs,
RichText,
} from '@atproto/api'
import {AtUri} from '../../third-party/uri'
import {RootStoreModel} from './root-store'
import * as apilib from 'lib/api/index'
import {cleanError} from 'lib/strings/errors'
import {RichText} from 'lib/strings/rich-text'
function* reactKeyGenerator(): Generator<string> {
let counter = 0
@ -26,10 +27,10 @@ export class PostThreadViewPostModel {
_hasMore = false
// data
post: FeedPost.View
post: AppBskyFeedDefs.PostView
postRecord?: FeedPost.Record
parent?: PostThreadViewPostModel | GetPostThread.NotFoundPost
replies?: (PostThreadViewPostModel | GetPostThread.NotFoundPost)[]
parent?: PostThreadViewPostModel | AppBskyFeedDefs.NotFoundPost
replies?: (PostThreadViewPostModel | AppBskyFeedDefs.NotFoundPost)[]
richText?: RichText
get uri() {
@ -43,7 +44,7 @@ export class PostThreadViewPostModel {
constructor(
public rootStore: RootStoreModel,
reactKey: string,
v: GetPostThread.ThreadViewPost,
v: AppBskyFeedDefs.ThreadViewPost,
) {
this._reactKey = reactKey
this.post = v.post
@ -51,11 +52,7 @@ export class PostThreadViewPostModel {
const valid = FeedPost.validateRecord(this.post.record)
if (valid.success) {
this.postRecord = this.post.record
this.richText = new RichText(
this.postRecord.text,
this.postRecord.entities,
{cleanNewlines: true},
)
this.richText = new RichText(this.postRecord, {cleanNewlines: true})
} else {
rootStore.log.warn(
'Received an invalid app.bsky.feed.post record',
@ -74,14 +71,14 @@ export class PostThreadViewPostModel {
assignTreeModels(
keyGen: Generator<string>,
v: GetPostThread.ThreadViewPost,
v: AppBskyFeedDefs.ThreadViewPost,
higlightedPostUri: string,
includeParent = true,
includeChildren = true,
) {
// parents
if (includeParent && v.parent) {
if (GetPostThread.isThreadViewPost(v.parent)) {
if (AppBskyFeedDefs.isThreadViewPost(v.parent)) {
const parentModel = new PostThreadViewPostModel(
this.rootStore,
keyGen.next().value,
@ -100,7 +97,7 @@ export class PostThreadViewPostModel {
)
}
this.parent = parentModel
} else if (GetPostThread.isNotFoundPost(v.parent)) {
} else if (AppBskyFeedDefs.isNotFoundPost(v.parent)) {
this.parent = v.parent
}
}
@ -108,7 +105,7 @@ export class PostThreadViewPostModel {
if (includeChildren && v.replies) {
const replies = []
for (const item of v.replies) {
if (GetPostThread.isThreadViewPost(item)) {
if (AppBskyFeedDefs.isThreadViewPost(item)) {
const itemModel = new PostThreadViewPostModel(
this.rootStore,
keyGen.next().value,
@ -128,7 +125,7 @@ export class PostThreadViewPostModel {
)
}
replies.push(itemModel)
} else if (GetPostThread.isNotFoundPost(item)) {
} else if (AppBskyFeedDefs.isNotFoundPost(item)) {
replies.push(item)
}
}
@ -136,68 +133,43 @@ export class PostThreadViewPostModel {
}
}
async toggleUpvote() {
const wasUpvoted = !!this.post.viewer.upvote
const wasDownvoted = !!this.post.viewer.downvote
const res = await this.rootStore.api.app.bsky.feed.setVote({
subject: {
uri: this.post.uri,
cid: this.post.cid,
},
direction: wasUpvoted ? 'none' : 'up',
})
runInAction(() => {
if (wasDownvoted) {
this.post.downvoteCount--
}
if (wasUpvoted) {
this.post.upvoteCount--
} else {
this.post.upvoteCount++
}
this.post.viewer.upvote = res.data.upvote
this.post.viewer.downvote = res.data.downvote
})
}
async toggleDownvote() {
const wasUpvoted = !!this.post.viewer.upvote
const wasDownvoted = !!this.post.viewer.downvote
const res = await this.rootStore.api.app.bsky.feed.setVote({
subject: {
uri: this.post.uri,
cid: this.post.cid,
},
direction: wasDownvoted ? 'none' : 'down',
})
runInAction(() => {
if (wasUpvoted) {
this.post.upvoteCount--
}
if (wasDownvoted) {
this.post.downvoteCount--
} else {
this.post.downvoteCount++
}
this.post.viewer.upvote = res.data.upvote
this.post.viewer.downvote = res.data.downvote
})
async toggleLike() {
if (this.post.viewer?.like) {
await this.rootStore.agent.deleteLike(this.post.viewer.like)
runInAction(() => {
this.post.likeCount = this.post.likeCount || 0
this.post.viewer = this.post.viewer || {}
this.post.likeCount--
this.post.viewer.like = undefined
})
} else {
const res = await this.rootStore.agent.like(this.post.uri, this.post.cid)
runInAction(() => {
this.post.likeCount = this.post.likeCount || 0
this.post.viewer = this.post.viewer || {}
this.post.likeCount++
this.post.viewer.like = res.uri
})
}
}
async toggleRepost() {
if (this.post.viewer.repost) {
await apilib.unrepost(this.rootStore, this.post.viewer.repost)
if (this.post.viewer?.repost) {
await this.rootStore.agent.deleteRepost(this.post.viewer.repost)
runInAction(() => {
this.post.repostCount = this.post.repostCount || 0
this.post.viewer = this.post.viewer || {}
this.post.repostCount--
this.post.viewer.repost = undefined
})
} else {
const res = await apilib.repost(
this.rootStore,
const res = await this.rootStore.agent.repost(
this.post.uri,
this.post.cid,
)
runInAction(() => {
this.post.repostCount = this.post.repostCount || 0
this.post.viewer = this.post.viewer || {}
this.post.repostCount++
this.post.viewer.repost = res.uri
})
@ -205,10 +177,7 @@ export class PostThreadViewPostModel {
}
async delete() {
await this.rootStore.api.app.bsky.feed.post.delete({
did: this.post.author.did,
rkey: new AtUri(this.post.uri).rkey,
})
await this.rootStore.agent.deletePost(this.post.uri)
this.rootStore.emitPostDeleted(this.post.uri)
}
}
@ -301,14 +270,14 @@ export class PostThreadViewModel {
// state transitions
// =
private _xLoading(isRefreshing = false) {
_xLoading(isRefreshing = false) {
this.isLoading = true
this.isRefreshing = isRefreshing
this.error = ''
this.notFound = false
}
private _xIdle(err?: any) {
_xIdle(err?: any) {
this.isLoading = false
this.isRefreshing = false
this.hasLoaded = true
@ -322,7 +291,7 @@ export class PostThreadViewModel {
// loader functions
// =
private async _resolveUri() {
async _resolveUri() {
const urip = new AtUri(this.params.uri)
if (!urip.host.startsWith('did:')) {
try {
@ -336,10 +305,10 @@ export class PostThreadViewModel {
})
}
private async _load(isRefreshing = false) {
async _load(isRefreshing = false) {
this._xLoading(isRefreshing)
try {
const res = await this.rootStore.api.app.bsky.feed.getPostThread(
const res = await this.rootStore.agent.getPostThread(
Object.assign({}, this.params, {uri: this.resolvedUri}),
)
this._replaceAll(res)
@ -349,18 +318,18 @@ export class PostThreadViewModel {
}
}
private _replaceAll(res: GetPostThread.Response) {
_replaceAll(res: GetPostThread.Response) {
sortThread(res.data.thread)
const keyGen = reactKeyGenerator()
const thread = new PostThreadViewPostModel(
this.rootStore,
keyGen.next().value,
res.data.thread as GetPostThread.ThreadViewPost,
res.data.thread as AppBskyFeedDefs.ThreadViewPost,
)
thread._isHighlightedPost = true
thread.assignTreeModels(
keyGen,
res.data.thread as GetPostThread.ThreadViewPost,
res.data.thread as AppBskyFeedDefs.ThreadViewPost,
thread.uri,
)
this.thread = thread
@ -368,25 +337,25 @@ export class PostThreadViewModel {
}
type MaybePost =
| GetPostThread.ThreadViewPost
| GetPostThread.NotFoundPost
| AppBskyFeedDefs.ThreadViewPost
| AppBskyFeedDefs.NotFoundPost
| {[k: string]: unknown; $type: string}
function sortThread(post: MaybePost) {
if (post.notFound) {
return
}
post = post as GetPostThread.ThreadViewPost
post = post as AppBskyFeedDefs.ThreadViewPost
if (post.replies) {
post.replies.sort((a: MaybePost, b: MaybePost) => {
post = post as GetPostThread.ThreadViewPost
post = post as AppBskyFeedDefs.ThreadViewPost
if (a.notFound) {
return 1
}
if (b.notFound) {
return -1
}
a = a as GetPostThread.ThreadViewPost
b = b as GetPostThread.ThreadViewPost
a = a as AppBskyFeedDefs.ThreadViewPost
b = b as AppBskyFeedDefs.ThreadViewPost
const aIsByOp = a.post.author.did === post.post.author.did
const bIsByOp = b.post.author.did === post.post.author.did
if (aIsByOp && bIsByOp) {

View file

@ -58,12 +58,12 @@ export class PostModel implements RemoveIndex<Post.Record> {
// state transitions
// =
private _xLoading() {
_xLoading() {
this.isLoading = true
this.error = ''
}
private _xIdle(err?: any) {
_xIdle(err?: any) {
this.isLoading = false
this.hasLoaded = true
this.error = cleanError(err)
@ -75,12 +75,12 @@ export class PostModel implements RemoveIndex<Post.Record> {
// loader functions
// =
private async _load() {
async _load() {
this._xLoading()
try {
const urip = new AtUri(this.uri)
const res = await this.rootStore.api.app.bsky.feed.post.get({
user: urip.host,
const res = await this.rootStore.agent.getPost({
repo: urip.host,
rkey: urip.rkey,
})
// TODO
@ -94,7 +94,7 @@ export class PostModel implements RemoveIndex<Post.Record> {
}
}
private _replaceAll(res: Post.Record) {
_replaceAll(res: Post.Record) {
this.text = res.text
this.entities = res.entities
this.reply = res.reply

View file

@ -2,15 +2,12 @@ import {makeAutoObservable, runInAction} from 'mobx'
import {PickedMedia} from 'lib/media/picker'
import {
AppBskyActorGetProfile as GetProfile,
AppBskySystemDeclRef,
AppBskyActorUpdateProfile,
AppBskyActorProfile,
RichText,
} from '@atproto/api'
type DeclRef = AppBskySystemDeclRef.Main
import {extractEntities} from 'lib/strings/rich-text-detection'
import {RootStoreModel} from './root-store'
import * as apilib from 'lib/api/index'
import {cleanError} from 'lib/strings/errors'
import {RichText} from 'lib/strings/rich-text'
export const ACTOR_TYPE_USER = 'app.bsky.system.actorUser'
@ -35,22 +32,18 @@ export class ProfileViewModel {
// data
did: string = ''
handle: string = ''
declaration: DeclRef = {
cid: '',
actorType: '',
}
creator: string = ''
displayName?: string
description?: string
avatar?: string
banner?: string
displayName?: string = ''
description?: string = ''
avatar?: string = ''
banner?: string = ''
followersCount: number = 0
followsCount: number = 0
postsCount: number = 0
viewer = new ProfileViewViewerModel()
// added data
descriptionRichText?: RichText
descriptionRichText?: RichText = new RichText({text: ''})
constructor(
public rootStore: RootStoreModel,
@ -79,10 +72,6 @@ export class ProfileViewModel {
return this.hasLoaded && !this.hasContent
}
get isUser() {
return this.declaration.actorType === ACTOR_TYPE_USER
}
// public api
// =
@ -111,18 +100,14 @@ export class ProfileViewModel {
}
if (followUri) {
await apilib.unfollow(this.rootStore, followUri)
await this.rootStore.agent.deleteFollow(followUri)
runInAction(() => {
this.followersCount--
this.viewer.following = undefined
this.rootStore.me.follows.removeFollow(this.did)
})
} else {
const res = await apilib.follow(
this.rootStore,
this.did,
this.declaration.cid,
)
const res = await this.rootStore.agent.follow(this.did)
runInAction(() => {
this.followersCount++
this.viewer.following = res.uri
@ -132,49 +117,48 @@ export class ProfileViewModel {
}
async updateProfile(
updates: AppBskyActorUpdateProfile.InputSchema,
updates: AppBskyActorProfile.Record,
newUserAvatar: PickedMedia | undefined | null,
newUserBanner: PickedMedia | undefined | null,
) {
if (newUserAvatar) {
const res = await apilib.uploadBlob(
this.rootStore,
newUserAvatar.path,
newUserAvatar.mime,
)
updates.avatar = {
cid: res.data.cid,
mimeType: newUserAvatar.mime,
await this.rootStore.agent.upsertProfile(async existing => {
existing = existing || {}
existing.displayName = updates.displayName
existing.description = updates.description
if (newUserAvatar) {
const res = await apilib.uploadBlob(
this.rootStore,
newUserAvatar.path,
newUserAvatar.mime,
)
existing.avatar = res.data.blob
} else if (newUserAvatar === null) {
existing.avatar = undefined
}
} else if (newUserAvatar === null) {
updates.avatar = null
}
if (newUserBanner) {
const res = await apilib.uploadBlob(
this.rootStore,
newUserBanner.path,
newUserBanner.mime,
)
updates.banner = {
cid: res.data.cid,
mimeType: newUserBanner.mime,
if (newUserBanner) {
const res = await apilib.uploadBlob(
this.rootStore,
newUserBanner.path,
newUserBanner.mime,
)
existing.banner = res.data.blob
} else if (newUserBanner === null) {
existing.banner = undefined
}
} else if (newUserBanner === null) {
updates.banner = null
}
await this.rootStore.api.app.bsky.actor.updateProfile(updates)
return existing
})
await this.rootStore.me.load()
await this.refresh()
}
async muteAccount() {
await this.rootStore.api.app.bsky.graph.mute({user: this.did})
await this.rootStore.agent.mute(this.did)
this.viewer.muted = true
await this.refresh()
}
async unmuteAccount() {
await this.rootStore.api.app.bsky.graph.unmute({user: this.did})
await this.rootStore.agent.unmute(this.did)
this.viewer.muted = false
await this.refresh()
}
@ -182,13 +166,13 @@ export class ProfileViewModel {
// state transitions
// =
private _xLoading(isRefreshing = false) {
_xLoading(isRefreshing = false) {
this.isLoading = true
this.isRefreshing = isRefreshing
this.error = ''
}
private _xIdle(err?: any) {
_xIdle(err?: any) {
this.isLoading = false
this.isRefreshing = false
this.hasLoaded = true
@ -201,40 +185,40 @@ export class ProfileViewModel {
// loader functions
// =
private async _load(isRefreshing = false) {
async _load(isRefreshing = false) {
this._xLoading(isRefreshing)
try {
const res = await this.rootStore.api.app.bsky.actor.getProfile(
this.params,
)
const res = await this.rootStore.agent.getProfile(this.params)
this.rootStore.profiles.overwrite(this.params.actor, res) // cache invalidation
this._replaceAll(res)
await this._createRichText()
this._xIdle()
} catch (e: any) {
this._xIdle(e)
}
}
private _replaceAll(res: GetProfile.Response) {
_replaceAll(res: GetProfile.Response) {
this.did = res.data.did
this.handle = res.data.handle
Object.assign(this.declaration, res.data.declaration)
this.creator = res.data.creator
this.displayName = res.data.displayName
this.description = res.data.description
this.avatar = res.data.avatar
this.banner = res.data.banner
this.followersCount = res.data.followersCount
this.followsCount = res.data.followsCount
this.postsCount = res.data.postsCount
this.followersCount = res.data.followersCount || 0
this.followsCount = res.data.followsCount || 0
this.postsCount = res.data.postsCount || 0
if (res.data.viewer) {
Object.assign(this.viewer, res.data.viewer)
this.rootStore.me.follows.hydrate(this.did, res.data.viewer.following)
}
}
async _createRichText() {
this.descriptionRichText = new RichText(
this.description || '',
extractEntities(this.description || ''),
{text: this.description || ''},
{cleanNewlines: true},
)
await this.descriptionRichText.detectFacets(this.rootStore.agent)
}
}

View file

@ -31,7 +31,7 @@ export class ProfilesViewModel {
}
}
try {
const promise = this.rootStore.api.app.bsky.actor.getProfile({
const promise = this.rootStore.agent.getProfile({
actor: did,
})
this.cache.set(did, promise)

View file

@ -2,7 +2,7 @@ import {makeAutoObservable, runInAction} from 'mobx'
import {AtUri} from '../../third-party/uri'
import {
AppBskyFeedGetRepostedBy as GetRepostedBy,
AppBskyActorRef as ActorRef,
AppBskyActorDefs,
} from '@atproto/api'
import {RootStoreModel} from './root-store'
import {bundleAsync} from 'lib/async/bundle'
@ -11,7 +11,7 @@ import * as apilib from 'lib/api/index'
const PAGE_SIZE = 30
export type RepostedByItem = ActorRef.WithInfo
export type RepostedByItem = AppBskyActorDefs.ProfileViewBasic
export class RepostedByViewModel {
// state
@ -71,9 +71,9 @@ export class RepostedByViewModel {
const params = Object.assign({}, this.params, {
uri: this.resolvedUri,
limit: PAGE_SIZE,
before: replace ? undefined : this.loadMoreCursor,
cursor: replace ? undefined : this.loadMoreCursor,
})
const res = await this.rootStore.api.app.bsky.feed.getRepostedBy(params)
const res = await this.rootStore.agent.getRepostedBy(params)
if (replace) {
this._replaceAll(res)
} else {
@ -88,13 +88,13 @@ export class RepostedByViewModel {
// state transitions
// =
private _xLoading(isRefreshing = false) {
_xLoading(isRefreshing = false) {
this.isLoading = true
this.isRefreshing = isRefreshing
this.error = ''
}
private _xIdle(err?: any) {
_xIdle(err?: any) {
this.isLoading = false
this.isRefreshing = false
this.hasLoaded = true
@ -107,7 +107,7 @@ export class RepostedByViewModel {
// helper functions
// =
private async _resolveUri() {
async _resolveUri() {
const urip = new AtUri(this.params.uri)
if (!urip.host.startsWith('did:')) {
try {
@ -121,12 +121,12 @@ export class RepostedByViewModel {
})
}
private _replaceAll(res: GetRepostedBy.Response) {
_replaceAll(res: GetRepostedBy.Response) {
this.repostedBy = []
this._appendAll(res)
}
private _appendAll(res: GetRepostedBy.Response) {
_appendAll(res: GetRepostedBy.Response) {
this.loadMoreCursor = res.data.cursor
this.hasMore = !!this.loadMoreCursor
this.repostedBy = this.repostedBy.concat(res.data.repostedBy)

View file

@ -2,8 +2,8 @@
* The root store is the base of all modeled state.
*/
import {makeAutoObservable, runInAction} from 'mobx'
import {AtpAgent} from '@atproto/api'
import {makeAutoObservable} from 'mobx'
import {BskyAgent} from '@atproto/api'
import {createContext, useContext} from 'react'
import {DeviceEventEmitter, EmitterSubscription} from 'react-native'
import * as BgScheduler from 'lib/bg-scheduler'
@ -29,7 +29,7 @@ export const appInfo = z.object({
export type AppInfo = z.infer<typeof appInfo>
export class RootStoreModel {
agent: AtpAgent
agent: BskyAgent
appInfo?: AppInfo
log = new LogModel()
session = new SessionModel(this)
@ -40,41 +40,16 @@ export class RootStoreModel {
linkMetas = new LinkMetasCache(this)
imageSizes = new ImageSizesCache()
// HACK
// this flag is to track the lexicon breaking refactor
// it should be removed once we get that done
// -prf
hackUpgradeNeeded = false
async hackCheckIfUpgradeNeeded() {
try {
this.log.debug('hackCheckIfUpgradeNeeded()')
const res = await fetch('https://bsky.social/xrpc/app.bsky.feed.getLikes')
await res.text()
runInAction(() => {
this.hackUpgradeNeeded = res.status !== 501
this.log.debug(
`hackCheckIfUpgradeNeeded() said ${this.hackUpgradeNeeded}`,
)
})
} catch (e) {
this.log.error('Failed to hackCheckIfUpgradeNeeded', {e})
}
}
constructor(agent: AtpAgent) {
constructor(agent: BskyAgent) {
this.agent = agent
makeAutoObservable(this, {
api: false,
agent: false,
serialize: false,
hydrate: false,
})
this.initBgFetch()
}
get api() {
return this.agent.api
}
setAppInfo(info: AppInfo) {
this.appInfo = info
}
@ -131,7 +106,7 @@ export class RootStoreModel {
/**
* Called by the session model. Refreshes session-oriented state.
*/
async handleSessionChange(agent: AtpAgent) {
async handleSessionChange(agent: BskyAgent) {
this.log.debug('RootStoreModel:handleSessionChange')
this.agent = agent
this.me.clear()
@ -259,7 +234,7 @@ export class RootStoreModel {
async onBgFetch(taskId: string) {
this.log.debug(`Background fetch fired for task ${taskId}`)
if (this.session.hasSession) {
const res = await this.api.app.bsky.notification.getCount()
const res = await this.agent.countUnreadNotifications()
const hasNewNotifs = this.me.notifications.unreadCount !== res.data.count
this.emitUnreadNotifications(res.data.count)
this.log.debug(
@ -286,7 +261,7 @@ export class RootStoreModel {
}
const throwawayInst = new RootStoreModel(
new AtpAgent({service: 'http://localhost'}),
new BskyAgent({service: 'http://localhost'}),
) // this will be replaced by the loader, we just need to supply a value at init
const RootStoreContext = createContext<RootStoreModel>(throwawayInst)
export const RootStoreProvider = RootStoreContext.Provider

View file

@ -1,9 +1,9 @@
import {makeAutoObservable, runInAction} from 'mobx'
import {
AtpAgent,
BskyAgent,
AtpSessionEvent,
AtpSessionData,
ComAtprotoServerGetAccountsConfig as GetAccountsConfig,
ComAtprotoServerDescribeServer as DescribeServer,
} from '@atproto/api'
import normalizeUrl from 'normalize-url'
import {isObj, hasProp} from 'lib/type-guards'
@ -11,7 +11,7 @@ import {networkRetry} from 'lib/async/retry'
import {z} from 'zod'
import {RootStoreModel} from './root-store'
export type ServiceDescription = GetAccountsConfig.OutputSchema
export type ServiceDescription = DescribeServer.OutputSchema
export const activeSession = z.object({
service: z.string(),
@ -40,7 +40,7 @@ export class SessionModel {
// emergency log facility to help us track down this logout issue
// remove when resolved
// -prf
private _log(message: string, details?: Record<string, any>) {
_log(message: string, details?: Record<string, any>) {
details = details || {}
details.state = {
data: this.data,
@ -73,6 +73,7 @@ export class SessionModel {
rootStore: false,
serialize: false,
hydrate: false,
hasSession: false,
})
}
@ -154,7 +155,7 @@ export class SessionModel {
/**
* Sets the active session
*/
async setActiveSession(agent: AtpAgent, did: string) {
async setActiveSession(agent: BskyAgent, did: string) {
this._log('SessionModel:setActiveSession')
this.data = {
service: agent.service.toString(),
@ -166,7 +167,7 @@ export class SessionModel {
/**
* Upserts a session into the accounts
*/
private persistSession(
persistSession(
service: string,
did: string,
event: AtpSessionEvent,
@ -225,7 +226,7 @@ export class SessionModel {
/**
* Clears any session tokens from the accounts; used on logout.
*/
private clearSessionTokens() {
clearSessionTokens() {
this._log('SessionModel:clearSessionTokens')
this.accounts = this.accounts.map(acct => ({
service: acct.service,
@ -239,10 +240,8 @@ export class SessionModel {
/**
* Fetches additional information about an account on load.
*/
private async loadAccountInfo(agent: AtpAgent, did: string) {
const res = await agent.api.app.bsky.actor
.getProfile({actor: did})
.catch(_e => undefined)
async loadAccountInfo(agent: BskyAgent, did: string) {
const res = await agent.getProfile({actor: did}).catch(_e => undefined)
if (res) {
return {
dispayName: res.data.displayName,
@ -255,8 +254,8 @@ export class SessionModel {
* Helper to fetch the accounts config settings from an account.
*/
async describeService(service: string): Promise<ServiceDescription> {
const agent = new AtpAgent({service})
const res = await agent.api.com.atproto.server.getAccountsConfig({})
const agent = new BskyAgent({service})
const res = await agent.com.atproto.server.describeServer({})
return res.data
}
@ -272,7 +271,7 @@ export class SessionModel {
return false
}
const agent = new AtpAgent({
const agent = new BskyAgent({
service: account.service,
persistSession: (evt: AtpSessionEvent, sess?: AtpSessionData) => {
this.persistSession(account.service, account.did, evt, sess)
@ -321,7 +320,7 @@ export class SessionModel {
password: string
}) {
this._log('SessionModel:login')
const agent = new AtpAgent({service})
const agent = new BskyAgent({service})
await agent.login({identifier, password})
if (!agent.session) {
throw new Error('Failed to establish session')
@ -355,7 +354,7 @@ export class SessionModel {
inviteCode?: string
}) {
this._log('SessionModel:createAccount')
const agent = new AtpAgent({service})
const agent = new BskyAgent({service})
await agent.createAccount({
handle,
password,
@ -389,7 +388,7 @@ export class SessionModel {
// need to evaluate why deleting the session has caused errors at times
// -prf
/*if (this.hasSession) {
this.rootStore.api.com.atproto.session.delete().catch((e: any) => {
this.rootStore.agent.com.atproto.session.delete().catch((e: any) => {
this.rootStore.log.warn(
'(Minor issue) Failed to delete session on the server',
e,
@ -415,7 +414,7 @@ export class SessionModel {
if (!sess) {
return
}
const res = await this.rootStore.api.app.bsky.actor
const res = await this.rootStore.agent
.getProfile({actor: sess.did})
.catch(_e => undefined)
if (res?.success) {

View file

@ -72,12 +72,12 @@ export class SuggestedPostsView {
// state transitions
// =
private _xLoading() {
_xLoading() {
this.isLoading = true
this.error = ''
}
private _xIdle(err?: any) {
_xIdle(err?: any) {
this.isLoading = false
this.hasLoaded = true
this.error = cleanError(err)

View file

@ -2,7 +2,7 @@ import {makeAutoObservable} from 'mobx'
import {RootStoreModel} from '../root-store'
import {ServiceDescription} from '../session'
import {DEFAULT_SERVICE} from 'state/index'
import {ComAtprotoAccountCreate} from '@atproto/api'
import {ComAtprotoServerCreateAccount} from '@atproto/api'
import * as EmailValidator from 'email-validator'
import {createFullHandle} from 'lib/strings/handles'
import {cleanError} from 'lib/strings/errors'
@ -99,7 +99,7 @@ export class CreateAccountModel {
})
} catch (e: any) {
let errMsg = e.toString()
if (e instanceof ComAtprotoAccountCreate.InvalidInviteCodeError) {
if (e instanceof ComAtprotoServerCreateAccount.InvalidInviteCodeError) {
errMsg =
'Invite code not accepted. Check that you input it correctly and try again.'
}

View file

@ -40,7 +40,7 @@ export class ProfileUiModel {
)
this.profile = new ProfileViewModel(rootStore, {actor: params.user})
this.feed = new FeedModel(rootStore, 'author', {
author: params.user,
actor: params.user,
limit: 10,
})
}
@ -64,16 +64,8 @@ export class ProfileUiModel {
return this.profile.isRefreshing || this.currentView.isRefreshing
}
get isUser() {
return this.profile.isUser
}
get selectorItems() {
if (this.isUser) {
return USER_SELECTOR_ITEMS
} else {
return USER_SELECTOR_ITEMS
}
return USER_SELECTOR_ITEMS
}
get selectedView() {

View file

@ -1,6 +1,6 @@
import {makeAutoObservable, runInAction} from 'mobx'
import {searchProfiles, searchPosts} from 'lib/api/search'
import {AppBskyActorProfile as Profile} from '@atproto/api'
import {AppBskyActorDefs} from '@atproto/api'
import {RootStoreModel} from '../root-store'
export class SearchUIModel {
@ -8,7 +8,7 @@ export class SearchUIModel {
isProfilesLoading = false
query: string = ''
postUris: string[] = []
profiles: Profile.View[] = []
profiles: AppBskyActorDefs.ProfileView[] = []
constructor(public rootStore: RootStoreModel) {
makeAutoObservable(this)
@ -34,10 +34,10 @@ export class SearchUIModel {
this.isPostsLoading = false
})
let profiles: Profile.View[] = []
let profiles: AppBskyActorDefs.ProfileView[] = []
if (profilesSearch?.length) {
do {
const res = await this.rootStore.api.app.bsky.actor.getProfiles({
const res = await this.rootStore.agent.getProfiles({
actors: profilesSearch.splice(0, 25).map(p => p.did),
})
profiles = profiles.concat(res.data.profiles)

View file

@ -1,3 +1,4 @@
import {AppBskyEmbedRecord} from '@atproto/api'
import {RootStoreModel} from '../root-store'
import {makeAutoObservable} from 'mobx'
import {ProfileViewModel} from '../profile-view'
@ -111,6 +112,7 @@ export interface ComposerOptsQuote {
displayName?: string
avatar?: string
}
embeds?: AppBskyEmbedRecord.ViewRecord['embeds']
}
export interface ComposerOpts {
replyTo?: ComposerOptsPostRef

View file

@ -1,5 +1,5 @@
import {makeAutoObservable, runInAction} from 'mobx'
import {AppBskyActorRef} from '@atproto/api'
import {AppBskyActorDefs} from '@atproto/api'
import AwaitLock from 'await-lock'
import {RootStoreModel} from './root-store'
@ -11,8 +11,8 @@ export class UserAutocompleteViewModel {
lock = new AwaitLock()
// data
follows: AppBskyActorRef.WithInfo[] = []
searchRes: AppBskyActorRef.WithInfo[] = []
follows: AppBskyActorDefs.ProfileViewBasic[] = []
searchRes: AppBskyActorDefs.ProfileViewBasic[] = []
knownHandles: Set<string> = new Set()
constructor(public rootStore: RootStoreModel) {
@ -76,9 +76,9 @@ export class UserAutocompleteViewModel {
// internal
// =
private async _getFollows() {
const res = await this.rootStore.api.app.bsky.graph.getFollows({
user: this.rootStore.me.did || '',
async _getFollows() {
const res = await this.rootStore.agent.getFollows({
actor: this.rootStore.me.did || '',
})
runInAction(() => {
this.follows = res.data.follows
@ -88,13 +88,13 @@ export class UserAutocompleteViewModel {
})
}
private async _search() {
const res = await this.rootStore.api.app.bsky.actor.searchTypeahead({
async _search() {
const res = await this.rootStore.agent.searchActorsTypeahead({
term: this.prefix,
limit: 8,
})
runInAction(() => {
this.searchRes = res.data.users
this.searchRes = res.data.actors
for (const u of this.searchRes) {
this.knownHandles.add(u.handle)
}

View file

@ -1,7 +1,7 @@
import {makeAutoObservable} from 'mobx'
import {
AppBskyGraphGetFollowers as GetFollowers,
AppBskyActorRef as ActorRef,
AppBskyActorDefs as ActorDefs,
} from '@atproto/api'
import {RootStoreModel} from './root-store'
import {cleanError} from 'lib/strings/errors'
@ -9,7 +9,7 @@ import {bundleAsync} from 'lib/async/bundle'
const PAGE_SIZE = 30
export type FollowerItem = ActorRef.WithInfo
export type FollowerItem = ActorDefs.ProfileViewBasic
export class UserFollowersViewModel {
// state
@ -22,10 +22,9 @@ export class UserFollowersViewModel {
loadMoreCursor?: string
// data
subject: ActorRef.WithInfo = {
subject: ActorDefs.ProfileViewBasic = {
did: '',
handle: '',
declaration: {cid: '', actorType: ''},
}
followers: FollowerItem[] = []
@ -71,9 +70,9 @@ export class UserFollowersViewModel {
try {
const params = Object.assign({}, this.params, {
limit: PAGE_SIZE,
before: replace ? undefined : this.loadMoreCursor,
cursor: replace ? undefined : this.loadMoreCursor,
})
const res = await this.rootStore.api.app.bsky.graph.getFollowers(params)
const res = await this.rootStore.agent.getFollowers(params)
if (replace) {
this._replaceAll(res)
} else {
@ -88,13 +87,13 @@ export class UserFollowersViewModel {
// state transitions
// =
private _xLoading(isRefreshing = false) {
_xLoading(isRefreshing = false) {
this.isLoading = true
this.isRefreshing = isRefreshing
this.error = ''
}
private _xIdle(err?: any) {
_xIdle(err?: any) {
this.isLoading = false
this.isRefreshing = false
this.hasLoaded = true
@ -107,12 +106,12 @@ export class UserFollowersViewModel {
// helper functions
// =
private _replaceAll(res: GetFollowers.Response) {
_replaceAll(res: GetFollowers.Response) {
this.followers = []
this._appendAll(res)
}
private _appendAll(res: GetFollowers.Response) {
_appendAll(res: GetFollowers.Response) {
this.loadMoreCursor = res.data.cursor
this.hasMore = !!this.loadMoreCursor
this.followers = this.followers.concat(res.data.followers)

View file

@ -1,7 +1,7 @@
import {makeAutoObservable} from 'mobx'
import {
AppBskyGraphGetFollows as GetFollows,
AppBskyActorRef as ActorRef,
AppBskyActorDefs as ActorDefs,
} from '@atproto/api'
import {RootStoreModel} from './root-store'
import {cleanError} from 'lib/strings/errors'
@ -9,7 +9,7 @@ import {bundleAsync} from 'lib/async/bundle'
const PAGE_SIZE = 30
export type FollowItem = ActorRef.WithInfo
export type FollowItem = ActorDefs.ProfileViewBasic
export class UserFollowsViewModel {
// state
@ -22,10 +22,9 @@ export class UserFollowsViewModel {
loadMoreCursor?: string
// data
subject: ActorRef.WithInfo = {
subject: ActorDefs.ProfileViewBasic = {
did: '',
handle: '',
declaration: {cid: '', actorType: ''},
}
follows: FollowItem[] = []
@ -71,9 +70,9 @@ export class UserFollowsViewModel {
try {
const params = Object.assign({}, this.params, {
limit: PAGE_SIZE,
before: replace ? undefined : this.loadMoreCursor,
cursor: replace ? undefined : this.loadMoreCursor,
})
const res = await this.rootStore.api.app.bsky.graph.getFollows(params)
const res = await this.rootStore.agent.getFollows(params)
if (replace) {
this._replaceAll(res)
} else {
@ -88,13 +87,13 @@ export class UserFollowsViewModel {
// state transitions
// =
private _xLoading(isRefreshing = false) {
_xLoading(isRefreshing = false) {
this.isLoading = true
this.isRefreshing = isRefreshing
this.error = ''
}
private _xIdle(err?: any) {
_xIdle(err?: any) {
this.isLoading = false
this.isRefreshing = false
this.hasLoaded = true
@ -107,12 +106,12 @@ export class UserFollowsViewModel {
// helper functions
// =
private _replaceAll(res: GetFollows.Response) {
_replaceAll(res: GetFollows.Response) {
this.follows = []
this._appendAll(res)
}
private _appendAll(res: GetFollows.Response) {
_appendAll(res: GetFollows.Response) {
this.loadMoreCursor = res.data.cursor
this.hasMore = !!this.loadMoreCursor
this.follows = this.follows.concat(res.data.follows)