about summary refs log tree commit diff
path: root/src/state/models
diff options
context:
space:
mode:
Diffstat (limited to 'src/state/models')
-rw-r--r--src/state/models/_common.ts4
-rw-r--r--src/state/models/feed-view.ts9
-rw-r--r--src/state/models/memberships-view.ts4
-rw-r--r--src/state/models/notifications-view.ts43
-rw-r--r--src/state/models/post-thread-view.ts7
-rw-r--r--src/state/models/profile-view.ts13
-rw-r--r--src/state/models/reposted-by-view.ts2
-rw-r--r--src/state/models/scene-invite-suggestions.ts126
-rw-r--r--src/state/models/shell-ui.ts22
-rw-r--r--src/state/models/suggested-actors-view.ts3
-rw-r--r--src/state/models/user-followers-view.ts7
-rw-r--r--src/state/models/user-follows-view.ts7
12 files changed, 226 insertions, 21 deletions
diff --git a/src/state/models/_common.ts b/src/state/models/_common.ts
new file mode 100644
index 000000000..459dd7602
--- /dev/null
+++ b/src/state/models/_common.ts
@@ -0,0 +1,4 @@
+export interface Declaration {
+  cid: string
+  actorType: string
+}
diff --git a/src/state/models/feed-view.ts b/src/state/models/feed-view.ts
index 392f9d215..27b4b04e2 100644
--- a/src/state/models/feed-view.ts
+++ b/src/state/models/feed-view.ts
@@ -21,8 +21,13 @@ export class FeedItemModel implements GetTimeline.FeedItem {
   // data
   uri: string = ''
   cid: string = ''
-  author: GetTimeline.User = {did: '', handle: '', displayName: ''}
-  repostedBy?: GetTimeline.User
+  author: GetTimeline.Actor = {
+    did: '',
+    handle: '',
+    displayName: '',
+    declaration: {cid: '', actorType: ''},
+  }
+  repostedBy?: GetTimeline.Actor
   record: Record<string, unknown> = {}
   embed?:
     | GetTimeline.RecordEmbed
diff --git a/src/state/models/memberships-view.ts b/src/state/models/memberships-view.ts
index 504369c4f..4dfe2ab13 100644
--- a/src/state/models/memberships-view.ts
+++ b/src/state/models/memberships-view.ts
@@ -47,6 +47,10 @@ export class MembershipsViewModel {
     return this.hasLoaded && !this.hasContent
   }
 
+  isMemberOf(did: string) {
+    return !!this.memberships.find(m => m.did === did)
+  }
+
   // public api
   // =
 
diff --git a/src/state/models/notifications-view.ts b/src/state/models/notifications-view.ts
index 7b1508a19..43bc0cb4d 100644
--- a/src/state/models/notifications-view.ts
+++ b/src/state/models/notifications-view.ts
@@ -1,7 +1,11 @@
 import {makeAutoObservable} from 'mobx'
 import * as ListNotifications from '../../third-party/api/src/client/types/app/bsky/notification/list'
 import {RootStoreModel} from './root-store'
+import {Declaration} from './_common'
 import {hasProp} from '../lib/type-guards'
+import {APP_BSKY_GRAPH} from '../../third-party/api'
+
+const UNGROUPABLE_REASONS = ['trend', 'assertion']
 
 export interface GroupedNotification extends ListNotifications.Notification {
   additional?: ListNotifications.Notification[]
@@ -18,7 +22,8 @@ export class NotificationsViewItemModel implements GroupedNotification {
     did: string
     handle: string
     displayName?: string
-  } = {did: '', handle: ''}
+    declaration: Declaration
+  } = {did: '', handle: '', declaration: {cid: '', actorType: ''}}
   reason: string = ''
   reasonSubject?: string
   record: any = {}
@@ -65,6 +70,10 @@ export class NotificationsViewItemModel implements GroupedNotification {
     return this.reason === 'repost'
   }
 
+  get isTrend() {
+    return this.reason === 'trend'
+  }
+
   get isReply() {
     return this.reason === 'reply'
   }
@@ -73,6 +82,16 @@ export class NotificationsViewItemModel implements GroupedNotification {
     return this.reason === 'follow'
   }
 
+  get isAssertion() {
+    return this.reason === 'assertion'
+  }
+
+  get isInvite() {
+    return (
+      this.isAssertion && this.record.assertion === APP_BSKY_GRAPH.AssertMember
+    )
+  }
+
   get subjectUri() {
     if (this.reasonSubject) {
       return this.reasonSubject
@@ -316,16 +335,18 @@ function groupNotifications(
   const items2: GroupedNotification[] = []
   for (const item of items) {
     let grouped = false
-    for (const item2 of items2) {
-      if (
-        item.reason === item2.reason &&
-        item.reasonSubject === item2.reasonSubject &&
-        item.author.did !== item2.author.did
-      ) {
-        item2.additional = item2.additional || []
-        item2.additional.push(item)
-        grouped = true
-        break
+    if (!UNGROUPABLE_REASONS.includes(item.reason)) {
+      for (const item2 of items2) {
+        if (
+          item.reason === item2.reason &&
+          item.reasonSubject === item2.reasonSubject &&
+          item.author.did !== item2.author.did
+        ) {
+          item2.additional = item2.additional || []
+          item2.additional.push(item)
+          grouped = true
+          break
+        }
       }
     }
     if (!grouped) {
diff --git a/src/state/models/post-thread-view.ts b/src/state/models/post-thread-view.ts
index 7a735de03..385aa2e8e 100644
--- a/src/state/models/post-thread-view.ts
+++ b/src/state/models/post-thread-view.ts
@@ -31,7 +31,12 @@ export class PostThreadViewPostModel implements GetPostThread.Post {
   // data
   uri: string = ''
   cid: string = ''
-  author: GetPostThread.User = {did: '', handle: '', displayName: ''}
+  author: GetPostThread.User = {
+    did: '',
+    handle: '',
+    displayName: '',
+    declaration: {cid: '', actorType: ''},
+  }
   record: Record<string, unknown> = {}
   embed?:
     | GetPostThread.RecordEmbed
diff --git a/src/state/models/profile-view.ts b/src/state/models/profile-view.ts
index 62a17a6fd..4df338fc0 100644
--- a/src/state/models/profile-view.ts
+++ b/src/state/models/profile-view.ts
@@ -1,6 +1,7 @@
 import {makeAutoObservable, runInAction} from 'mobx'
 import * as GetProfile from '../../third-party/api/src/client/types/app/bsky/actor/getProfile'
 import * as Profile from '../../third-party/api/src/client/types/app/bsky/actor/profile'
+import {Declaration} from './_common'
 import {RootStoreModel} from './root-store'
 import * as apilib from '../lib/api'
 
@@ -9,6 +10,7 @@ export const ACTOR_TYPE_SCENE = 'app.bsky.system.actorScene'
 
 export class ProfileViewMyStateModel {
   follow?: string
+  member?: string
 
   constructor() {
     makeAutoObservable(this)
@@ -26,7 +28,10 @@ export class ProfileViewModel {
   // data
   did: string = ''
   handle: string = ''
-  actorType = ACTOR_TYPE_USER
+  declaration: Declaration = {
+    cid: '',
+    actorType: '',
+  }
   creator: string = ''
   displayName?: string
   description?: string
@@ -64,11 +69,11 @@ export class ProfileViewModel {
   }
 
   get isUser() {
-    return this.actorType === ACTOR_TYPE_USER
+    return this.declaration.actorType === ACTOR_TYPE_USER
   }
 
   get isScene() {
-    return this.actorType === ACTOR_TYPE_SCENE
+    return this.declaration.actorType === ACTOR_TYPE_SCENE
   }
 
   // public api
@@ -145,7 +150,7 @@ export class ProfileViewModel {
     console.log(res.data)
     this.did = res.data.did
     this.handle = res.data.handle
-    this.actorType = res.data.actorType
+    Object.assign(this.declaration, res.data.declaration)
     this.creator = res.data.creator
     this.displayName = res.data.displayName
     this.description = res.data.description
diff --git a/src/state/models/reposted-by-view.ts b/src/state/models/reposted-by-view.ts
index 2911ae7ef..211a755dc 100644
--- a/src/state/models/reposted-by-view.ts
+++ b/src/state/models/reposted-by-view.ts
@@ -2,6 +2,7 @@ import {makeAutoObservable, runInAction} from 'mobx'
 import {AtUri} from '../../third-party/uri'
 import * as GetRepostedBy from '../../third-party/api/src/client/types/app/bsky/feed/getRepostedBy'
 import {RootStoreModel} from './root-store'
+import {Declaration} from './_common'
 
 type RepostedByItem = GetRepostedBy.OutputSchema['repostedBy'][number]
 
@@ -13,6 +14,7 @@ export class RepostedByViewItemModel implements RepostedByItem {
   did: string = ''
   handle: string = ''
   displayName: string = ''
+  declaration: Declaration = {cid: '', actorType: ''}
   createdAt?: string
   indexedAt: string = ''
 
diff --git a/src/state/models/scene-invite-suggestions.ts b/src/state/models/scene-invite-suggestions.ts
new file mode 100644
index 000000000..34bb5c28d
--- /dev/null
+++ b/src/state/models/scene-invite-suggestions.ts
@@ -0,0 +1,126 @@
+import {makeAutoObservable, runInAction} from 'mobx'
+import {RootStoreModel} from './root-store'
+import {MembersViewModel} from './members-view'
+import {UserFollowsViewModel, FollowItem} from './user-follows-view'
+
+export interface SceneInviteSuggestionsParams {
+  sceneDid: string
+}
+
+export class SceneInviteSuggestions {
+  // state
+  isLoading = false
+  isRefreshing = false
+  hasLoaded = false
+  error = ''
+  params: SceneInviteSuggestionsParams
+  sceneMembersView: MembersViewModel
+  myFollowsView: UserFollowsViewModel
+
+  // data
+  suggestions: FollowItem[] = []
+
+  constructor(
+    public rootStore: RootStoreModel,
+    params: SceneInviteSuggestionsParams,
+  ) {
+    makeAutoObservable(
+      this,
+      {
+        rootStore: false,
+        params: false,
+      },
+      {autoBind: true},
+    )
+    this.params = params
+    this.sceneMembersView = new MembersViewModel(rootStore, {
+      actor: params.sceneDid,
+    })
+    this.myFollowsView = new UserFollowsViewModel(rootStore, {
+      user: rootStore.me.did || '',
+    })
+  }
+
+  get hasContent() {
+    return this.suggestions.length > 0
+  }
+
+  get hasError() {
+    return this.error !== ''
+  }
+
+  get isEmpty() {
+    return this.hasLoaded && !this.hasContent
+  }
+
+  // public api
+  // =
+
+  async setup() {
+    await this._fetch(false)
+  }
+
+  async refresh() {
+    await this._fetch(true)
+  }
+
+  async loadMore() {
+    // TODO
+  }
+
+  // state transitions
+  // =
+
+  private _xLoading(isRefreshing = false) {
+    this.isLoading = true
+    this.isRefreshing = isRefreshing
+    this.error = ''
+  }
+
+  private _xIdle(err: string = '') {
+    this.isLoading = false
+    this.isRefreshing = false
+    this.hasLoaded = true
+    this.error = err
+  }
+
+  // loader functions
+  // =
+
+  private async _fetch(isRefreshing = false) {
+    this._xLoading(isRefreshing)
+    try {
+      await this.sceneMembersView.setup()
+    } catch (e) {
+      console.error(e)
+      this._xIdle(
+        'Failed to fetch the current scene members. Check your internet connection and try again.',
+      )
+      return
+    }
+    try {
+      await this.myFollowsView.setup()
+    } catch (e) {
+      console.error(e)
+      this._xIdle(
+        'Failed to fetch the your current followers. Check your internet connection and try again.',
+      )
+      return
+    }
+
+    // collect all followed users that arent already in the scene
+    const newSuggestions: FollowItem[] = []
+    for (const follow of this.myFollowsView.follows) {
+      // TODO: filter out scenes
+      if (
+        !this.sceneMembersView.members.find(member => member.did === follow.did)
+      ) {
+        newSuggestions.push(follow)
+      }
+    }
+    runInAction(() => {
+      this.suggestions = newSuggestions
+    })
+    this._xIdle()
+  }
+}
diff --git a/src/state/models/shell-ui.ts b/src/state/models/shell-ui.ts
index 345a6b4a9..8eefc711c 100644
--- a/src/state/models/shell-ui.ts
+++ b/src/state/models/shell-ui.ts
@@ -19,6 +19,18 @@ export class LinkActionsModel {
   }
 }
 
+export class ConfirmModel {
+  name = 'confirm'
+
+  constructor(
+    public title: string,
+    public message: string | (() => JSX.Element),
+    public onPressConfirm: () => void | Promise<void>,
+  ) {
+    makeAutoObservable(this)
+  }
+}
+
 export class SharePostModel {
   name = 'share-post'
 
@@ -43,6 +55,14 @@ export class CreateSceneModel {
   }
 }
 
+export class InviteToSceneModel {
+  name = 'invite-to-scene'
+
+  constructor(public profileView: ProfileViewModel) {
+    makeAutoObservable(this)
+  }
+}
+
 export interface ComposerOpts {
   replyTo?: Post.PostRef
   onPost?: () => void
@@ -52,6 +72,7 @@ export class ShellUiModel {
   isModalActive = false
   activeModal:
     | LinkActionsModel
+    | ConfirmModel
     | SharePostModel
     | EditProfileModel
     | CreateSceneModel
@@ -66,6 +87,7 @@ export class ShellUiModel {
   openModal(
     modal:
       | LinkActionsModel
+      | ConfirmModel
       | SharePostModel
       | EditProfileModel
       | CreateSceneModel,
diff --git a/src/state/models/suggested-actors-view.ts b/src/state/models/suggested-actors-view.ts
index 245dbf020..a22532378 100644
--- a/src/state/models/suggested-actors-view.ts
+++ b/src/state/models/suggested-actors-view.ts
@@ -1,5 +1,6 @@
 import {makeAutoObservable} from 'mobx'
 import {RootStoreModel} from './root-store'
+import {Declaration} from './_common'
 
 interface Response {
   data: {
@@ -9,6 +10,7 @@ interface Response {
 export type ResponseSuggestedActor = {
   did: string
   handle: string
+  declaration: Declaration
   displayName?: string
   description?: string
   createdAt?: string
@@ -109,7 +111,6 @@ export class SuggestedActorsViewModel {
     for (const item of res.data.suggestions) {
       this._append({
         _reactKey: `item-${counter++}`,
-        description: 'Just another cool person using Bluesky',
         ...item,
       })
     }
diff --git a/src/state/models/user-followers-view.ts b/src/state/models/user-followers-view.ts
index 1edd95232..b27201997 100644
--- a/src/state/models/user-followers-view.ts
+++ b/src/state/models/user-followers-view.ts
@@ -16,7 +16,12 @@ export class UserFollowersViewModel {
   params: GetFollowers.QueryParams
 
   // data
-  subject: Subject = {did: '', handle: '', displayName: ''}
+  subject: Subject = {
+    did: '',
+    handle: '',
+    displayName: '',
+    declaration: {cid: '', actorType: ''},
+  }
   followers: FollowerItem[] = []
 
   constructor(
diff --git a/src/state/models/user-follows-view.ts b/src/state/models/user-follows-view.ts
index b7875c22c..13742c327 100644
--- a/src/state/models/user-follows-view.ts
+++ b/src/state/models/user-follows-view.ts
@@ -16,7 +16,12 @@ export class UserFollowsViewModel {
   params: GetFollows.QueryParams
 
   // data
-  subject: Subject = {did: '', handle: '', displayName: ''}
+  subject: Subject = {
+    did: '',
+    handle: '',
+    displayName: '',
+    declaration: {cid: '', actorType: ''},
+  }
   follows: FollowItem[] = []
 
   constructor(