diff --git a/src/state/models/notifications-view.ts b/src/state/models/notifications-view.ts index 80e5c80c..f3163822 100644 --- a/src/state/models/notifications-view.ts +++ b/src/state/models/notifications-view.ts @@ -1,6 +1,7 @@ -import {makeAutoObservable} from 'mobx' +import {makeAutoObservable, runInAction} from 'mobx' import * as ListNotifications from '../../third-party/api/src/client/types/app/bsky/notification/list' import {RootStoreModel} from './root-store' +import {PostThreadViewModel} from './post-thread-view' import {Declaration} from './_common' import {hasProp} from '../lib/type-guards' import {APP_BSKY_GRAPH} from '../../third-party/api' @@ -34,6 +35,9 @@ export class NotificationsViewItemModel implements GroupedNotification { indexedAt: string = '' additional?: NotificationsViewItemModel[] + // additional data + additionalPost?: PostThreadViewModel + constructor( public rootStore: RootStoreModel, reactKey: string, @@ -89,6 +93,13 @@ export class NotificationsViewItemModel implements GroupedNotification { return this.reason === 'assertion' } + get needsAdditionalData() { + if (this.isUpvote || this.isRepost || this.isTrend || this.isReply) { + return !this.additionalPost + } + return false + } + get isInvite() { return ( this.isAssertion && this.record.assertion === APP_BSKY_GRAPH.AssertMember @@ -107,6 +118,27 @@ export class NotificationsViewItemModel implements GroupedNotification { } return '' } + + async fetchAdditionalData() { + if (!this.needsAdditionalData) { + return + } + let postUri + if (this.isReply) { + postUri = this.uri + } else if (this.isUpvote || this.isRead || this.isTrend) { + postUri = this.subjectUri + } + if (postUri) { + this.additionalPost = new PostThreadViewModel(this.rootStore, { + uri: postUri, + depth: 0, + }) + await this.additionalPost.setup().catch(e => { + console.error('Failed to load post needed by notification', e) + }) + } + } } export class NotificationsViewModel { @@ -246,7 +278,7 @@ export class NotificationsViewModel { limit: PAGE_SIZE, }) const res = await this.rootStore.api.app.bsky.notification.list(params) - this._replaceAll(res) + await this._replaceAll(res) this._xIdle() } catch (e: any) { this._xIdle(`Failed to load notifications: ${e.toString()}`) @@ -264,7 +296,7 @@ export class NotificationsViewModel { before: this.loadMoreCursor, }) const res = await this.rootStore.api.app.bsky.notification.list(params) - this._appendAll(res) + await this._appendAll(res) this._xIdle() } catch (e: any) { this._xIdle(`Failed to load notifications: ${e.toString()}`) @@ -296,25 +328,37 @@ export class NotificationsViewModel { } } - private _replaceAll(res: ListNotifications.Response) { + private async _replaceAll(res: ListNotifications.Response) { this.notifications.length = 0 - this._appendAll(res) + return this._appendAll(res) } - private _appendAll(res: ListNotifications.Response) { + private async _appendAll(res: ListNotifications.Response) { this.loadMoreCursor = res.data.cursor this.hasMore = !!this.loadMoreCursor let counter = this.notifications.length + const promises = [] + const itemModels: NotificationsViewItemModel[] = [] for (const item of groupNotifications(res.data.notifications)) { - this._append(counter++, item) + const itemModel = new NotificationsViewItemModel( + this.rootStore, + `item-${counter++}`, + item, + ) + if (itemModel.needsAdditionalData) { + promises.push(itemModel.fetchAdditionalData()) + } + itemModels.push(itemModel) } - } - - private _append(keyId: number, item: GroupedNotification) { - // TODO: validate .record - this.notifications.push( - new NotificationsViewItemModel(this.rootStore, `item-${keyId}`, item), - ) + await Promise.all(promises).catch(e => { + console.error( + 'Uncaught failure during notifications-view _appendAll()', + e, + ) + }) + runInAction(() => { + this.notifications = this.notifications.concat(itemModels) + }) } private _updateAll(res: ListNotifications.Response) { diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx index 8741e423..1b9fe51f 100644 --- a/src/view/com/notifications/FeedItem.tsx +++ b/src/view/com/notifications/FeedItem.tsx @@ -4,11 +4,12 @@ import {StyleSheet, Text, View} from 'react-native' import {AtUri} from '../../../third-party/uri' import {FontAwesomeIcon, Props} from '@fortawesome/react-native-fontawesome' import {NotificationsViewItemModel} from '../../../state/models/notifications-view' +import {PostThreadViewModel} from '../../../state/models/post-thread-view' import {s, colors} from '../../lib/styles' import {ago, pluralize} from '../../../lib/strings' import {UpIconSolid} from '../../lib/icons' import {UserAvatar} from '../util/UserAvatar' -import {PostText} from '../post/PostText' +import {ErrorMessage} from '../util/ErrorMessage' import {Post} from '../post/Post' import {Link} from '../util/Link' import {InviteAccepter} from './InviteAccepter' @@ -51,7 +52,7 @@ export const FeedItem = observer(function FeedItem({ ]} href={itemHref} title={itemTitle}> - + ) } @@ -170,7 +171,7 @@ export const FeedItem = observer(function FeedItem({ {item.isUpvote || item.isRepost || item.isTrend ? ( - + ) : ( <> )} @@ -181,17 +182,24 @@ export const FeedItem = observer(function FeedItem({ )} - {item.isReply ? ( - - - - ) : ( - <> - )} ) }) +function AdditionalPostText({ + additionalPost, +}: { + additionalPost?: PostThreadViewModel +}) { + if (!additionalPost) { + return + } + if (additionalPost.error) { + return + } + return {additionalPost.thread?.record.text} +} + const styles = StyleSheet.create({ outer: { backgroundColor: colors.white, diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index 4d668cac..033cc656 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -15,19 +15,25 @@ import {UserAvatar} from '../util/UserAvatar' import {useStores} from '../../../state' import {s, colors} from '../../lib/styles' -export const Post = observer(function Post({uri}: {uri: string}) { +export const Post = observer(function Post({ + uri, + initView, +}: { + uri: string + initView?: PostThreadViewModel +}) { const store = useStores() - const [view, setView] = useState() + const [view, setView] = useState(initView) const [deleted, setDeleted] = useState(false) useEffect(() => { - if (view?.params.uri === uri) { + if (initView || view?.params.uri === uri) { return // no change needed? or trigger refresh? } const newView = new PostThreadViewModel(store, {uri, depth: 0}) setView(newView) newView.setup().catch(err => console.error('Failed to fetch post', err)) - }, [uri, view?.params.uri, store]) + }, [initView, uri, view?.params.uri, store]) // deleted // =