diff options
author | Paul Frazee <pfrazee@gmail.com> | 2022-12-06 12:29:13 -0600 |
---|---|---|
committer | Paul Frazee <pfrazee@gmail.com> | 2022-12-06 12:29:13 -0600 |
commit | 246b0e19e183f8e751789a7e60e55bad25656a4e (patch) | |
tree | 628c8602784eb36fd4ba30f5777a14cde261a208 /src | |
parent | d60de5e214c049853b4d997b2b0d85530c34adb8 (diff) | |
download | voidsky-246b0e19e183f8e751789a7e60e55bad25656a4e.tar.zst |
Add context to replies when appearing in the feed
Diffstat (limited to 'src')
-rw-r--r-- | src/state/models/feed-view.ts | 119 | ||||
-rw-r--r-- | src/view/com/posts/FeedItem.tsx | 49 | ||||
-rw-r--r-- | src/view/com/util/UserAvatar.tsx | 6 | ||||
-rw-r--r-- | src/view/com/util/UserBanner.tsx | 6 |
4 files changed, 140 insertions, 40 deletions
diff --git a/src/state/models/feed-view.ts b/src/state/models/feed-view.ts index f5dce9e05..65248c575 100644 --- a/src/state/models/feed-view.ts +++ b/src/state/models/feed-view.ts @@ -1,6 +1,8 @@ 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 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 {RootStoreModel} from './root-store' import * as apilib from '../lib/api' @@ -43,10 +45,6 @@ export class FeedItemModel implements GetTimeline.FeedItem { repostedBy?: GetTimeline.Actor trendedBy?: GetTimeline.Actor record: Record<string, unknown> = {} - embed?: - | GetTimeline.RecordEmbed - | GetTimeline.ExternalEmbed - | GetTimeline.UnknownEmbed replyCount: number = 0 repostCount: number = 0 upvoteCount: number = 0 @@ -54,6 +52,9 @@ export class FeedItemModel implements GetTimeline.FeedItem { indexedAt: string = '' myState = new FeedItemMyStateModel() + // additional data + additionalParentPost?: PostThreadViewModel + constructor( public rootStore: RootStoreModel, reactKey: string, @@ -73,7 +74,6 @@ export class FeedItemModel implements GetTimeline.FeedItem { this.repostedBy = v.repostedBy this.trendedBy = v.trendedBy this.record = v.record - this.embed = v.embed this.replyCount = v.replyCount this.repostCount = v.repostCount this.upvoteCount = v.upvoteCount @@ -156,6 +156,29 @@ export class FeedItemModel implements GetTimeline.FeedItem { rkey: new AtUri(this.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) + }) + } } export class FeedModel { @@ -345,7 +368,7 @@ export class FeedModel { this._xLoading(isRefreshing) try { const res = await this._getFeed({limit: PAGE_SIZE}) - this._replaceAll(res) + await this._replaceAll(res) this._xIdle() } catch (e: any) { this._xIdle(e.toString()) @@ -356,7 +379,7 @@ export class FeedModel { this._xLoading() try { const res = await this._getFeed({limit: PAGE_SIZE}) - this._prependAll(res) + await this._prependAll(res) this._xIdle() } catch (e: any) { this._xIdle(e.toString()) @@ -373,7 +396,7 @@ export class FeedModel { before: this.loadMoreCursor, limit: PAGE_SIZE, }) - this._appendAll(res) + await this._appendAll(res) this._xIdle() } catch (e: any) { this._xIdle(`Failed to load feed: ${e.toString()}`) @@ -407,13 +430,17 @@ export class FeedModel { } } - private _replaceAll(res: GetTimeline.Response | GetAuthorFeed.Response) { - this.feed.length = 0 + private async _replaceAll( + res: GetTimeline.Response | GetAuthorFeed.Response, + ) { this.pollCursor = res.data.feed[0]?.uri - this._appendAll(res) + return this._appendAll(res, true) } - private _appendAll(res: GetTimeline.Response | GetAuthorFeed.Response) { + private async _appendAll( + res: GetTimeline.Response | GetAuthorFeed.Response, + replace = false, + ) { this.loadMoreCursor = res.data.cursor this.hasMore = !!this.loadMoreCursor let counter = this.feed.length @@ -428,40 +455,64 @@ export class FeedModel { // -prf const reorgedFeed = preprocessFeed(res.data.feed, this.feedType === 'home') + const promises = [] + const toAppend: FeedItemModel[] = [] for (const item of reorgedFeed) { - this._append(counter++, item) + const itemModel = new FeedItemModel( + this.rootStore, + `item-${counter++}`, + item, + ) + if (itemModel.needsAdditionalData) { + promises.push( + itemModel.fetchAdditionalData().catch(e => { + console.error('Failure during feed-view _appendAll()', e) + }), + ) + } + toAppend.push(itemModel) } + await Promise.all(promises) + runInAction(() => { + if (replace) { + this.feed = toAppend + } else { + this.feed = this.feed.concat(toAppend) + } + }) } - private _append( - keyId: number, - item: GetTimeline.FeedItem | GetAuthorFeed.FeedItem, + private async _prependAll( + res: GetTimeline.Response | GetAuthorFeed.Response, ) { - // TODO: validate .record - this.feed.push(new FeedItemModel(this.rootStore, `item-${keyId}`, item)) - } - - private _prependAll(res: GetTimeline.Response | GetAuthorFeed.Response) { this.pollCursor = res.data.feed[0]?.uri let counter = this.feed.length - const toPrepend = [] + + const promises = [] + const toPrepend: FeedItemModel[] = [] for (const item of res.data.feed) { if (this.feed.find(item2 => item2.uri === item.uri)) { break // stop here - we've hit a post we already have } - toPrepend.unshift(item) // reverse the order - } - for (const item of toPrepend) { - this._prepend(counter++, item) - } - } - private _prepend( - keyId: number, - item: GetTimeline.FeedItem | GetAuthorFeed.FeedItem, - ) { - // TODO: validate .record - this.feed.unshift(new FeedItemModel(this.rootStore, `item-${keyId}`, item)) + const itemModel = new FeedItemModel( + this.rootStore, + `item-${counter++}`, + item, + ) + if (itemModel.needsAdditionalData) { + promises.push( + itemModel.fetchAdditionalData().catch(e => { + console.error('Failure during feed-view _prependAll()', e) + }), + ) + } + toPrepend.push(itemModel) + } + await Promise.all(promises) + runInAction(() => { + this.feed = toPrepend.concat(this.feed) + }) } private _updateAll(res: GetTimeline.Response | GetAuthorFeed.Response) { diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index b34fe239d..0e3a58090 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -18,6 +18,7 @@ import {s, colors} from '../../lib/styles' import {useStores} from '../../../state' const TOP_REPLY_LINE_LENGTH = 12 +const REPLYING_TO_LINE_LENGTH = 8 export const FeedItem = observer(function FeedItem({ item, @@ -129,6 +130,25 @@ export const FeedItem = observer(function FeedItem({ </Text> </Link> )} + {item.additionalParentPost ? ( + <View style={styles.replyingTo}> + <View style={styles.replyingToLine} /> + <View style={styles.replyingToAvatar}> + <UserAvatar + handle={item.additionalParentPost?.thread?.author.handle} + displayName={ + item.additionalParentPost?.thread?.author.displayName + } + size={32} + /> + </View> + <View style={styles.replyingToTextContainer}> + <Text style={styles.replyingToText} numberOfLines={2}> + {item.additionalParentPost?.thread?.record.text} + </Text> + </View> + </View> + ) : undefined} <View style={styles.layout}> <View style={styles.layoutAvi}> <Link @@ -237,6 +257,35 @@ const styles = StyleSheet.create({ marginRight: 4, color: colors.gray4, }, + replyingToLine: { + position: 'absolute', + left: 24, + bottom: -1 * REPLYING_TO_LINE_LENGTH + 6, + height: REPLYING_TO_LINE_LENGTH, + borderLeftWidth: 2, + borderLeftColor: colors.gray2, + }, + replyingTo: { + flexDirection: 'row', + backgroundColor: colors.white, + paddingBottom: 8, + paddingRight: 24, + }, + replyingToAvatar: { + marginLeft: 9, + marginRight: 19, + marginTop: 1, + }, + replyingToTextContainer: { + flex: 1, + flexDirection: 'row', + height: 34, + alignItems: 'center', + }, + replyingToText: { + flex: 1, + color: colors.gray5, + }, layout: { flexDirection: 'row', }, diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index 2ed161253..2b2388473 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -21,7 +21,7 @@ export function UserAvatar({ size: number handle: string displayName: string | undefined - userAvatar: string | null + userAvatar: string | null | undefined setUserAvatar?: React.Dispatch<React.SetStateAction<string | null>> }) { const initials = getInitials(displayName || handle) @@ -92,7 +92,7 @@ export function UserAvatar({ // setUserAvatar is only passed as prop on the EditProfile component return setUserAvatar != null && IMAGES_ENABLED ? ( <TouchableOpacity onPress={handleEditAvatar}> - {userAvatar != null ? ( + {userAvatar ? ( <Image style={styles.avatarImage} source={{uri: userAvatar}} /> ) : ( renderSvg(size, initials) @@ -105,7 +105,7 @@ export function UserAvatar({ /> </View> </TouchableOpacity> - ) : userAvatar != null ? ( + ) : userAvatar ? ( <Image style={styles.avatarImage} resizeMode="stretch" diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx index c0421fe12..684e984bd 100644 --- a/src/view/com/util/UserBanner.tsx +++ b/src/view/com/util/UserBanner.tsx @@ -17,7 +17,7 @@ export function UserBanner({ setUserBanner, }: { handle: string - userBanner: string | null + userBanner: string | null | undefined setUserBanner?: React.Dispatch<React.SetStateAction<string | null>> }) { const gradient = getGradient(handle) @@ -81,7 +81,7 @@ export function UserBanner({ // setUserBanner is only passed as prop on the EditProfile component return setUserBanner != null && IMAGES_ENABLED ? ( <TouchableOpacity onPress={handleEditBanner}> - {userBanner != null ? ( + {userBanner ? ( <Image style={styles.bannerImage} source={{uri: userBanner}} /> ) : ( renderSvg() @@ -94,7 +94,7 @@ export function UserBanner({ /> </View> </TouchableOpacity> - ) : userBanner != null ? ( + ) : userBanner ? ( <Image style={styles.bannerImage} resizeMode="stretch" |