Add user invite codes (#393)
* Add mobile UIs for invite codes * Update invite code UIs for web * Finish implementing invite code behaviors (including notifications of invited users) * Bump deps * Update web right nav to use real data; also fix lint
This commit is contained in:
parent
8e28d3c6be
commit
ea04c2bd33
26 changed files with 932 additions and 246 deletions
70
src/state/models/invited-users.ts
Normal file
70
src/state/models/invited-users.ts
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue