Remove scenes (#36)

* Remove scenes from the main menu

* Remove scenes from the profile view

* Remove 'scenes explainer' from onboarding flow

* Remove scene-related modals

* Remove member/membership code

* Remove all scenes-related items from notifications

* Remove scene-related code from posts feed

* Remove scene-related API helpers

* Update tests
This commit is contained in:
Paul Frazee 2023-01-17 10:11:30 -06:00 committed by GitHub
parent 5abcc8e336
commit bf1092ad86
29 changed files with 18 additions and 1714 deletions

View file

@ -216,54 +216,6 @@ export async function unfollow(store: RootStoreModel, followUri: string) {
})
}
export async function inviteToScene(
store: RootStoreModel,
sceneDid: string,
subjectDid: string,
subjectDeclarationCid: string,
): Promise<string> {
const res = await store.api.app.bsky.graph.assertion.create(
{
did: sceneDid,
},
{
subject: {
did: subjectDid,
declarationCid: subjectDeclarationCid,
},
assertion: APP_BSKY_GRAPH.AssertMember,
createdAt: new Date().toISOString(),
},
)
return res.uri
}
interface Confirmation {
originator: {
did: string
declarationCid: string
}
assertion: {
uri: string
cid: string
}
}
export async function acceptSceneInvite(
store: RootStoreModel,
details: Confirmation,
): Promise<string> {
const res = await store.api.app.bsky.graph.confirmation.create(
{
did: store.me.did || '',
},
{
...details,
createdAt: new Date().toISOString(),
},
)
return res.uri
}
interface FetchHandlerResponse {
status: number
headers: Record<string, string>

View file

@ -6,7 +6,6 @@ import {
AppBskyFeedGetAuthorFeed as GetAuthorFeed,
} from '@atproto/api'
type FeedViewPost = AppBskyFeedFeedViewPost.Main
type ReasonTrend = AppBskyFeedFeedViewPost.ReasonTrend
type ReasonRepost = AppBskyFeedFeedViewPost.ReasonRepost
type PostView = AppBskyFeedPost.View
import {AtUri} from '../../third-party/uri'
@ -94,12 +93,6 @@ export class FeedItemModel {
}
}
get reasonTrend(): ReasonTrend | undefined {
if (this.reason?.$type === 'app.bsky.feed.feedViewPost#reasonTrend') {
return this.reason as ReasonTrend
}
}
async toggleUpvote() {
const wasUpvoted = !!this.post.viewer.upvote
const wasDownvoted = !!this.post.viewer.downvote
@ -494,10 +487,9 @@ export class FeedModel {
private _updateAll(res: GetTimeline.Response | GetAuthorFeed.Response) {
for (const item of res.data.feed) {
const existingItem = this.feed.find(
// HACK: need to find the reposts and trends item, so we have to check for that -prf
// HACK: need to find the reposts' item, so we have to check for that -prf
item2 =>
item.post.uri === item2.post.uri &&
item.reason?.$trend === item2.reason?.$trend &&
// @ts-ignore todo
item.reason?.by?.did === item2.reason?.by?.did,
)

View file

@ -1,7 +1,6 @@
import {makeAutoObservable, runInAction} from 'mobx'
import {RootStoreModel} from './root-store'
import {FeedModel} from './feed-view'
import {MembershipsViewModel} from './memberships-view'
import {NotificationsViewModel} from './notifications-view'
import {isObj, hasProp} from '../lib/type-guards'
@ -12,7 +11,6 @@ export class MeModel {
description: string = ''
avatar: string = ''
notificationCount: number = 0
memberships?: MembershipsViewModel
mainFeed: FeedModel
notifications: NotificationsViewModel
@ -35,7 +33,6 @@ export class MeModel {
this.description = ''
this.avatar = ''
this.notificationCount = 0
this.memberships = undefined
}
serialize(): unknown {
@ -99,13 +96,7 @@ export class MeModel {
algorithm: 'reverse-chronological',
})
this.notifications = new NotificationsViewModel(this.rootStore, {})
this.memberships = new MembershipsViewModel(this.rootStore, {
actor: this.did,
})
await Promise.all([
this.memberships?.setup().catch(e => {
this.rootStore.log.error('Failed to setup memberships model', e)
}),
this.mainFeed.setup().catch(e => {
this.rootStore.log.error('Failed to setup main feed model', e)
}),
@ -133,8 +124,4 @@ export class MeModel {
}
})
}
async refreshMemberships() {
return this.memberships?.refresh()
}
}

View file

@ -1,149 +0,0 @@
import {makeAutoObservable, runInAction} from 'mobx'
import {
AppBskyGraphGetMembers as GetMembers,
AppBskyActorRef as ActorRef,
APP_BSKY_GRAPH,
} from '@atproto/api'
import {AtUri} from '../../third-party/uri'
import {RootStoreModel} from './root-store'
export type MemberItem = GetMembers.Member & {
_reactKey: string
}
export class MembersViewModel {
// state
isLoading = false
isRefreshing = false
hasLoaded = false
error = ''
params: GetMembers.QueryParams
// data
subject: ActorRef.WithInfo = {
did: '',
handle: '',
displayName: '',
declaration: {cid: '', actorType: ''},
avatar: undefined,
}
members: MemberItem[] = []
constructor(
public rootStore: RootStoreModel,
params: GetMembers.QueryParams,
) {
makeAutoObservable(
this,
{
rootStore: false,
params: false,
},
{autoBind: true},
)
this.params = params
}
get hasContent() {
return this.members.length !== 0
}
get hasError() {
return this.error !== ''
}
get isEmpty() {
return this.hasLoaded && !this.hasContent
}
isMember(did: string) {
return this.members.find(member => member.did === did)
}
// public api
// =
async setup() {
await this._fetch()
}
async refresh() {
await this._fetch(true)
}
async loadMore() {
// TODO
}
async removeMember(did: string) {
const assertsRes = await this.rootStore.api.app.bsky.graph.getAssertions({
author: this.subject.did,
subject: did,
assertion: APP_BSKY_GRAPH.AssertMember,
})
if (assertsRes.data.assertions.length < 1) {
throw new Error('Could not find membership record')
}
for (const assert of assertsRes.data.assertions) {
await this.rootStore.api.app.bsky.graph.assertion.delete({
did: this.subject.did,
rkey: new AtUri(assert.uri).rkey,
})
}
runInAction(() => {
this.members = this.members.filter(m => m.did !== did)
})
}
// state transitions
// =
private _xLoading(isRefreshing = false) {
this.isLoading = true
this.isRefreshing = isRefreshing
this.error = ''
}
private _xIdle(err?: any) {
this.isLoading = false
this.isRefreshing = false
this.hasLoaded = true
this.error = err ? err.toString() : ''
if (err) {
this.rootStore.log.error('Failed to fetch members', err)
}
}
// loader functions
// =
private async _fetch(isRefreshing = false) {
this._xLoading(isRefreshing)
try {
const res = await this.rootStore.api.app.bsky.graph.getMembers(
this.params,
)
this._replaceAll(res)
this._xIdle()
} catch (e: any) {
this._xIdle(e)
}
}
private _replaceAll(res: GetMembers.Response) {
this.subject.did = res.data.subject.did
this.subject.handle = res.data.subject.handle
this.subject.displayName = res.data.subject.displayName
this.subject.declaration = res.data.subject.declaration
this.subject.avatar = res.data.subject.avatar
this.members.length = 0
let counter = 0
for (const item of res.data.members) {
this._append({_reactKey: `item-${counter++}`, ...item})
}
}
private _append(item: MemberItem) {
this.members.push(item)
}
}

View file

@ -1,127 +0,0 @@
import {makeAutoObservable} from 'mobx'
import {
AppBskyGraphGetMemberships as GetMemberships,
AppBskyActorRef as ActorRef,
} from '@atproto/api'
import {RootStoreModel} from './root-store'
export type MembershipItem = GetMemberships.Membership & {
_reactKey: string
}
export class MembershipsViewModel {
// state
isLoading = false
isRefreshing = false
hasLoaded = false
error = ''
params: GetMemberships.QueryParams
// data
subject: ActorRef.WithInfo = {
did: '',
handle: '',
displayName: '',
declaration: {cid: '', actorType: ''},
avatar: undefined,
}
memberships: MembershipItem[] = []
constructor(
public rootStore: RootStoreModel,
params: GetMemberships.QueryParams,
) {
makeAutoObservable(
this,
{
rootStore: false,
params: false,
},
{autoBind: true},
)
this.params = params
}
get hasContent() {
return this.memberships.length !== 0
}
get hasError() {
return this.error !== ''
}
get isEmpty() {
return this.hasLoaded && !this.hasContent
}
isMemberOf(did: string) {
return !!this.memberships.find(m => m.did === did)
}
// public api
// =
async setup() {
await this._fetch()
}
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?: any) {
this.isLoading = false
this.isRefreshing = false
this.hasLoaded = true
this.error = err ? err.toString() : ''
if (err) {
this.rootStore.log.error('Failed to fetch memberships', err)
}
}
// loader functions
// =
private async _fetch(isRefreshing = false) {
this._xLoading(isRefreshing)
try {
const res = await this.rootStore.api.app.bsky.graph.getMemberships(
this.params,
)
this._replaceAll(res)
this._xIdle()
} catch (e: any) {
this._xIdle(e)
}
}
private _replaceAll(res: GetMemberships.Response) {
this.subject.did = res.data.subject.did
this.subject.handle = res.data.subject.handle
this.subject.displayName = res.data.subject.displayName
this.subject.declaration = res.data.subject.declaration
this.subject.avatar = res.data.subject.avatar
this.memberships.length = 0
let counter = 0
for (const item of res.data.memberships) {
this._append({_reactKey: `item-${counter++}`, ...item})
}
}
private _append(item: MembershipItem) {
this.memberships.push(item)
}
}

View file

@ -4,17 +4,15 @@ import {
AppBskyActorRef as ActorRef,
AppBskyFeedPost,
AppBskyFeedRepost,
AppBskyFeedTrend,
AppBskyFeedVote,
AppBskyGraphAssertion,
AppBskyGraphFollow,
APP_BSKY_GRAPH,
} from '@atproto/api'
import {RootStoreModel} from './root-store'
import {PostThreadViewModel} from './post-thread-view'
import {cleanError} from '../../lib/strings'
const UNGROUPABLE_REASONS = ['trend', 'assertion']
const UNGROUPABLE_REASONS = ['assertion']
const PAGE_SIZE = 30
const MS_60MIN = 1e3 * 60 * 60
@ -27,7 +25,6 @@ export interface GroupedNotification extends ListNotifications.Notification {
type SupportedRecord =
| AppBskyFeedPost.Record
| AppBskyFeedRepost.Record
| AppBskyFeedTrend.Record
| AppBskyFeedVote.Record
| AppBskyGraphAssertion.Record
| AppBskyGraphFollow.Record
@ -94,10 +91,6 @@ export class NotificationsViewItemModel {
return this.reason === 'repost'
}
get isTrend() {
return this.reason === 'trend'
}
get isMention() {
return this.reason === 'mention'
}
@ -115,26 +108,12 @@ export class NotificationsViewItemModel {
}
get needsAdditionalData() {
if (
this.isUpvote ||
this.isRepost ||
this.isTrend ||
this.isReply ||
this.isMention
) {
if (this.isUpvote || this.isRepost || this.isReply || this.isMention) {
return !this.additionalPost
}
return false
}
get isInvite() {
return (
this.isAssertion &&
AppBskyGraphAssertion.isRecord(this.record) &&
this.record.assertion === APP_BSKY_GRAPH.AssertMember
)
}
get subjectUri(): string {
if (this.reasonSubject) {
return this.reasonSubject
@ -142,7 +121,6 @@ export class NotificationsViewItemModel {
const record = this.record
if (
AppBskyFeedRepost.isRecord(record) ||
AppBskyFeedTrend.isRecord(record) ||
AppBskyFeedVote.isRecord(record)
) {
return record.subject.uri
@ -154,7 +132,6 @@ export class NotificationsViewItemModel {
for (const ns of [
AppBskyFeedPost,
AppBskyFeedRepost,
AppBskyFeedTrend,
AppBskyFeedVote,
AppBskyGraphAssertion,
AppBskyGraphFollow,
@ -185,7 +162,7 @@ export class NotificationsViewItemModel {
let postUri
if (this.isReply || this.isMention) {
postUri = this.uri
} else if (this.isUpvote || this.isRepost || this.isTrend) {
} else if (this.isUpvote || this.isRepost) {
postUri = this.subjectUri
}
if (postUri) {

View file

@ -1,24 +1,14 @@
import {makeAutoObservable} from 'mobx'
import {RootStoreModel} from './root-store'
import {ProfileViewModel} from './profile-view'
import {MembersViewModel} from './members-view'
import {MembershipsViewModel} from './memberships-view'
import {FeedModel} from './feed-view'
export enum Sections {
Posts = 'Posts',
PostsWithReplies = 'Posts & replies',
Scenes = 'Scenes',
Trending = 'Trending',
Members = 'Members',
}
const USER_SELECTOR_ITEMS = [
Sections.Posts,
Sections.PostsWithReplies,
Sections.Scenes,
]
const SCENE_SELECTOR_ITEMS = [Sections.Trending, Sections.Members]
const USER_SELECTOR_ITEMS = [Sections.Posts, Sections.PostsWithReplies]
export interface ProfileUiParams {
user: string
@ -28,8 +18,6 @@ export class ProfileUiModel {
// data
profile: ProfileViewModel
feed: FeedModel
memberships: MembershipsViewModel
members: MembersViewModel
// ui state
selectedViewIndex = 0
@ -51,24 +39,15 @@ export class ProfileUiModel {
author: params.user,
limit: 10,
})
this.memberships = new MembershipsViewModel(rootStore, {actor: params.user})
this.members = new MembersViewModel(rootStore, {actor: params.user})
}
get currentView(): FeedModel | MembershipsViewModel | MembersViewModel {
get currentView(): FeedModel {
if (
this.selectedView === Sections.Posts ||
this.selectedView === Sections.PostsWithReplies ||
this.selectedView === Sections.Trending
this.selectedView === Sections.PostsWithReplies
) {
return this.feed
}
if (this.selectedView === Sections.Scenes) {
return this.memberships
}
if (this.selectedView === Sections.Members) {
return this.members
}
throw new Error(`Invalid selector value: ${this.selectedViewIndex}`)
}
@ -85,15 +64,9 @@ export class ProfileUiModel {
return this.profile.isUser
}
get isScene() {
return this.profile.isScene
}
get selectorItems() {
if (this.isUser) {
return USER_SELECTOR_ITEMS
} else if (this.isScene) {
return SCENE_SELECTOR_ITEMS
} else {
return USER_SELECTOR_ITEMS
}
@ -119,16 +92,6 @@ export class ProfileUiModel {
.setup()
.catch(err => this.rootStore.log.error('Failed to fetch feed', err)),
])
if (this.isUser) {
await this.memberships
.setup()
.catch(err => this.rootStore.log.error('Failed to fetch members', err))
}
if (this.isScene) {
await this.members
.setup()
.catch(err => this.rootStore.log.error('Failed to fetch members', err))
}
}
async update() {

View file

@ -13,11 +13,9 @@ import {RootStoreModel} from './root-store'
import * as apilib from '../lib/api'
export const ACTOR_TYPE_USER = 'app.bsky.system.actorUser'
export const ACTOR_TYPE_SCENE = 'app.bsky.system.actorScene'
export class ProfileViewMyStateModel {
follow?: string
member?: string
muted?: boolean
constructor() {
@ -47,7 +45,6 @@ export class ProfileViewModel {
banner?: string
followersCount: number = 0
followsCount: number = 0
membersCount: number = 0
postsCount: number = 0
myState = new ProfileViewMyStateModel()
@ -85,10 +82,6 @@ export class ProfileViewModel {
return this.declaration.actorType === ACTOR_TYPE_USER
}
get isScene() {
return this.declaration.actorType === ACTOR_TYPE_SCENE
}
// public api
// =
@ -216,7 +209,6 @@ export class ProfileViewModel {
this.banner = res.data.banner
this.followersCount = res.data.followersCount
this.followsCount = res.data.followsCount
this.membersCount = res.data.membersCount
this.postsCount = res.data.postsCount
if (res.data.myState) {
Object.assign(this.myState, res.data.myState)

View file

@ -25,22 +25,6 @@ export class EditProfileModal {
}
}
export class CreateSceneModal {
name = 'create-scene'
constructor() {
makeAutoObservable(this)
}
}
export class InviteToSceneModal {
name = 'invite-to-scene'
constructor(public profileView: ProfileViewModel) {
makeAutoObservable(this)
}
}
export class ServerInputModal {
name = 'server-input'
@ -143,7 +127,6 @@ export class ShellUiModel {
activeModal:
| ConfirmModal
| EditProfileModal
| CreateSceneModal
| ServerInputModal
| ReportPostModal
| ReportAccountModal
@ -191,7 +174,6 @@ export class ShellUiModel {
modal:
| ConfirmModal
| EditProfileModal
| CreateSceneModal
| ServerInputModal
| ReportPostModal
| ReportAccountModal,

View file

@ -1,142 +0,0 @@
import {makeAutoObservable, runInAction} from 'mobx'
import {RootStoreModel} from './root-store'
import {UserFollowsViewModel, FollowItem} from './user-follows-view'
import {GetAssertionsView} from './get-assertions-view'
import {APP_BSKY_SYSTEM, APP_BSKY_GRAPH} from '@atproto/api'
export interface SuggestedInvitesViewParams {
sceneDid: string
}
export class SuggestedInvitesView {
// state
isLoading = false
isRefreshing = false
hasLoaded = false
error = ''
params: SuggestedInvitesViewParams
sceneAssertionsView: GetAssertionsView
myFollowsView: UserFollowsViewModel
// data
suggestions: FollowItem[] = []
constructor(
public rootStore: RootStoreModel,
params: SuggestedInvitesViewParams,
) {
makeAutoObservable(
this,
{
rootStore: false,
params: false,
},
{autoBind: true},
)
this.params = params
this.sceneAssertionsView = new GetAssertionsView(rootStore, {
author: params.sceneDid,
assertion: APP_BSKY_GRAPH.AssertMember,
})
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
}
get unconfirmed() {
return this.sceneAssertionsView.unconfirmed
}
// 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?: any) {
this.isLoading = false
this.isRefreshing = false
this.hasLoaded = true
this.error = err ? err.toString() : ''
if (err) {
this.rootStore.log.error('Failed to fetch suggested invites', err)
}
}
// loader functions
// =
private async _fetch(isRefreshing = false) {
this._xLoading(isRefreshing)
try {
// TODO need to fetch all!
await this.sceneAssertionsView.setup()
} catch (e: any) {
this.rootStore.log.error(
'Failed to fetch current scene members in suggested invites',
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: any) {
this.rootStore.log.error(
'Failed to fetch current followers in suggested invites',
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) {
if (follow.declaration.actorType !== APP_BSKY_SYSTEM.ActorUser) {
continue
}
if (!this.sceneAssertionsView.getBySubject(follow.did)) {
newSuggestions.push(follow)
}
}
runInAction(() => {
this.suggestions = newSuggestions
})
this._xIdle()
}
}