Update to use new getTimeline, getAuthorFeed, and getPostThread output models

zio/stable
Paul Frazee 2022-12-20 22:54:56 -06:00
parent 4f3bf401da
commit e7d971410f
17 changed files with 706 additions and 898 deletions

View File

@ -1,37 +1,28 @@
import {makeAutoObservable, runInAction} from 'mobx' import {makeAutoObservable, runInAction} from 'mobx'
import {Record as PostRecord} from '../../third-party/api/src/client/types/app/bsky/feed/post'
import * as GetTimeline from '../../third-party/api/src/client/types/app/bsky/feed/getTimeline' import * as GetTimeline from '../../third-party/api/src/client/types/app/bsky/feed/getTimeline'
import * as ActorRef from '../../third-party/api/src/client/types/app/bsky/actor/ref' import {
Main as FeedViewPost,
ReasonTrend,
ReasonRepost,
} from '../../third-party/api/src/client/types/app/bsky/feed/feedViewPost'
import {View as PostView} from '../../third-party/api/src/client/types/app/bsky/feed/post'
import * as GetAuthorFeed from '../../third-party/api/src/client/types/app/bsky/feed/getAuthorFeed' import * as GetAuthorFeed from '../../third-party/api/src/client/types/app/bsky/feed/getAuthorFeed'
import {PostThreadViewModel} from './post-thread-view'
import {AtUri} from '../../third-party/uri' import {AtUri} from '../../third-party/uri'
import {RootStoreModel} from './root-store' import {RootStoreModel} from './root-store'
import * as apilib from '../lib/api' import * as apilib from '../lib/api'
import {cleanError} from '../../lib/strings' import {cleanError} from '../../lib/strings'
import {isObj, hasProp} from '../lib/type-guards'
const PAGE_SIZE = 30 const PAGE_SIZE = 30
let _idCounter = 0 let _idCounter = 0
type FeedItem = GetTimeline.FeedItem | GetAuthorFeed.FeedItem type FeedViewPostWithThreadMeta = FeedViewPost & {
type FeedItemWithThreadMeta = FeedItem & {
_isThreadParent?: boolean _isThreadParent?: boolean
_isThreadChildElided?: boolean _isThreadChildElided?: boolean
_isThreadChild?: boolean _isThreadChild?: boolean
} }
export class FeedItemMyStateModel { export class FeedItemModel {
repost?: string
upvote?: string
downvote?: string
constructor() {
makeAutoObservable(this)
}
}
export class FeedItemModel implements GetTimeline.FeedItem {
// ui state // ui state
_reactKey: string = '' _reactKey: string = ''
_isThreadParent: boolean = false _isThreadParent: boolean = false
@ -39,153 +30,128 @@ export class FeedItemModel implements GetTimeline.FeedItem {
_isThreadChild: boolean = false _isThreadChild: boolean = false
// data // data
uri: string = '' post: PostView
cid: string = '' reply?: FeedViewPost['reply']
author: ActorRef.WithInfo = { replyParent?: FeedItemModel
did: '', reason?: FeedViewPost['reason']
handle: '',
displayName: '',
declaration: {cid: '', actorType: ''},
avatar: undefined,
}
repostedBy?: ActorRef.WithInfo
trendedBy?: ActorRef.WithInfo
record: Record<string, unknown> = {}
embed?: GetTimeline.FeedItem['embed']
replyCount: number = 0
repostCount: number = 0
upvoteCount: number = 0
downvoteCount: number = 0
indexedAt: string = ''
myState = new FeedItemMyStateModel()
// additional data
additionalParentPost?: PostThreadViewModel
constructor( constructor(
public rootStore: RootStoreModel, public rootStore: RootStoreModel,
reactKey: string, reactKey: string,
v: FeedItemWithThreadMeta, v: FeedViewPostWithThreadMeta,
) { ) {
makeAutoObservable(this, {rootStore: false})
this._reactKey = reactKey this._reactKey = reactKey
this.copy(v) this.post = v.post
this.reply = v.reply
if (v.reply?.parent) {
this.replyParent = new FeedItemModel(rootStore, '', {
post: v.reply.parent,
})
}
this.reason = v.reason
this._isThreadParent = v._isThreadParent || false this._isThreadParent = v._isThreadParent || false
this._isThreadChild = v._isThreadChild || false this._isThreadChild = v._isThreadChild || false
this._isThreadChildElided = v._isThreadChildElided || false this._isThreadChildElided = v._isThreadChildElided || false
makeAutoObservable(this, {rootStore: false})
} }
copy(v: GetTimeline.FeedItem | GetAuthorFeed.FeedItem) { copy(v: FeedViewPost) {
this.uri = v.uri this.post = v.post
this.cid = v.cid this.reply = v.reply
this.author = v.author if (v.reply?.parent) {
this.repostedBy = v.repostedBy this.replyParent = new FeedItemModel(this.rootStore, '', {
this.trendedBy = v.trendedBy post: v.reply.parent,
this.record = v.record })
this.embed = v.embed } else {
this.replyCount = v.replyCount this.replyParent = undefined
this.repostCount = v.repostCount }
this.upvoteCount = v.upvoteCount this.reason = v.reason
this.downvoteCount = v.downvoteCount }
this.indexedAt = v.indexedAt
if (v.myState) { get reasonRepost(): ReasonRepost | undefined {
this.myState.upvote = v.myState.upvote if (this.reason?.$type === 'app.bsky.feed.feedViewPost#reasonRepost') {
this.myState.downvote = v.myState.downvote return this.reason as ReasonRepost
this.myState.repost = v.myState.repost }
}
get reasonTrend(): ReasonTrend | undefined {
if (this.reason?.$type === 'app.bsky.feed.feedViewPost#reasonTrend') {
return this.reason as ReasonTrend
} }
} }
async toggleUpvote() { async toggleUpvote() {
const wasUpvoted = !!this.myState.upvote const wasUpvoted = !!this.post.viewer.upvote
const wasDownvoted = !!this.myState.downvote const wasDownvoted = !!this.post.viewer.downvote
const res = await this.rootStore.api.app.bsky.feed.setVote({ const res = await this.rootStore.api.app.bsky.feed.setVote({
subject: { subject: {
uri: this.uri, uri: this.post.uri,
cid: this.cid, cid: this.post.cid,
}, },
direction: wasUpvoted ? 'none' : 'up', direction: wasUpvoted ? 'none' : 'up',
}) })
runInAction(() => { runInAction(() => {
if (wasDownvoted) { if (wasDownvoted) {
this.downvoteCount-- this.post.downvoteCount--
} }
if (wasUpvoted) { if (wasUpvoted) {
this.upvoteCount-- this.post.upvoteCount--
} else { } else {
this.upvoteCount++ this.post.upvoteCount++
} }
this.myState.upvote = res.data.upvote this.post.viewer.upvote = res.data.upvote
this.myState.downvote = res.data.downvote this.post.viewer.downvote = res.data.downvote
}) })
} }
async toggleDownvote() { async toggleDownvote() {
const wasUpvoted = !!this.myState.upvote const wasUpvoted = !!this.post.viewer.upvote
const wasDownvoted = !!this.myState.downvote const wasDownvoted = !!this.post.viewer.downvote
const res = await this.rootStore.api.app.bsky.feed.setVote({ const res = await this.rootStore.api.app.bsky.feed.setVote({
subject: { subject: {
uri: this.uri, uri: this.post.uri,
cid: this.cid, cid: this.post.cid,
}, },
direction: wasDownvoted ? 'none' : 'down', direction: wasDownvoted ? 'none' : 'down',
}) })
runInAction(() => { runInAction(() => {
if (wasUpvoted) { if (wasUpvoted) {
this.upvoteCount-- this.post.upvoteCount--
} }
if (wasDownvoted) { if (wasDownvoted) {
this.downvoteCount-- this.post.downvoteCount--
} else { } else {
this.downvoteCount++ this.post.downvoteCount++
} }
this.myState.upvote = res.data.upvote this.post.viewer.upvote = res.data.upvote
this.myState.downvote = res.data.downvote this.post.viewer.downvote = res.data.downvote
}) })
} }
async toggleRepost() { async toggleRepost() {
if (this.myState.repost) { if (this.post.viewer.repost) {
await apilib.unrepost(this.rootStore, this.myState.repost) await apilib.unrepost(this.rootStore, this.post.viewer.repost)
runInAction(() => { runInAction(() => {
this.repostCount-- this.post.repostCount--
this.myState.repost = undefined this.post.viewer.repost = undefined
}) })
} else { } else {
const res = await apilib.repost(this.rootStore, this.uri, this.cid) const res = await apilib.repost(
this.rootStore,
this.post.uri,
this.post.cid,
)
runInAction(() => { runInAction(() => {
this.repostCount++ this.post.repostCount++
this.myState.repost = res.uri this.post.viewer.repost = res.uri
}) })
} }
} }
async delete() { async delete() {
await this.rootStore.api.app.bsky.feed.post.delete({ await this.rootStore.api.app.bsky.feed.post.delete({
did: this.author.did, did: this.post.author.did,
rkey: new AtUri(this.uri).rkey, rkey: new AtUri(this.post.uri).rkey,
})
}
get needsAdditionalData() {
if (
(this.record as PostRecord).reply?.parent?.uri &&
!this._isThreadChild
) {
return !this.additionalParentPost
}
return false
}
async fetchAdditionalData() {
if (!this.needsAdditionalData) {
return
}
this.additionalParentPost = new PostThreadViewModel(this.rootStore, {
uri: (this.record as PostRecord).reply?.parent.uri,
depth: 0,
})
await this.additionalParentPost.setup().catch(e => {
console.error('Failed to load post needed by notification', e)
}) })
} }
} }
@ -244,12 +210,11 @@ export class FeedModel {
get nonReplyFeed() { get nonReplyFeed() {
return this.feed.filter( return this.feed.filter(
post => item =>
!post.record.reply || // not a reply !item.reply || // not a reply
!!post.repostedBy || // or a repost ((item._isThreadParent || // but allow if it's a thread by the user
!!post.trendedBy || // or a trend item._isThreadChild) &&
post._isThreadParent || // but allow if it's a thread by the user item.reply?.root.author.did === item.post.author.did),
post._isThreadChild,
) )
} }
@ -335,7 +300,7 @@ export class FeedModel {
const res = await this._getFeed({limit: 1}) const res = await this._getFeed({limit: 1})
const currentLatestUri = this.pollCursor const currentLatestUri = this.pollCursor
const receivedLatestUri = res.data.feed[0] const receivedLatestUri = res.data.feed[0]
? res.data.feed[0].uri ? res.data.feed[0].post.uri
: undefined : undefined
const hasNewLatest = Boolean( const hasNewLatest = Boolean(
receivedLatestUri && receivedLatestUri &&
@ -435,7 +400,9 @@ export class FeedModel {
} }
this._updateAll(res) this._updateAll(res)
numToFetch -= res.data.feed.length numToFetch -= res.data.feed.length
cursor = this.feed[res.data.feed.length - 1].indexedAt cursor = this.feed[res.data.feed.length - 1]
? ts(this.feed[res.data.feed.length - 1])
: undefined
console.log(numToFetch, cursor, res.data.feed.length) console.log(numToFetch, cursor, res.data.feed.length)
} while (numToFetch > 0) } while (numToFetch > 0)
this._xIdle() this._xIdle()
@ -447,7 +414,7 @@ export class FeedModel {
private async _replaceAll( private async _replaceAll(
res: GetTimeline.Response | GetAuthorFeed.Response, res: GetTimeline.Response | GetAuthorFeed.Response,
) { ) {
this.pollCursor = res.data.feed[0]?.uri this.pollCursor = res.data.feed[0]?.post.uri
return this._appendAll(res, true) return this._appendAll(res, true)
} }
@ -460,7 +427,6 @@ export class FeedModel {
const reorgedFeed = preprocessFeed(res.data.feed) const reorgedFeed = preprocessFeed(res.data.feed)
const promises = []
const toAppend: FeedItemModel[] = [] const toAppend: FeedItemModel[] = []
for (const item of reorgedFeed) { for (const item of reorgedFeed) {
const itemModel = new FeedItemModel( const itemModel = new FeedItemModel(
@ -468,16 +434,8 @@ export class FeedModel {
`item-${_idCounter++}`, `item-${_idCounter++}`,
item, item,
) )
if (itemModel.needsAdditionalData) {
promises.push(
itemModel.fetchAdditionalData().catch(e => {
console.error('Failure during feed-view _appendAll()', e)
}),
)
}
toAppend.push(itemModel) toAppend.push(itemModel)
} }
await Promise.all(promises)
runInAction(() => { runInAction(() => {
if (replace) { if (replace) {
this.feed = toAppend this.feed = toAppend
@ -490,12 +448,11 @@ export class FeedModel {
private async _prependAll( private async _prependAll(
res: GetTimeline.Response | GetAuthorFeed.Response, res: GetTimeline.Response | GetAuthorFeed.Response,
) { ) {
this.pollCursor = res.data.feed[0]?.uri this.pollCursor = res.data.feed[0]?.post.uri
const promises = []
const toPrepend: FeedItemModel[] = [] const toPrepend: FeedItemModel[] = []
for (const item of res.data.feed) { for (const item of res.data.feed) {
if (this.feed.find(item2 => item2.uri === item.uri)) { if (this.feed.find(item2 => item2.post.uri === item.post.uri)) {
break // stop here - we've hit a post we already have break // stop here - we've hit a post we already have
} }
@ -504,16 +461,8 @@ export class FeedModel {
`item-${_idCounter++}`, `item-${_idCounter++}`,
item, item,
) )
if (itemModel.needsAdditionalData) {
promises.push(
itemModel.fetchAdditionalData().catch(e => {
console.error('Failure during feed-view _prependAll()', e)
}),
)
}
toPrepend.push(itemModel) toPrepend.push(itemModel)
} }
await Promise.all(promises)
runInAction(() => { runInAction(() => {
this.feed = toPrepend.concat(this.feed) this.feed = toPrepend.concat(this.feed)
}) })
@ -524,9 +473,10 @@ export class FeedModel {
const existingItem = this.feed.find( const existingItem = this.feed.find(
// HACK: need to find the reposts and trends item, so we have to check for that -prf // HACK: need to find the reposts and trends item, so we have to check for that -prf
item2 => item2 =>
item.uri === item2.uri && item.uri === item2.post.uri &&
item.repostedBy?.did === item2.repostedBy?.did && item.reason?.$trend === item2.reason?.$trend &&
item.trendedBy?.did === item2.trendedBy?.did, // @ts-ignore todo
item.reason?.by?.did === item2.reason?.by?.did,
) )
if (existingItem) { if (existingItem) {
existingItem.copy(item) existingItem.copy(item)
@ -554,17 +504,19 @@ interface Slice {
index: number index: number
length: number length: number
} }
function preprocessFeed(feed: FeedItem[]): FeedItemWithThreadMeta[] { function preprocessFeed(feed: FeedViewPost[]): FeedViewPostWithThreadMeta[] {
const reorg: FeedItemWithThreadMeta[] = [] const reorg: FeedViewPostWithThreadMeta[] = []
// phase one: identify threads and reorganize them into the feed so // phase one: identify threads and reorganize them into the feed so
// that they are in order and marked as part of a thread // that they are in order and marked as part of a thread
for (let i = feed.length - 1; i >= 0; i--) { for (let i = feed.length - 1; i >= 0; i--) {
const item = feed[i] as FeedItemWithThreadMeta const item = feed[i] as FeedViewPostWithThreadMeta
const selfReplyUri = getSelfReplyUri(item) const selfReplyUri = getSelfReplyUri(item)
if (selfReplyUri) { if (selfReplyUri) {
const parentIndex = reorg.findIndex(item2 => item2.uri === selfReplyUri) const parentIndex = reorg.findIndex(
item2 => item2.post.uri === selfReplyUri,
)
if (parentIndex !== -1 && !reorg[parentIndex]._isThreadParent) { if (parentIndex !== -1 && !reorg[parentIndex]._isThreadParent) {
reorg[parentIndex]._isThreadParent = true reorg[parentIndex]._isThreadParent = true
item._isThreadChild = true item._isThreadChild = true
@ -579,7 +531,7 @@ function preprocessFeed(feed: FeedItem[]): FeedItemWithThreadMeta[] {
let activeSlice = -1 let activeSlice = -1
let threadSlices: Slice[] = [] let threadSlices: Slice[] = []
for (let i = 0; i < reorg.length; i++) { for (let i = 0; i < reorg.length; i++) {
const item = reorg[i] as FeedItemWithThreadMeta const item = reorg[i] as FeedViewPostWithThreadMeta
if (activeSlice === -1) { if (activeSlice === -1) {
if (item._isThreadParent) { if (item._isThreadParent) {
activeSlice = i activeSlice = i
@ -602,14 +554,12 @@ function preprocessFeed(feed: FeedItem[]): FeedItemWithThreadMeta[] {
// phase three: reorder the feed so that the timestamp of the // phase three: reorder the feed so that the timestamp of the
// last post in a thread establishes its ordering // last post in a thread establishes its ordering
for (const slice of threadSlices) { for (const slice of threadSlices) {
const removed: FeedItemWithThreadMeta[] = reorg.splice( const removed: FeedViewPostWithThreadMeta[] = reorg.splice(
slice.index, slice.index,
slice.length, slice.length,
) )
const targetDate = new Date(removed[removed.length - 1].indexedAt) const targetDate = new Date(ts(removed[removed.length - 1]))
let newIndex = reorg.findIndex( let newIndex = reorg.findIndex(item => new Date(ts(item)) < targetDate)
item => new Date(item.indexedAt) < targetDate,
)
if (newIndex === -1) { if (newIndex === -1) {
newIndex = reorg.length newIndex = reorg.length
} }
@ -630,20 +580,17 @@ function preprocessFeed(feed: FeedItem[]): FeedItemWithThreadMeta[] {
return reorg return reorg
} }
function getSelfReplyUri( function getSelfReplyUri(item: FeedViewPost): string | undefined {
item: GetTimeline.FeedItem | GetAuthorFeed.FeedItem, return item.reply?.parent.author.did === item.post.author.did
): string | undefined { ? item.reply?.parent.uri
if ( : undefined
isObj(item.record) && }
hasProp(item.record, 'reply') &&
isObj(item.record.reply) && function ts(item: FeedViewPost | FeedItemModel): string {
hasProp(item.record.reply, 'parent') && if (item.reason?.indexedAt) {
isObj(item.record.reply.parent) && // @ts-ignore need better type checks
hasProp(item.record.reply.parent, 'uri') && return item.reason.indexedAt
typeof item.record.reply.parent.uri === 'string' }
) { console.log(item)
if (new AtUri(item.record.reply.parent.uri).host === item.author.did) { return item.post.indexedAt
return item.record.reply.parent.uri
}
}
} }

View File

@ -1,18 +1,14 @@
import {makeAutoObservable, runInAction} from 'mobx' import {makeAutoObservable, runInAction} from 'mobx'
import {AppBskyFeedGetPostThread as GetPostThread} from '../../third-party/api' import {AppBskyFeedGetPostThread as GPT} from '../../third-party/api'
import * as ActorRef from '../../third-party/api/src/client/types/app/bsky/actor/ref' import type * as GetPostThread from '../../third-party/api/src/client/types/app/bsky/feed/getPostThread'
import {AtUri} from '../../third-party/uri' import {AtUri} from '../../third-party/uri'
import _omit from 'lodash.omit'
import {RootStoreModel} from './root-store' import {RootStoreModel} from './root-store'
import * as apilib from '../lib/api' import * as apilib from '../lib/api'
type MaybePost = interface UnknownPost {
| GetPostThread.Post $type: string
| GetPostThread.NotFoundPost [k: string]: unknown
| { }
$type: string
[k: string]: unknown
}
function* reactKeyGenerator(): Generator<string> { function* reactKeyGenerator(): Generator<string> {
let counter = 0 let counter = 0
@ -33,17 +29,18 @@ interface OriginalRecord {
text: string text: string
} }
export class PostThreadViewPostMyStateModel { function isThreadViewPost(
repost?: string v: GetPostThread.ThreadViewPost | GetPostThread.NotFoundPost | UnknownPost,
upvote?: string ): v is GetPostThread.ThreadViewPost {
downvote?: string return v.$type === 'app.bksy.feed.getPostThread#threadViewPost'
}
constructor() { function isNotFoundPost(
makeAutoObservable(this) v: GetPostThread.ThreadViewPost | GetPostThread.NotFoundPost | UnknownPost,
} ): v is GetPostThread.NotFoundPost {
return v.$type === 'app.bsky.feed.getPostThread#notFoundPost'
} }
export class PostThreadViewPostModel implements GetPostThread.Post { export class PostThreadViewPostModel {
// ui state // ui state
_reactKey: string = '' _reactKey: string = ''
_depth = 0 _depth = 0
@ -51,24 +48,9 @@ export class PostThreadViewPostModel implements GetPostThread.Post {
_hasMore = false _hasMore = false
// data // data
$type: string = '' post: GetPostThread.ThreadViewPost['post']
uri: string = '' parent?: PostThreadViewPostModel | GetPostThread.NotFoundPost
cid: string = '' replies?: (PostThreadViewPostModel | GetPostThread.NotFoundPost)[]
author: ActorRef.WithInfo = {
did: '',
handle: '',
declaration: {cid: '', actorType: ''},
}
record: Record<string, unknown> = {}
embed?: GetPostThread.Post['embed'] = undefined
parent?: PostThreadViewPostModel
replyCount: number = 0
replies?: PostThreadViewPostModel[]
repostCount: number = 0
upvoteCount: number = 0
downvoteCount: number = 0
indexedAt: string = ''
myState = new PostThreadViewPostMyStateModel()
// added data // added data
replyingTo?: ReplyingTo replyingTo?: ReplyingTo
@ -76,45 +58,49 @@ export class PostThreadViewPostModel implements GetPostThread.Post {
constructor( constructor(
public rootStore: RootStoreModel, public rootStore: RootStoreModel,
reactKey: string, reactKey: string,
v?: GetPostThread.Post, v: GetPostThread.ThreadViewPost,
) { ) {
makeAutoObservable(this, {rootStore: false})
this._reactKey = reactKey this._reactKey = reactKey
if (v) { this.post = v.post
Object.assign(this, _omit(v, 'parent', 'replies', 'myState')) // replies and parent are handled via assignTreeModels // replies and parent are handled via assignTreeModels
if (v.myState) { makeAutoObservable(this, {rootStore: false})
Object.assign(this.myState, v.myState)
}
}
} }
assignTreeModels( assignTreeModels(
keyGen: Generator<string>, keyGen: Generator<string>,
v: GetPostThread.Post, v: GetPostThread.ThreadViewPost,
includeParent = true, includeParent = true,
includeChildren = true, includeChildren = true,
isFirstChild = true, isFirstChild = true,
) { ) {
// parents // parents
if (includeParent && v.parent) { if (includeParent && v.parent) {
// TODO: validate .record if (isThreadViewPost(v.parent)) {
const parentModel = new PostThreadViewPostModel( const parentModel = new PostThreadViewPostModel(
this.rootStore, this.rootStore,
keyGen.next().value, keyGen.next().value,
v.parent, v.parent,
) )
parentModel._depth = this._depth - 1 parentModel._depth = this._depth - 1
if (v.parent.parent) { if (v.parent.parent) {
parentModel.assignTreeModels(keyGen, v.parent, true, false) parentModel.assignTreeModels(keyGen, v.parent, true, false)
}
this.parent = parentModel
} else if (isNotFoundPost(v.parent)) {
this.parent = v.parent
} }
this.parent = parentModel
} }
if (!includeParent && v.parent?.author.handle && !isFirstChild) { if (
!includeParent &&
v.parent &&
isThreadViewPost(v.parent) &&
!isFirstChild
) {
this.replyingTo = { this.replyingTo = {
author: { author: {
handle: v.parent.author.handle, handle: v.parent.post.author.handle,
displayName: v.parent.author.displayName, displayName: v.parent.post.author.displayName,
avatar: v.parent.author.avatar, avatar: v.parent.post.author.avatar,
}, },
text: (v.parent.record as OriginalRecord).text, text: (v.parent.record as OriginalRecord).text,
} }
@ -124,97 +110,104 @@ export class PostThreadViewPostModel implements GetPostThread.Post {
const replies = [] const replies = []
let isChildFirstChild = true let isChildFirstChild = true
for (const item of v.replies) { for (const item of v.replies) {
// TODO: validate .record if (isThreadViewPost(item)) {
const itemModel = new PostThreadViewPostModel( const itemModel = new PostThreadViewPostModel(
this.rootStore, this.rootStore,
keyGen.next().value, keyGen.next().value,
item,
)
itemModel._depth = this._depth + 1
if (item.replies) {
itemModel.assignTreeModels(
keyGen,
item, item,
false,
true,
isChildFirstChild,
) )
itemModel._depth = this._depth + 1
if (item.replies) {
itemModel.assignTreeModels(
keyGen,
item,
false,
true,
isChildFirstChild,
)
}
isChildFirstChild = false
replies.push(itemModel)
} else if (isNotFoundPost(item)) {
replies.push(item)
} }
isChildFirstChild = false
replies.push(itemModel)
} }
this.replies = replies this.replies = replies
} }
} }
async toggleUpvote() { async toggleUpvote() {
const wasUpvoted = !!this.myState.upvote const wasUpvoted = !!this.post.viewer.upvote
const wasDownvoted = !!this.myState.downvote const wasDownvoted = !!this.post.viewer.downvote
const res = await this.rootStore.api.app.bsky.feed.setVote({ const res = await this.rootStore.api.app.bsky.feed.setVote({
subject: { subject: {
uri: this.uri, uri: this.post.uri,
cid: this.cid, cid: this.post.cid,
}, },
direction: wasUpvoted ? 'none' : 'up', direction: wasUpvoted ? 'none' : 'up',
}) })
runInAction(() => { runInAction(() => {
if (wasDownvoted) { if (wasDownvoted) {
this.downvoteCount-- this.post.downvoteCount--
} }
if (wasUpvoted) { if (wasUpvoted) {
this.upvoteCount-- this.post.upvoteCount--
} else { } else {
this.upvoteCount++ this.post.upvoteCount++
} }
this.myState.upvote = res.data.upvote this.post.viewer.upvote = res.data.upvote
this.myState.downvote = res.data.downvote this.post.viewer.downvote = res.data.downvote
}) })
} }
async toggleDownvote() { async toggleDownvote() {
const wasUpvoted = !!this.myState.upvote const wasUpvoted = !!this.post.viewer.upvote
const wasDownvoted = !!this.myState.downvote const wasDownvoted = !!this.post.viewer.downvote
const res = await this.rootStore.api.app.bsky.feed.setVote({ const res = await this.rootStore.api.app.bsky.feed.setVote({
subject: { subject: {
uri: this.uri, uri: this.post.uri,
cid: this.cid, cid: this.post.cid,
}, },
direction: wasDownvoted ? 'none' : 'down', direction: wasDownvoted ? 'none' : 'down',
}) })
runInAction(() => { runInAction(() => {
if (wasUpvoted) { if (wasUpvoted) {
this.upvoteCount-- this.post.upvoteCount--
} }
if (wasDownvoted) { if (wasDownvoted) {
this.downvoteCount-- this.post.downvoteCount--
} else { } else {
this.downvoteCount++ this.post.downvoteCount++
} }
this.myState.upvote = res.data.upvote this.post.viewer.upvote = res.data.upvote
this.myState.downvote = res.data.downvote this.post.viewer.downvote = res.data.downvote
}) })
} }
async toggleRepost() { async toggleRepost() {
if (this.myState.repost) { if (this.post.viewer.repost) {
await apilib.unrepost(this.rootStore, this.myState.repost) await apilib.unrepost(this.rootStore, this.post.viewer.repost)
runInAction(() => { runInAction(() => {
this.repostCount-- this.post.repostCount--
this.myState.repost = undefined this.post.viewer.repost = undefined
}) })
} else { } else {
const res = await apilib.repost(this.rootStore, this.uri, this.cid) const res = await apilib.repost(
this.rootStore,
this.post.uri,
this.post.cid,
)
runInAction(() => { runInAction(() => {
this.repostCount++ this.post.repostCount++
this.myState.repost = res.uri this.post.viewer.repost = res.uri
}) })
} }
} }
async delete() { async delete() {
await this.rootStore.api.app.bsky.feed.post.delete({ await this.rootStore.api.app.bsky.feed.post.delete({
did: this.author.did, did: this.post.author.did,
rkey: new AtUri(this.uri).rkey, rkey: new AtUri(this.post.uri).rkey,
}) })
} }
} }
@ -304,7 +297,7 @@ export class PostThreadViewModel {
this.isRefreshing = false this.isRefreshing = false
this.hasLoaded = true this.hasLoaded = true
this.error = err ? err.toString() : '' this.error = err ? err.toString() : ''
this.notFound = err instanceof GetPostThread.NotFoundError this.notFound = err instanceof GPT.NotFoundError
} }
// loader functions // loader functions
@ -339,19 +332,24 @@ export class PostThreadViewModel {
private _replaceAll(res: GetPostThread.Response) { private _replaceAll(res: GetPostThread.Response) {
// TODO: validate .record // TODO: validate .record
sortThread(res.data.thread) // sortThread(res.data.thread) TODO needed?
const keyGen = reactKeyGenerator() const keyGen = reactKeyGenerator()
const thread = new PostThreadViewPostModel( const thread = new PostThreadViewPostModel(
this.rootStore, this.rootStore,
keyGen.next().value, keyGen.next().value,
res.data.thread as GetPostThread.Post, res.data.thread as GetPostThread.ThreadViewPost,
) )
thread._isHighlightedPost = true thread._isHighlightedPost = true
thread.assignTreeModels(keyGen, res.data.thread as GetPostThread.Post) thread.assignTreeModels(
keyGen,
res.data.thread as GetPostThread.ThreadViewPost,
)
this.thread = thread this.thread = thread
} }
} }
/*
TODO needed?
function sortThread(post: MaybePost) { function sortThread(post: MaybePost) {
if (post.notFound) { if (post.notFound) {
return return
@ -382,3 +380,4 @@ function sortThread(post: MaybePost) {
post.replies.forEach(reply => sortThread(reply)) post.replies.forEach(reply => sortThread(reply))
} }
} }
*/

View File

@ -40,6 +40,7 @@ __export(src_exports, {
AppBskyActorUpdateProfile: () => updateProfile_exports, AppBskyActorUpdateProfile: () => updateProfile_exports,
AppBskyEmbedExternal: () => external_exports, AppBskyEmbedExternal: () => external_exports,
AppBskyEmbedImages: () => images_exports, AppBskyEmbedImages: () => images_exports,
AppBskyFeedFeedViewPost: () => feedViewPost_exports,
AppBskyFeedGetAuthorFeed: () => getAuthorFeed_exports, AppBskyFeedGetAuthorFeed: () => getAuthorFeed_exports,
AppBskyFeedGetPostThread: () => getPostThread_exports, AppBskyFeedGetPostThread: () => getPostThread_exports,
AppBskyFeedGetRepostedBy: () => getRepostedBy_exports, AppBskyFeedGetRepostedBy: () => getRepostedBy_exports,
@ -5756,6 +5757,73 @@ var schemaDict = {
} }
} }
}, },
AppBskyFeedFeedViewPost: {
lexicon: 1,
id: "app.bsky.feed.feedViewPost",
defs: {
main: {
type: "object",
required: ["post"],
properties: {
post: {
type: "ref",
ref: "lex:app.bsky.feed.post#view"
},
reply: {
type: "ref",
ref: "lex:app.bsky.feed.feedViewPost#replyRef"
},
reason: {
type: "union",
refs: [
"lex:app.bsky.feed.feedViewPost#reasonTrend",
"lex:app.bsky.feed.feedViewPost#reasonRepost"
]
}
}
},
replyRef: {
type: "object",
required: ["root", "parent"],
properties: {
root: {
type: "ref",
ref: "lex:app.bsky.feed.post#view"
},
parent: {
type: "ref",
ref: "lex:app.bsky.feed.post#view"
}
}
},
reasonTrend: {
type: "object",
required: ["by", "indexedAt"],
properties: {
by: {
type: "ref",
ref: "lex:app.bsky.actor.ref#withInfo"
},
indexedAt: {
type: "datetime"
}
}
},
reasonRepost: {
type: "object",
required: ["by", "indexedAt"],
properties: {
by: {
type: "ref",
ref: "lex:app.bsky.actor.ref#withInfo"
},
indexedAt: {
type: "datetime"
}
}
}
}
},
AppBskyFeedGetAuthorFeed: { AppBskyFeedGetAuthorFeed: {
lexicon: 1, lexicon: 1,
id: "app.bsky.feed.getAuthorFeed", id: "app.bsky.feed.getAuthorFeed",
@ -5794,89 +5862,12 @@ var schemaDict = {
type: "array", type: "array",
items: { items: {
type: "ref", type: "ref",
ref: "lex:app.bsky.feed.getAuthorFeed#feedItem" ref: "lex:app.bsky.feed.feedViewPost"
} }
} }
} }
} }
} }
},
feedItem: {
type: "object",
required: [
"uri",
"cid",
"author",
"record",
"replyCount",
"repostCount",
"upvoteCount",
"downvoteCount",
"indexedAt"
],
properties: {
uri: {
type: "string"
},
cid: {
type: "string"
},
author: {
type: "ref",
ref: "lex:app.bsky.actor.ref#withInfo"
},
trendedBy: {
type: "ref",
ref: "lex:app.bsky.actor.ref#withInfo"
},
repostedBy: {
type: "ref",
ref: "lex:app.bsky.actor.ref#withInfo"
},
record: {
type: "unknown"
},
embed: {
type: "union",
refs: [
"lex:app.bsky.embed.images#presented",
"lex:app.bsky.embed.external#presented"
]
},
replyCount: {
type: "integer"
},
repostCount: {
type: "integer"
},
upvoteCount: {
type: "integer"
},
downvoteCount: {
type: "integer"
},
indexedAt: {
type: "datetime"
},
myState: {
type: "ref",
ref: "lex:app.bsky.feed.getAuthorFeed#myState"
}
}
},
myState: {
type: "object",
properties: {
repost: {
type: "string"
},
upvote: {
type: "string"
},
downvote: {
type: "string"
}
}
} }
} }
}, },
@ -5907,7 +5898,7 @@ var schemaDict = {
thread: { thread: {
type: "union", type: "union",
refs: [ refs: [
"lex:app.bsky.feed.getPostThread#post", "lex:app.bsky.feed.getPostThread#threadViewPost",
"lex:app.bsky.feed.getPostThread#notFoundPost" "lex:app.bsky.feed.getPostThread#notFoundPost"
] ]
} }
@ -5920,75 +5911,30 @@ var schemaDict = {
} }
] ]
}, },
post: { threadViewPost: {
type: "object", type: "object",
required: [ required: ["post"],
"uri",
"cid",
"author",
"record",
"replyCount",
"repostCount",
"upvoteCount",
"downvoteCount",
"indexedAt"
],
properties: { properties: {
uri: { post: {
type: "string"
},
cid: {
type: "string"
},
author: {
type: "ref", type: "ref",
ref: "lex:app.bsky.actor.ref#withInfo" ref: "lex:app.bsky.feed.post#view"
},
record: {
type: "unknown"
},
embed: {
type: "union",
refs: [
"lex:app.bsky.embed.images#presented",
"lex:app.bsky.embed.external#presented"
]
}, },
parent: { parent: {
type: "union", type: "union",
refs: [ refs: [
"lex:app.bsky.feed.getPostThread#post", "lex:app.bsky.feed.getPostThread#threadViewPost",
"lex:app.bsky.feed.getPostThread#notFoundPost" "lex:app.bsky.feed.getPostThread#notFoundPost"
] ]
}, },
replyCount: {
type: "integer"
},
replies: { replies: {
type: "array", type: "array",
items: { items: {
type: "union", type: "union",
refs: [ refs: [
"lex:app.bsky.feed.getPostThread#post", "lex:app.bsky.feed.getPostThread#threadViewPost",
"lex:app.bsky.feed.getPostThread#notFoundPost" "lex:app.bsky.feed.getPostThread#notFoundPost"
] ]
} }
},
repostCount: {
type: "integer"
},
upvoteCount: {
type: "integer"
},
downvoteCount: {
type: "integer"
},
indexedAt: {
type: "datetime"
},
myState: {
type: "ref",
ref: "lex:app.bsky.feed.getPostThread#myState"
} }
} }
}, },
@ -6004,20 +5950,6 @@ var schemaDict = {
const: true const: true
} }
} }
},
myState: {
type: "object",
properties: {
repost: {
type: "string"
},
upvote: {
type: "string"
},
downvote: {
type: "string"
}
}
} }
} }
}, },
@ -6142,89 +6074,12 @@ var schemaDict = {
type: "array", type: "array",
items: { items: {
type: "ref", type: "ref",
ref: "lex:app.bsky.feed.getTimeline#feedItem" ref: "lex:app.bsky.feed.feedViewPost"
} }
} }
} }
} }
} }
},
feedItem: {
type: "object",
required: [
"uri",
"cid",
"author",
"record",
"replyCount",
"repostCount",
"upvoteCount",
"downvoteCount",
"indexedAt"
],
properties: {
uri: {
type: "string"
},
cid: {
type: "string"
},
author: {
type: "ref",
ref: "lex:app.bsky.actor.ref#withInfo"
},
trendedBy: {
type: "ref",
ref: "lex:app.bsky.actor.ref#withInfo"
},
repostedBy: {
type: "ref",
ref: "lex:app.bsky.actor.ref#withInfo"
},
record: {
type: "unknown"
},
embed: {
type: "union",
refs: [
"lex:app.bsky.embed.images#presented",
"lex:app.bsky.embed.external#presented"
]
},
replyCount: {
type: "integer"
},
repostCount: {
type: "integer"
},
upvoteCount: {
type: "integer"
},
downvoteCount: {
type: "integer"
},
indexedAt: {
type: "datetime"
},
myState: {
type: "ref",
ref: "lex:app.bsky.feed.getTimeline#myState"
}
}
},
myState: {
type: "object",
properties: {
repost: {
type: "string"
},
upvote: {
type: "string"
},
downvote: {
type: "string"
}
}
} }
} }
}, },
@ -6390,6 +6245,76 @@ var schemaDict = {
minimum: 0 minimum: 0
} }
} }
},
view: {
type: "object",
required: [
"uri",
"cid",
"author",
"record",
"replyCount",
"repostCount",
"upvoteCount",
"downvoteCount",
"indexedAt",
"viewer"
],
properties: {
uri: {
type: "string"
},
cid: {
type: "string"
},
author: {
type: "ref",
ref: "lex:app.bsky.actor.ref#withInfo"
},
record: {
type: "unknown"
},
embed: {
type: "union",
refs: [
"lex:app.bsky.embed.images#presented",
"lex:app.bsky.embed.external#presented"
]
},
replyCount: {
type: "integer"
},
repostCount: {
type: "integer"
},
upvoteCount: {
type: "integer"
},
downvoteCount: {
type: "integer"
},
indexedAt: {
type: "datetime"
},
viewer: {
type: "ref",
ref: "lex:app.bsky.feed.post#viewerState"
}
}
},
viewerState: {
type: "object",
properties: {
repost: {
type: "string"
},
upvote: {
type: "string"
},
downvote: {
type: "string"
}
}
} }
} }
}, },
@ -7826,6 +7751,9 @@ var external_exports = {};
// src/client/types/app/bsky/embed/images.ts // src/client/types/app/bsky/embed/images.ts
var images_exports = {}; var images_exports = {};
// src/client/types/app/bsky/feed/feedViewPost.ts
var feedViewPost_exports = {};
// src/client/types/app/bsky/feed/post.ts // src/client/types/app/bsky/feed/post.ts
var post_exports = {}; var post_exports = {};
@ -8709,6 +8637,7 @@ var SessionManager = class extends import_events.default {
AppBskyActorUpdateProfile, AppBskyActorUpdateProfile,
AppBskyEmbedExternal, AppBskyEmbedExternal,
AppBskyEmbedImages, AppBskyEmbedImages,
AppBskyFeedFeedViewPost,
AppBskyFeedGetAuthorFeed, AppBskyFeedGetAuthorFeed,
AppBskyFeedGetPostThread, AppBskyFeedGetPostThread,
AppBskyFeedGetRepostedBy, AppBskyFeedGetRepostedBy,

File diff suppressed because one or more lines are too long

View File

@ -85,6 +85,7 @@ export * as AppBskyActorSearchTypeahead from './types/app/bsky/actor/searchTypea
export * as AppBskyActorUpdateProfile from './types/app/bsky/actor/updateProfile'; export * as AppBskyActorUpdateProfile from './types/app/bsky/actor/updateProfile';
export * as AppBskyEmbedExternal from './types/app/bsky/embed/external'; export * as AppBskyEmbedExternal from './types/app/bsky/embed/external';
export * as AppBskyEmbedImages from './types/app/bsky/embed/images'; export * as AppBskyEmbedImages from './types/app/bsky/embed/images';
export * as AppBskyFeedFeedViewPost from './types/app/bsky/feed/feedViewPost';
export * as AppBskyFeedGetAuthorFeed from './types/app/bsky/feed/getAuthorFeed'; export * as AppBskyFeedGetAuthorFeed from './types/app/bsky/feed/getAuthorFeed';
export * as AppBskyFeedGetPostThread from './types/app/bsky/feed/getPostThread'; export * as AppBskyFeedGetPostThread from './types/app/bsky/feed/getPostThread';
export * as AppBskyFeedGetRepostedBy from './types/app/bsky/feed/getRepostedBy'; export * as AppBskyFeedGetRepostedBy from './types/app/bsky/feed/getRepostedBy';

View File

@ -1504,6 +1504,70 @@ export declare const schemaDict: {
}; };
}; };
}; };
AppBskyFeedFeedViewPost: {
lexicon: number;
id: string;
defs: {
main: {
type: string;
required: string[];
properties: {
post: {
type: string;
ref: string;
};
reply: {
type: string;
ref: string;
};
reason: {
type: string;
refs: string[];
};
};
};
replyRef: {
type: string;
required: string[];
properties: {
root: {
type: string;
ref: string;
};
parent: {
type: string;
ref: string;
};
};
};
reasonTrend: {
type: string;
required: string[];
properties: {
by: {
type: string;
ref: string;
};
indexedAt: {
type: string;
};
};
};
reasonRepost: {
type: string;
required: string[];
properties: {
by: {
type: string;
ref: string;
};
indexedAt: {
type: string;
};
};
};
};
};
AppBskyFeedGetAuthorFeed: { AppBskyFeedGetAuthorFeed: {
lexicon: number; lexicon: number;
id: string; id: string;
@ -1549,70 +1613,6 @@ export declare const schemaDict: {
}; };
}; };
}; };
feedItem: {
type: string;
required: string[];
properties: {
uri: {
type: string;
};
cid: {
type: string;
};
author: {
type: string;
ref: string;
};
trendedBy: {
type: string;
ref: string;
};
repostedBy: {
type: string;
ref: string;
};
record: {
type: string;
};
embed: {
type: string;
refs: string[];
};
replyCount: {
type: string;
};
repostCount: {
type: string;
};
upvoteCount: {
type: string;
};
downvoteCount: {
type: string;
};
indexedAt: {
type: string;
};
myState: {
type: string;
ref: string;
};
};
};
myState: {
type: string;
properties: {
repost: {
type: string;
};
upvote: {
type: string;
};
downvote: {
type: string;
};
};
};
}; };
}; };
AppBskyFeedGetPostThread: { AppBskyFeedGetPostThread: {
@ -1650,34 +1650,18 @@ export declare const schemaDict: {
name: string; name: string;
}[]; }[];
}; };
post: { threadViewPost: {
type: string; type: string;
required: string[]; required: string[];
properties: { properties: {
uri: { post: {
type: string;
};
cid: {
type: string;
};
author: {
type: string; type: string;
ref: string; ref: string;
}; };
record: {
type: string;
};
embed: {
type: string;
refs: string[];
};
parent: { parent: {
type: string; type: string;
refs: string[]; refs: string[];
}; };
replyCount: {
type: string;
};
replies: { replies: {
type: string; type: string;
items: { items: {
@ -1685,22 +1669,6 @@ export declare const schemaDict: {
refs: string[]; refs: string[];
}; };
}; };
repostCount: {
type: string;
};
upvoteCount: {
type: string;
};
downvoteCount: {
type: string;
};
indexedAt: {
type: string;
};
myState: {
type: string;
ref: string;
};
}; };
}; };
notFoundPost: { notFoundPost: {
@ -1716,20 +1684,6 @@ export declare const schemaDict: {
}; };
}; };
}; };
myState: {
type: string;
properties: {
repost: {
type: string;
};
upvote: {
type: string;
};
downvote: {
type: string;
};
};
};
}; };
}; };
AppBskyFeedGetRepostedBy: { AppBskyFeedGetRepostedBy: {
@ -1860,70 +1814,6 @@ export declare const schemaDict: {
}; };
}; };
}; };
feedItem: {
type: string;
required: string[];
properties: {
uri: {
type: string;
};
cid: {
type: string;
};
author: {
type: string;
ref: string;
};
trendedBy: {
type: string;
ref: string;
};
repostedBy: {
type: string;
ref: string;
};
record: {
type: string;
};
embed: {
type: string;
refs: string[];
};
replyCount: {
type: string;
};
repostCount: {
type: string;
};
upvoteCount: {
type: string;
};
downvoteCount: {
type: string;
};
indexedAt: {
type: string;
};
myState: {
type: string;
ref: string;
};
};
};
myState: {
type: string;
properties: {
repost: {
type: string;
};
upvote: {
type: string;
};
downvote: {
type: string;
};
};
};
}; };
}; };
AppBskyFeedGetVotes: { AppBskyFeedGetVotes: {
@ -2086,6 +1976,62 @@ export declare const schemaDict: {
}; };
}; };
}; };
view: {
type: string;
required: string[];
properties: {
uri: {
type: string;
};
cid: {
type: string;
};
author: {
type: string;
ref: string;
};
record: {
type: string;
};
embed: {
type: string;
refs: string[];
};
replyCount: {
type: string;
};
repostCount: {
type: string;
};
upvoteCount: {
type: string;
};
downvoteCount: {
type: string;
};
indexedAt: {
type: string;
};
viewer: {
type: string;
ref: string;
};
};
};
viewerState: {
type: string;
properties: {
repost: {
type: string;
};
upvote: {
type: string;
};
downvote: {
type: string;
};
};
};
}; };
}; };
AppBskyFeedRepost: { AppBskyFeedRepost: {
@ -2933,6 +2879,7 @@ export declare const ids: {
AppBskyActorUpdateProfile: string; AppBskyActorUpdateProfile: string;
AppBskyEmbedExternal: string; AppBskyEmbedExternal: string;
AppBskyEmbedImages: string; AppBskyEmbedImages: string;
AppBskyFeedFeedViewPost: string;
AppBskyFeedGetAuthorFeed: string; AppBskyFeedGetAuthorFeed: string;
AppBskyFeedGetPostThread: string; AppBskyFeedGetPostThread: string;
AppBskyFeedGetRepostedBy: string; AppBskyFeedGetRepostedBy: string;

View File

@ -0,0 +1,26 @@
import * as AppBskyFeedPost from './post';
import * as AppBskyActorRef from '../actor/ref';
export interface Main {
post: AppBskyFeedPost.View;
reply?: ReplyRef;
reason?: ReasonTrend | ReasonRepost | {
$type: string;
[k: string]: unknown;
};
[k: string]: unknown;
}
export interface ReplyRef {
root: AppBskyFeedPost.View;
parent: AppBskyFeedPost.View;
[k: string]: unknown;
}
export interface ReasonTrend {
by: AppBskyActorRef.WithInfo;
indexedAt: string;
[k: string]: unknown;
}
export interface ReasonRepost {
by: AppBskyActorRef.WithInfo;
indexedAt: string;
[k: string]: unknown;
}

View File

@ -1,7 +1,5 @@
import { Headers } from '@atproto/xrpc'; import { Headers } from '@atproto/xrpc';
import * as AppBskyActorRef from '../actor/ref'; import * as AppBskyFeedFeedViewPost from './feedViewPost';
import * as AppBskyEmbedImages from '../embed/images';
import * as AppBskyEmbedExternal from '../embed/external';
export interface QueryParams { export interface QueryParams {
author: string; author: string;
limit?: number; limit?: number;
@ -10,7 +8,7 @@ export interface QueryParams {
export declare type InputSchema = undefined; export declare type InputSchema = undefined;
export interface OutputSchema { export interface OutputSchema {
cursor?: string; cursor?: string;
feed: FeedItem[]; feed: AppBskyFeedFeedViewPost.Main[];
[k: string]: unknown; [k: string]: unknown;
} }
export interface CallOptions { export interface CallOptions {
@ -22,28 +20,3 @@ export interface Response {
data: OutputSchema; data: OutputSchema;
} }
export declare function toKnownErr(e: any): any; export declare function toKnownErr(e: any): any;
export interface FeedItem {
uri: string;
cid: string;
author: AppBskyActorRef.WithInfo;
trendedBy?: AppBskyActorRef.WithInfo;
repostedBy?: AppBskyActorRef.WithInfo;
record: {};
embed?: AppBskyEmbedImages.Presented | AppBskyEmbedExternal.Presented | {
$type: string;
[k: string]: unknown;
};
replyCount: number;
repostCount: number;
upvoteCount: number;
downvoteCount: number;
indexedAt: string;
myState?: MyState;
[k: string]: unknown;
}
export interface MyState {
repost?: string;
upvote?: string;
downvote?: string;
[k: string]: unknown;
}

View File

@ -1,14 +1,12 @@
import { Headers, XRPCError } from '@atproto/xrpc'; import { Headers, XRPCError } from '@atproto/xrpc';
import * as AppBskyActorRef from '../actor/ref'; import * as AppBskyFeedPost from './post';
import * as AppBskyEmbedImages from '../embed/images';
import * as AppBskyEmbedExternal from '../embed/external';
export interface QueryParams { export interface QueryParams {
uri: string; uri: string;
depth?: number; depth?: number;
} }
export declare type InputSchema = undefined; export declare type InputSchema = undefined;
export interface OutputSchema { export interface OutputSchema {
thread: Post | NotFoundPost | { thread: ThreadViewPost | NotFoundPost | {
$type: string; $type: string;
[k: string]: unknown; [k: string]: unknown;
}; };
@ -26,29 +24,16 @@ export declare class NotFoundError extends XRPCError {
constructor(src: XRPCError); constructor(src: XRPCError);
} }
export declare function toKnownErr(e: any): any; export declare function toKnownErr(e: any): any;
export interface Post { export interface ThreadViewPost {
uri: string; post: AppBskyFeedPost.View;
cid: string; parent?: ThreadViewPost | NotFoundPost | {
author: AppBskyActorRef.WithInfo;
record: {};
embed?: AppBskyEmbedImages.Presented | AppBskyEmbedExternal.Presented | {
$type: string; $type: string;
[k: string]: unknown; [k: string]: unknown;
}; };
parent?: Post | NotFoundPost | { replies?: (ThreadViewPost | NotFoundPost | {
$type: string;
[k: string]: unknown;
};
replyCount: number;
replies?: (Post | NotFoundPost | {
$type: string; $type: string;
[k: string]: unknown; [k: string]: unknown;
})[]; })[];
repostCount: number;
upvoteCount: number;
downvoteCount: number;
indexedAt: string;
myState?: MyState;
[k: string]: unknown; [k: string]: unknown;
} }
export interface NotFoundPost { export interface NotFoundPost {
@ -56,9 +41,3 @@ export interface NotFoundPost {
notFound: true; notFound: true;
[k: string]: unknown; [k: string]: unknown;
} }
export interface MyState {
repost?: string;
upvote?: string;
downvote?: string;
[k: string]: unknown;
}

View File

@ -1,7 +1,5 @@
import { Headers } from '@atproto/xrpc'; import { Headers } from '@atproto/xrpc';
import * as AppBskyActorRef from '../actor/ref'; import * as AppBskyFeedFeedViewPost from './feedViewPost';
import * as AppBskyEmbedImages from '../embed/images';
import * as AppBskyEmbedExternal from '../embed/external';
export interface QueryParams { export interface QueryParams {
algorithm?: string; algorithm?: string;
limit?: number; limit?: number;
@ -10,7 +8,7 @@ export interface QueryParams {
export declare type InputSchema = undefined; export declare type InputSchema = undefined;
export interface OutputSchema { export interface OutputSchema {
cursor?: string; cursor?: string;
feed: FeedItem[]; feed: AppBskyFeedFeedViewPost.Main[];
[k: string]: unknown; [k: string]: unknown;
} }
export interface CallOptions { export interface CallOptions {
@ -22,28 +20,3 @@ export interface Response {
data: OutputSchema; data: OutputSchema;
} }
export declare function toKnownErr(e: any): any; export declare function toKnownErr(e: any): any;
export interface FeedItem {
uri: string;
cid: string;
author: AppBskyActorRef.WithInfo;
trendedBy?: AppBskyActorRef.WithInfo;
repostedBy?: AppBskyActorRef.WithInfo;
record: {};
embed?: AppBskyEmbedImages.Presented | AppBskyEmbedExternal.Presented | {
$type: string;
[k: string]: unknown;
};
replyCount: number;
repostCount: number;
upvoteCount: number;
downvoteCount: number;
indexedAt: string;
myState?: MyState;
[k: string]: unknown;
}
export interface MyState {
repost?: string;
upvote?: string;
downvote?: string;
[k: string]: unknown;
}

View File

@ -1,6 +1,7 @@
import * as AppBskyEmbedImages from '../embed/images'; import * as AppBskyEmbedImages from '../embed/images';
import * as AppBskyEmbedExternal from '../embed/external'; import * as AppBskyEmbedExternal from '../embed/external';
import * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef'; import * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef';
import * as AppBskyActorRef from '../actor/ref';
export interface Record { export interface Record {
text: string; text: string;
entities?: Entity[]; entities?: Entity[];
@ -28,3 +29,26 @@ export interface TextSlice {
end: number; end: number;
[k: string]: unknown; [k: string]: unknown;
} }
export interface View {
uri: string;
cid: string;
author: AppBskyActorRef.WithInfo;
record: {};
embed?: AppBskyEmbedImages.Presented | AppBskyEmbedExternal.Presented | {
$type: string;
[k: string]: unknown;
};
replyCount: number;
repostCount: number;
upvoteCount: number;
downvoteCount: number;
indexedAt: string;
viewer: ViewerState;
[k: string]: unknown;
}
export interface ViewerState {
repost?: string;
upvote?: string;
downvote?: string;
[k: string]: unknown;
}

File diff suppressed because one or more lines are too long

View File

@ -213,7 +213,9 @@ function AdditionalPostText({
if (additionalPost.error) { if (additionalPost.error) {
return <ErrorMessage message={additionalPost.error} /> return <ErrorMessage message={additionalPost.error} />
} }
return <Text style={[s.gray5]}>{additionalPost.thread?.record.text}</Text> return (
<Text style={[s.gray5]}>{additionalPost.thread?.post.record.text}</Text>
)
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({

View File

@ -1,11 +1,10 @@
import React, {useRef} from 'react' import React, {useRef} from 'react'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {ActivityIndicator, FlatList, Text, View} from 'react-native' import {ActivityIndicator, FlatList, View} from 'react-native'
import { import {
PostThreadViewModel, PostThreadViewModel,
PostThreadViewPostModel, PostThreadViewPostModel,
} from '../../../state/models/post-thread-view' } from '../../../state/models/post-thread-view'
import {useStores} from '../../../state'
import {PostThreadItem} from './PostThreadItem' import {PostThreadItem} from './PostThreadItem'
import {ErrorMessage} from '../util/ErrorMessage' import {ErrorMessage} from '../util/ErrorMessage'
@ -93,14 +92,22 @@ function* flattenThread(
isAscending = false, isAscending = false,
): Generator<PostThreadViewPostModel, void> { ): Generator<PostThreadViewPostModel, void> {
if (post.parent) { if (post.parent) {
yield* flattenThread(post.parent, true) if ('notFound' in post.parent && post.parent.notFound) {
// TODO render not found
} else {
yield* flattenThread(post.parent as PostThreadViewPostModel, true)
}
} }
yield post yield post
if (post.replies?.length) { if (post.replies?.length) {
for (const reply of post.replies) { for (const reply of post.replies) {
yield* flattenThread(reply) if ('notFound' in reply && reply.notFound) {
// TODO render not found
} else {
yield* flattenThread(reply as PostThreadViewPostModel)
}
} }
} else if (!isAscending && !post.parent && post.replyCount > 0) { } else if (!isAscending && !post.parent && post.post.replyCount > 0) {
post._hasMore = true post._hasMore = true
} }
} }

View File

@ -32,37 +32,37 @@ export const PostThreadItem = observer(function PostThreadItem({
}) { }) {
const store = useStores() const store = useStores()
const [deleted, setDeleted] = useState(false) const [deleted, setDeleted] = useState(false)
const record = item.record as unknown as PostType.Record const record = item.post.record as unknown as PostType.Record
const hasEngagement = item.upvoteCount || item.repostCount const hasEngagement = item.post.upvoteCount || item.post.repostCount
const itemHref = useMemo(() => { const itemHref = useMemo(() => {
const urip = new AtUri(item.uri) const urip = new AtUri(item.post.uri)
return `/profile/${item.author.handle}/post/${urip.rkey}` return `/profile/${item.post.author.handle}/post/${urip.rkey}`
}, [item.uri, item.author.handle]) }, [item.post.uri, item.post.author.handle])
const itemTitle = `Post by ${item.author.handle}` const itemTitle = `Post by ${item.post.author.handle}`
const authorHref = `/profile/${item.author.handle}` const authorHref = `/profile/${item.post.author.handle}`
const authorTitle = item.author.handle const authorTitle = item.post.author.handle
const upvotesHref = useMemo(() => { const upvotesHref = useMemo(() => {
const urip = new AtUri(item.uri) const urip = new AtUri(item.post.uri)
return `/profile/${item.author.handle}/post/${urip.rkey}/upvoted-by` return `/profile/${item.post.author.handle}/post/${urip.rkey}/upvoted-by`
}, [item.uri, item.author.handle]) }, [item.post.uri, item.post.author.handle])
const upvotesTitle = 'Upvotes on this post' const upvotesTitle = 'Upvotes on this post'
const repostsHref = useMemo(() => { const repostsHref = useMemo(() => {
const urip = new AtUri(item.uri) const urip = new AtUri(item.post.uri)
return `/profile/${item.author.handle}/post/${urip.rkey}/reposted-by` return `/profile/${item.post.author.handle}/post/${urip.rkey}/reposted-by`
}, [item.uri, item.author.handle]) }, [item.post.uri, item.post.author.handle])
const repostsTitle = 'Reposts of this post' const repostsTitle = 'Reposts of this post'
const onPressReply = () => { const onPressReply = () => {
store.shell.openComposer({ store.shell.openComposer({
replyTo: { replyTo: {
uri: item.uri, uri: item.post.uri,
cid: item.cid, cid: item.post.cid,
text: item.record.text as string, text: record.text as string,
author: { author: {
handle: item.author.handle, handle: item.post.author.handle,
displayName: item.author.displayName, displayName: item.post.author.displayName,
avatar: item.author.avatar, avatar: item.post.author.avatar,
}, },
}, },
onPost: onPostReply, onPost: onPostReply,
@ -113,9 +113,9 @@ export const PostThreadItem = observer(function PostThreadItem({
<Link href={authorHref} title={authorTitle}> <Link href={authorHref} title={authorTitle}>
<UserAvatar <UserAvatar
size={50} size={50}
displayName={item.author.displayName} displayName={item.post.author.displayName}
handle={item.author.handle} handle={item.post.author.handle}
avatar={item.author.avatar} avatar={item.post.author.avatar}
/> />
</Link> </Link>
</View> </View>
@ -126,18 +126,18 @@ export const PostThreadItem = observer(function PostThreadItem({
href={authorHref} href={authorHref}
title={authorTitle}> title={authorTitle}>
<Text style={[s.f16, s.bold, s.black]} numberOfLines={1}> <Text style={[s.f16, s.bold, s.black]} numberOfLines={1}>
{item.author.displayName || item.author.handle} {item.post.author.displayName || item.post.author.handle}
</Text> </Text>
</Link> </Link>
<Text style={[styles.metaItem, s.f15, s.gray5]}> <Text style={[styles.metaItem, s.f15, s.gray5]}>
&middot; {ago(item.indexedAt)} &middot; {ago(item.post.indexedAt)}
</Text> </Text>
<View style={s.flex1} /> <View style={s.flex1} />
<PostDropdownBtn <PostDropdownBtn
style={styles.metaItem} style={styles.metaItem}
itemHref={itemHref} itemHref={itemHref}
itemTitle={itemTitle} itemTitle={itemTitle}
isAuthor={item.author.did === store.me.did} isAuthor={item.post.author.did === store.me.did}
onCopyPostText={onCopyPostText} onCopyPostText={onCopyPostText}
onDeletePost={onDeletePost}> onDeletePost={onDeletePost}>
<FontAwesomeIcon <FontAwesomeIcon
@ -153,7 +153,7 @@ export const PostThreadItem = observer(function PostThreadItem({
href={authorHref} href={authorHref}
title={authorTitle}> title={authorTitle}>
<Text style={[s.f15, s.gray5]} numberOfLines={1}> <Text style={[s.f15, s.gray5]} numberOfLines={1}>
@{item.author.handle} @{item.post.author.handle}
</Text> </Text>
</Link> </Link>
</View> </View>
@ -173,34 +173,34 @@ export const PostThreadItem = observer(function PostThreadItem({
/> />
</View> </View>
) : undefined} ) : undefined}
<PostEmbeds embed={item.embed} style={s.mb10} /> <PostEmbeds embed={item.post.embed} style={s.mb10} />
{item._isHighlightedPost && hasEngagement ? ( {item._isHighlightedPost && hasEngagement ? (
<View style={styles.expandedInfo}> <View style={styles.expandedInfo}>
{item.repostCount ? ( {item.post.repostCount ? (
<Link <Link
style={styles.expandedInfoItem} style={styles.expandedInfoItem}
href={repostsHref} href={repostsHref}
title={repostsTitle}> title={repostsTitle}>
<Text style={[s.gray5, s.semiBold, s.f17]}> <Text style={[s.gray5, s.semiBold, s.f17]}>
<Text style={[s.bold, s.black, s.f17]}> <Text style={[s.bold, s.black, s.f17]}>
{item.repostCount} {item.post.repostCount}
</Text>{' '} </Text>{' '}
{pluralize(item.repostCount, 'repost')} {pluralize(item.post.repostCount, 'repost')}
</Text> </Text>
</Link> </Link>
) : ( ) : (
<></> <></>
)} )}
{item.upvoteCount ? ( {item.post.upvoteCount ? (
<Link <Link
style={styles.expandedInfoItem} style={styles.expandedInfoItem}
href={upvotesHref} href={upvotesHref}
title={upvotesTitle}> title={upvotesTitle}>
<Text style={[s.gray5, s.semiBold, s.f17]}> <Text style={[s.gray5, s.semiBold, s.f17]}>
<Text style={[s.bold, s.black, s.f17]}> <Text style={[s.bold, s.black, s.f17]}>
{item.upvoteCount} {item.post.upvoteCount}
</Text>{' '} </Text>{' '}
{pluralize(item.upvoteCount, 'upvote')} {pluralize(item.post.upvoteCount, 'upvote')}
</Text> </Text>
</Link> </Link>
) : ( ) : (
@ -213,8 +213,8 @@ export const PostThreadItem = observer(function PostThreadItem({
<View style={[s.pl10, s.pb5]}> <View style={[s.pl10, s.pb5]}>
<PostCtrls <PostCtrls
big big
isReposted={!!item.myState.repost} isReposted={!!item.post.viewer.repost}
isUpvoted={!!item.myState.upvote} isUpvoted={!!item.post.viewer.upvote}
onPressReply={onPressReply} onPressReply={onPressReply}
onPressToggleRepost={onPressToggleRepost} onPressToggleRepost={onPressToggleRepost}
onPressToggleUpvote={onPressToggleUpvote} onPressToggleUpvote={onPressToggleUpvote}
@ -234,7 +234,7 @@ export const PostThreadItem = observer(function PostThreadItem({
return ( return (
<> <>
<Link style={styles.outer} href={itemHref} title={itemTitle} noFeedback> <Link style={styles.outer} href={itemHref} title={itemTitle} noFeedback>
{!item.replyingTo && item.record.reply && ( {!item.replyingTo && record.reply && (
<View style={styles.parentReplyLine} /> <View style={styles.parentReplyLine} />
)} )}
{item.replies?.length !== 0 && <View style={styles.childReplyLine} />} {item.replies?.length !== 0 && <View style={styles.childReplyLine} />}
@ -259,9 +259,9 @@ export const PostThreadItem = observer(function PostThreadItem({
<Link href={authorHref} title={authorTitle}> <Link href={authorHref} title={authorTitle}>
<UserAvatar <UserAvatar
size={50} size={50}
displayName={item.author.displayName} displayName={item.post.author.displayName}
handle={item.author.handle} handle={item.post.author.handle}
avatar={item.author.avatar} avatar={item.post.author.avatar}
/> />
</Link> </Link>
</View> </View>
@ -270,10 +270,10 @@ export const PostThreadItem = observer(function PostThreadItem({
itemHref={itemHref} itemHref={itemHref}
itemTitle={itemTitle} itemTitle={itemTitle}
authorHref={authorHref} authorHref={authorHref}
authorHandle={item.author.handle} authorHandle={item.post.author.handle}
authorDisplayName={item.author.displayName} authorDisplayName={item.post.author.displayName}
timestamp={item.indexedAt} timestamp={item.post.indexedAt}
isAuthor={item.author.did === store.me.did} isAuthor={item.post.author.did === store.me.did}
onCopyPostText={onCopyPostText} onCopyPostText={onCopyPostText}
onDeletePost={onDeletePost} onDeletePost={onDeletePost}
/> />
@ -288,13 +288,13 @@ export const PostThreadItem = observer(function PostThreadItem({
) : ( ) : (
<View style={{height: 5}} /> <View style={{height: 5}} />
)} )}
<PostEmbeds embed={item.embed} style={{marginBottom: 10}} /> <PostEmbeds embed={item.post.embed} style={{marginBottom: 10}} />
<PostCtrls <PostCtrls
replyCount={item.replyCount} replyCount={item.post.replyCount}
repostCount={item.repostCount} repostCount={item.post.repostCount}
upvoteCount={item.upvoteCount} upvoteCount={item.post.upvoteCount}
isReposted={!!item.myState.repost} isReposted={!!item.post.viewer.repost}
isUpvoted={!!item.myState.upvote} isUpvoted={!!item.post.viewer.upvote}
onPressReply={onPressReply} onPressReply={onPressReply}
onPressToggleRepost={onPressToggleRepost} onPressToggleRepost={onPressToggleRepost}
onPressToggleUpvote={onPressToggleUpvote} onPressToggleUpvote={onPressToggleUpvote}

View File

@ -77,13 +77,13 @@ export const Post = observer(function Post({
// loaded // loaded
// = // =
const item = view.thread const item = view.thread
const record = view.thread?.record as unknown as PostType.Record const record = view.thread?.post.record as unknown as PostType.Record
const itemUrip = new AtUri(item.uri) const itemUrip = new AtUri(item.post.uri)
const itemHref = `/profile/${item.author.handle}/post/${itemUrip.rkey}` const itemHref = `/profile/${item.post.author.handle}/post/${itemUrip.rkey}`
const itemTitle = `Post by ${item.author.handle}` const itemTitle = `Post by ${item.post.author.handle}`
const authorHref = `/profile/${item.author.handle}` const authorHref = `/profile/${item.post.author.handle}`
const authorTitle = item.author.handle const authorTitle = item.post.author.handle
let replyAuthorDid = '' let replyAuthorDid = ''
let replyHref = '' let replyHref = ''
if (record.reply) { if (record.reply) {
@ -94,13 +94,13 @@ export const Post = observer(function Post({
const onPressReply = () => { const onPressReply = () => {
store.shell.openComposer({ store.shell.openComposer({
replyTo: { replyTo: {
uri: item.uri, uri: item.post.uri,
cid: item.cid, cid: item.post.cid,
text: item.record.text as string, text: record.text as string,
author: { author: {
handle: item.author.handle, handle: item.post.author.handle,
displayName: item.author.displayName, displayName: item.post.author.displayName,
avatar: item.author.avatar, avatar: item.post.author.avatar,
}, },
}, },
}) })
@ -144,9 +144,9 @@ export const Post = observer(function Post({
<Link href={authorHref} title={authorTitle}> <Link href={authorHref} title={authorTitle}>
<UserAvatar <UserAvatar
size={52} size={52}
displayName={item.author.displayName} displayName={item.post.author.displayName}
handle={item.author.handle} handle={item.post.author.handle}
avatar={item.author.avatar} avatar={item.post.author.avatar}
/> />
</Link> </Link>
</View> </View>
@ -155,10 +155,10 @@ export const Post = observer(function Post({
itemHref={itemHref} itemHref={itemHref}
itemTitle={itemTitle} itemTitle={itemTitle}
authorHref={authorHref} authorHref={authorHref}
authorHandle={item.author.handle} authorHandle={item.post.author.handle}
authorDisplayName={item.author.displayName} authorDisplayName={item.post.author.displayName}
timestamp={item.indexedAt} timestamp={item.post.indexedAt}
isAuthor={item.author.did === store.me.did} isAuthor={item.post.author.did === store.me.did}
onCopyPostText={onCopyPostText} onCopyPostText={onCopyPostText}
onDeletePost={onDeletePost} onDeletePost={onDeletePost}
/> />
@ -186,13 +186,13 @@ export const Post = observer(function Post({
) : ( ) : (
<View style={{height: 5}} /> <View style={{height: 5}} />
)} )}
<PostEmbeds embed={item.embed} style={{marginBottom: 10}} /> <PostEmbeds embed={item.post.embed} style={{marginBottom: 10}} />
<PostCtrls <PostCtrls
replyCount={item.replyCount} replyCount={item.post.replyCount}
repostCount={item.repostCount} repostCount={item.post.repostCount}
upvoteCount={item.upvoteCount} upvoteCount={item.post.upvoteCount}
isReposted={!!item.myState.repost} isReposted={!!item.post.viewer.repost}
isUpvoted={!!item.myState.upvote} isUpvoted={!!item.post.viewer.upvote}
onPressReply={onPressReply} onPressReply={onPressReply}
onPressToggleRepost={onPressToggleRepost} onPressToggleRepost={onPressToggleRepost}
onPressToggleUpvote={onPressToggleUpvote} onPressToggleUpvote={onPressToggleUpvote}

View File

@ -10,7 +10,6 @@ import {FeedItemModel} from '../../../state/models/feed-view'
import {Link} from '../util/Link' import {Link} from '../util/Link'
import {Text} from '../util/Text' import {Text} from '../util/Text'
import {UserInfoText} from '../util/UserInfoText' import {UserInfoText} from '../util/UserInfoText'
import {Post} from '../post/Post'
import {PostMeta} from '../util/PostMeta' import {PostMeta} from '../util/PostMeta'
import {PostCtrls} from '../util/PostCtrls' import {PostCtrls} from '../util/PostCtrls'
import {PostEmbeds} from '../util/PostEmbeds' import {PostEmbeds} from '../util/PostEmbeds'
@ -20,22 +19,22 @@ import {UserAvatar} from '../util/UserAvatar'
import {s, colors} from '../../lib/styles' import {s, colors} from '../../lib/styles'
import {useStores} from '../../../state' import {useStores} from '../../../state'
const TOP_REPLY_LINE_LENGTH = 8
export const FeedItem = observer(function FeedItem({ export const FeedItem = observer(function FeedItem({
item, item,
showReplyLine,
}: { }: {
item: FeedItemModel item: FeedItemModel
showReplyLine?: boolean
}) { }) {
const store = useStores() const store = useStores()
const [deleted, setDeleted] = useState(false) const [deleted, setDeleted] = useState(false)
const record = item.record as unknown as PostType.Record const record = item.post.record as unknown as PostType.Record
const itemHref = useMemo(() => { const itemHref = useMemo(() => {
const urip = new AtUri(item.uri) const urip = new AtUri(item.post.uri)
return `/profile/${item.author.handle}/post/${urip.rkey}` return `/profile/${item.post.author.handle}/post/${urip.rkey}`
}, [item.uri, item.author.handle]) }, [item.post.uri, item.post.author.handle])
const itemTitle = `Post by ${item.author.handle}` const itemTitle = `Post by ${item.post.author.handle}`
const authorHref = `/profile/${item.author.handle}` const authorHref = `/profile/${item.post.author.handle}`
const replyAuthorDid = useMemo(() => { const replyAuthorDid = useMemo(() => {
if (!record.reply) return '' if (!record.reply) return ''
const urip = new AtUri(record.reply.parent?.uri || record.reply.root.uri) const urip = new AtUri(record.reply.parent?.uri || record.reply.root.uri)
@ -50,13 +49,13 @@ export const FeedItem = observer(function FeedItem({
const onPressReply = () => { const onPressReply = () => {
store.shell.openComposer({ store.shell.openComposer({
replyTo: { replyTo: {
uri: item.uri, uri: item.post.uri,
cid: item.cid, cid: item.post.cid,
text: item.record.text as string, text: record.text as string,
author: { author: {
handle: item.author.handle, handle: item.post.author.handle,
displayName: item.author.displayName, displayName: item.post.author.displayName,
avatar: item.author.avatar, avatar: item.post.author.avatar,
}, },
}, },
}) })
@ -92,66 +91,68 @@ export const FeedItem = observer(function FeedItem({
return <View /> return <View />
} }
const isChild = const isChild = item._isThreadChild || (!item.reason && item.reply)
item._isThreadChild || const isSmallTop = isChild && item._isThreadChild
(!item.repostedBy && !item.trendedBy && item.additionalParentPost?.thread) const isNoTop = isChild && !item._isThreadChild
const outerStyles = [ const outerStyles = [
styles.outer, styles.outer,
isChild isSmallTop ? styles.outerSmallTop : undefined,
? item._isThreadChild isNoTop ? styles.outerNoTop : undefined,
? styles.outerSmallTop
: styles.outerNoTop
: undefined,
item._isThreadParent ? styles.outerNoBottom : undefined, item._isThreadParent ? styles.outerNoBottom : undefined,
] ]
return ( return (
<> <>
{isChild && item.additionalParentPost?.thread ? ( {isChild && !item._isThreadChild && item.replyParent ? (
<Post <View style={{marginTop: 2}}>
uri={item.additionalParentPost.thread.uri} <FeedItem item={item.replyParent} showReplyLine />
initView={item.additionalParentPost} </View>
showReplyLine
style={{marginTop: 2}}
/>
) : undefined} ) : undefined}
<Link style={outerStyles} href={itemHref} title={itemTitle} noFeedback> <Link style={outerStyles} href={itemHref} title={itemTitle} noFeedback>
{isChild && <View style={[styles.topReplyLine]} />} {item._isThreadChild && <View style={[styles.topReplyLine]} />}
{item._isThreadParent && <View style={[styles.bottomReplyLine]} />} {(showReplyLine || item._isThreadParent) && (
{item.repostedBy && ( <View
style={[styles.bottomReplyLine, isNoTop ? {top: 64} : undefined]}
/>
)}
{item.reasonRepost && (
<Link <Link
style={styles.includeReason} style={styles.includeReason}
href={`/profile/${item.repostedBy.handle}`} href={`/profile/${item.reasonRepost.by.handle}`}
title={item.repostedBy.displayName || item.repostedBy.handle}> title={
item.reasonRepost.by.displayName || item.reasonRepost.by.handle
}>
<FontAwesomeIcon icon="retweet" style={styles.includeReasonIcon} /> <FontAwesomeIcon icon="retweet" style={styles.includeReasonIcon} />
<Text style={[s.gray4, s.bold, s.f13]}> <Text style={[s.gray4, s.bold, s.f13]}>
Reposted by{' '} Reposted by{' '}
{item.repostedBy.displayName || item.repostedBy.handle} {item.reasonRepost.by.displayName || item.reasonRepost.by.handle}
</Text> </Text>
</Link> </Link>
)} )}
{item.trendedBy && ( {item.reasonTrend && (
<Link <Link
style={styles.includeReason} style={styles.includeReason}
href={`/profile/${item.trendedBy.handle}`} href={`/profile/${item.reasonTrend.by.handle}`}
title={item.trendedBy.displayName || item.trendedBy.handle}> title={
item.reasonTrend.by.displayName || item.reasonTrend.by.handle
}>
<FontAwesomeIcon <FontAwesomeIcon
icon="arrow-trend-up" icon="arrow-trend-up"
style={styles.includeReasonIcon} style={styles.includeReasonIcon}
/> />
<Text style={[s.gray4, s.bold, s.f13]}> <Text style={[s.gray4, s.bold, s.f13]}>
Trending with{' '} Trending with{' '}
{item.trendedBy.displayName || item.trendedBy.handle} {item.reasonTrend.by.displayName || item.reasonTrend.by.handle}
</Text> </Text>
</Link> </Link>
)} )}
<View style={styles.layout}> <View style={styles.layout}>
<View style={styles.layoutAvi}> <View style={styles.layoutAvi}>
<Link href={authorHref} title={item.author.handle}> <Link href={authorHref} title={item.post.author.handle}>
<UserAvatar <UserAvatar
size={52} size={52}
displayName={item.author.displayName} displayName={item.post.author.displayName}
handle={item.author.handle} handle={item.post.author.handle}
avatar={item.author.avatar} avatar={item.post.author.avatar}
/> />
</Link> </Link>
</View> </View>
@ -160,10 +161,10 @@ export const FeedItem = observer(function FeedItem({
itemHref={itemHref} itemHref={itemHref}
itemTitle={itemTitle} itemTitle={itemTitle}
authorHref={authorHref} authorHref={authorHref}
authorHandle={item.author.handle} authorHandle={item.post.author.handle}
authorDisplayName={item.author.displayName} authorDisplayName={item.post.author.displayName}
timestamp={item.indexedAt} timestamp={item.post.indexedAt}
isAuthor={item.author.did === store.me.did} isAuthor={item.post.author.did === store.me.did}
onCopyPostText={onCopyPostText} onCopyPostText={onCopyPostText}
onDeletePost={onDeletePost} onDeletePost={onDeletePost}
/> />
@ -195,13 +196,13 @@ export const FeedItem = observer(function FeedItem({
) : ( ) : (
<View style={{height: 5}} /> <View style={{height: 5}} />
)} )}
<PostEmbeds embed={item.embed} style={styles.postEmbeds} /> <PostEmbeds embed={item.post.embed} style={styles.postEmbeds} />
<PostCtrls <PostCtrls
replyCount={item.replyCount} replyCount={item.post.replyCount}
repostCount={item.repostCount} repostCount={item.post.repostCount}
upvoteCount={item.upvoteCount} upvoteCount={item.post.upvoteCount}
isReposted={!!item.myState.repost} isReposted={!!item.post.viewer.repost}
isUpvoted={!!item.myState.upvote} isUpvoted={!!item.post.viewer.upvote}
onPressReply={onPressReply} onPressReply={onPressReply}
onPressToggleRepost={onPressToggleRepost} onPressToggleRepost={onPressToggleRepost}
onPressToggleUpvote={onPressToggleUpvote} onPressToggleUpvote={onPressToggleUpvote}
@ -266,15 +267,15 @@ const styles = StyleSheet.create({
topReplyLine: { topReplyLine: {
position: 'absolute', position: 'absolute',
left: 34, left: 34,
top: -1 * TOP_REPLY_LINE_LENGTH, top: 0,
height: TOP_REPLY_LINE_LENGTH, height: 6,
borderLeftWidth: 2, borderLeftWidth: 2,
borderLeftColor: colors.gray2, borderLeftColor: colors.gray2,
}, },
bottomReplyLine: { bottomReplyLine: {
position: 'absolute', position: 'absolute',
left: 34, left: 34,
top: 60, top: 72,
bottom: 0, bottom: 0,
borderLeftWidth: 2, borderLeftWidth: 2,
borderLeftColor: colors.gray2, borderLeftColor: colors.gray2,