diff options
Diffstat (limited to 'src/state/models')
-rw-r--r-- | src/state/models/invited-users.ts | 70 | ||||
-rw-r--r-- | src/state/models/me.ts | 90 | ||||
-rw-r--r-- | src/state/models/root-store.ts | 8 | ||||
-rw-r--r-- | src/state/models/ui/shell.ts | 5 |
4 files changed, 154 insertions, 19 deletions
diff --git a/src/state/models/invited-users.ts b/src/state/models/invited-users.ts new file mode 100644 index 000000000..121161a32 --- /dev/null +++ b/src/state/models/invited-users.ts @@ -0,0 +1,70 @@ +import {makeAutoObservable, runInAction} from 'mobx' +import {ComAtprotoServerDefs, AppBskyActorDefs} from '@atproto/api' +import {RootStoreModel} from './root-store' +import {isObj, hasProp, isStrArray} from 'lib/type-guards' + +export class InvitedUsers { + seenDids: string[] = [] + profiles: AppBskyActorDefs.ProfileViewDetailed[] = [] + + get numNotifs() { + return this.profiles.length + } + + constructor(public rootStore: RootStoreModel) { + makeAutoObservable( + this, + {rootStore: false, serialize: false, hydrate: false}, + {autoBind: true}, + ) + } + + serialize() { + return {seenDids: this.seenDids} + } + + hydrate(v: unknown) { + if (isObj(v) && hasProp(v, 'seenDids') && isStrArray(v.seenDids)) { + this.seenDids = v.seenDids + } + } + + async fetch(invites: ComAtprotoServerDefs.InviteCode[]) { + // pull the dids of invited users not marked seen + const dids = [] + for (const invite of invites) { + for (const use of invite.uses) { + if (!this.seenDids.includes(use.usedBy)) { + dids.push(use.usedBy) + } + } + } + + // fetch their profiles + this.profiles = [] + if (dids.length) { + try { + const res = await this.rootStore.agent.app.bsky.actor.getProfiles({ + actors: dids, + }) + runInAction(() => { + // save the ones following -- these are the ones we want to notify the user about + this.profiles = res.data.profiles.filter( + profile => !profile.viewer?.following, + ) + }) + this.rootStore.me.follows.hydrateProfiles(this.profiles) + } catch (e) { + this.rootStore.log.error( + 'Failed to fetch profiles for invited users', + e, + ) + } + } + } + + markSeen(did: string) { + this.seenDids.push(did) + this.profiles = this.profiles.filter(profile => profile.did !== did) + } +} diff --git a/src/state/models/me.ts b/src/state/models/me.ts index 3adbc7c6c..1dcccb6f1 100644 --- a/src/state/models/me.ts +++ b/src/state/models/me.ts @@ -1,10 +1,13 @@ import {makeAutoObservable, runInAction} from 'mobx' +import {ComAtprotoServerDefs} from '@atproto/api' import {RootStoreModel} from './root-store' import {PostsFeedModel} from './feeds/posts' import {NotificationsFeedModel} from './feeds/notifications' import {MyFollowsCache} from './cache/my-follows' import {isObj, hasProp} from 'lib/type-guards' +const PROFILE_UPDATE_INTERVAL = 10 * 60 * 1e3 // 10min + export class MeModel { did: string = '' handle: string = '' @@ -16,6 +19,12 @@ export class MeModel { mainFeed: PostsFeedModel notifications: NotificationsFeedModel follows: MyFollowsCache + invites: ComAtprotoServerDefs.InviteCode[] = [] + lastProfileStateUpdate = Date.now() + + get invitesAvailable() { + return this.invites.filter(isInviteAvailable).length + } constructor(public rootStore: RootStoreModel) { makeAutoObservable( @@ -39,6 +48,7 @@ export class MeModel { this.displayName = '' this.description = '' this.avatar = '' + this.invites = [] } serialize(): unknown { @@ -85,24 +95,7 @@ export class MeModel { if (sess.hasSession) { this.did = sess.currentSession?.did || '' this.handle = sess.currentSession?.handle || '' - const profile = await this.rootStore.agent.getProfile({ - actor: this.did, - }) - runInAction(() => { - if (profile?.data) { - this.displayName = profile.data.displayName || '' - this.description = profile.data.description || '' - this.avatar = profile.data.avatar || '' - this.followsCount = profile.data.followsCount - this.followersCount = profile.data.followersCount - } else { - this.displayName = '' - this.description = '' - this.avatar = '' - this.followsCount = profile.data.followsCount - this.followersCount = undefined - } - }) + await this.fetchProfile() this.mainFeed.clear() await Promise.all([ this.mainFeed.setup().catch(e => { @@ -113,8 +106,69 @@ export class MeModel { }), ]) this.rootStore.emitSessionLoaded() + await this.fetchInviteCodes() } else { this.clear() } } + + async updateIfNeeded() { + if (Date.now() - this.lastProfileStateUpdate > PROFILE_UPDATE_INTERVAL) { + this.rootStore.log.debug('Updating me profile information') + await this.fetchProfile() + await this.fetchInviteCodes() + } + await this.notifications.loadUnreadCount() + } + + async fetchProfile() { + const profile = await this.rootStore.agent.getProfile({ + actor: this.did, + }) + runInAction(() => { + if (profile?.data) { + this.displayName = profile.data.displayName || '' + this.description = profile.data.description || '' + this.avatar = profile.data.avatar || '' + this.followsCount = profile.data.followsCount + this.followersCount = profile.data.followersCount + } else { + this.displayName = '' + this.description = '' + this.avatar = '' + this.followsCount = profile.data.followsCount + this.followersCount = undefined + } + }) + } + + async fetchInviteCodes() { + if (this.rootStore.session) { + try { + const res = + await this.rootStore.agent.com.atproto.server.getAccountInviteCodes( + {}, + ) + runInAction(() => { + this.invites = res.data.codes + this.invites.sort((a, b) => { + if (!isInviteAvailable(a)) { + return 1 + } + if (!isInviteAvailable(b)) { + return -1 + } + return 0 + }) + }) + } catch (e) { + this.rootStore.log.error('Failed to fetch user invite codes', e) + } + await this.rootStore.invitedUsers.fetch(this.invites) + } + } +} + +function isInviteAvailable(invite: ComAtprotoServerDefs.InviteCode): boolean { + return invite.available - invite.uses.length > 0 && !invite.disabled } diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts index 0d893415f..9207f27ba 100644 --- a/src/state/models/root-store.ts +++ b/src/state/models/root-store.ts @@ -16,6 +16,7 @@ import {ProfilesCache} from './cache/profiles-view' import {LinkMetasCache} from './cache/link-metas' import {NotificationsFeedItemModel} from './feeds/notifications' import {MeModel} from './me' +import {InvitedUsers} from './invited-users' import {PreferencesModel} from './ui/preferences' import {resetToTab} from '../../Navigation' import {ImageSizesCache} from './cache/image-sizes' @@ -36,6 +37,7 @@ export class RootStoreModel { shell = new ShellUiModel(this) preferences = new PreferencesModel() me = new MeModel(this) + invitedUsers = new InvitedUsers(this) profiles = new ProfilesCache(this) linkMetas = new LinkMetasCache(this) imageSizes = new ImageSizesCache() @@ -61,6 +63,7 @@ export class RootStoreModel { me: this.me.serialize(), shell: this.shell.serialize(), preferences: this.preferences.serialize(), + invitedUsers: this.invitedUsers.serialize(), } } @@ -84,6 +87,9 @@ export class RootStoreModel { if (hasProp(v, 'preferences')) { this.preferences.hydrate(v.preferences) } + if (hasProp(v, 'invitedUsers')) { + this.invitedUsers.hydrate(v.invitedUsers) + } } } @@ -141,7 +147,7 @@ export class RootStoreModel { return } try { - await this.me.notifications.loadUnreadCount() + await this.me.updateIfNeeded() } catch (e: any) { this.log.error('Failed to fetch latest state', e) } diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts index b782dd2f7..917e7a09f 100644 --- a/src/state/models/ui/shell.ts +++ b/src/state/models/ui/shell.ts @@ -61,6 +61,10 @@ export interface WaitlistModal { name: 'waitlist' } +export interface InviteCodesModal { + name: 'invite-codes' +} + export type Modal = | ConfirmModal | EditProfileModal @@ -72,6 +76,7 @@ export type Modal = | RepostModal | ChangeHandleModal | WaitlistModal + | InviteCodesModal interface LightboxModel {} |