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 testszio/stable
parent
5abcc8e336
commit
bf1092ad86
|
@ -57,14 +57,4 @@ describe('Menu', () => {
|
||||||
expect(onCloseMock).toHaveBeenCalled()
|
expect(onCloseMock).toHaveBeenCalled()
|
||||||
expect(mockedNavigationStore.switchTo).toHaveBeenCalledWith(1, true)
|
expect(mockedNavigationStore.switchTo).toHaveBeenCalledWith(1, true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('presses new scene button', () => {
|
|
||||||
const {getAllByTestId} = render(<Menu {...mockedProps} />)
|
|
||||||
|
|
||||||
const menuItemButton = getAllByTestId('menuItemButton')
|
|
||||||
fireEvent.press(menuItemButton[3])
|
|
||||||
|
|
||||||
expect(onCloseMock).toHaveBeenCalled()
|
|
||||||
expect(mockedShellStore.openModal).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 122 KiB |
|
@ -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 {
|
interface FetchHandlerResponse {
|
||||||
status: number
|
status: number
|
||||||
headers: Record<string, string>
|
headers: Record<string, string>
|
||||||
|
|
|
@ -6,7 +6,6 @@ import {
|
||||||
AppBskyFeedGetAuthorFeed as GetAuthorFeed,
|
AppBskyFeedGetAuthorFeed as GetAuthorFeed,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
type FeedViewPost = AppBskyFeedFeedViewPost.Main
|
type FeedViewPost = AppBskyFeedFeedViewPost.Main
|
||||||
type ReasonTrend = AppBskyFeedFeedViewPost.ReasonTrend
|
|
||||||
type ReasonRepost = AppBskyFeedFeedViewPost.ReasonRepost
|
type ReasonRepost = AppBskyFeedFeedViewPost.ReasonRepost
|
||||||
type PostView = AppBskyFeedPost.View
|
type PostView = AppBskyFeedPost.View
|
||||||
import {AtUri} from '../../third-party/uri'
|
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() {
|
async toggleUpvote() {
|
||||||
const wasUpvoted = !!this.post.viewer.upvote
|
const wasUpvoted = !!this.post.viewer.upvote
|
||||||
const wasDownvoted = !!this.post.viewer.downvote
|
const wasDownvoted = !!this.post.viewer.downvote
|
||||||
|
@ -494,10 +487,9 @@ export class FeedModel {
|
||||||
private _updateAll(res: GetTimeline.Response | GetAuthorFeed.Response) {
|
private _updateAll(res: GetTimeline.Response | GetAuthorFeed.Response) {
|
||||||
for (const item of res.data.feed) {
|
for (const item of res.data.feed) {
|
||||||
const existingItem = this.feed.find(
|
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 =>
|
item2 =>
|
||||||
item.post.uri === item2.post.uri &&
|
item.post.uri === item2.post.uri &&
|
||||||
item.reason?.$trend === item2.reason?.$trend &&
|
|
||||||
// @ts-ignore todo
|
// @ts-ignore todo
|
||||||
item.reason?.by?.did === item2.reason?.by?.did,
|
item.reason?.by?.did === item2.reason?.by?.did,
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import {makeAutoObservable, runInAction} from 'mobx'
|
import {makeAutoObservable, runInAction} from 'mobx'
|
||||||
import {RootStoreModel} from './root-store'
|
import {RootStoreModel} from './root-store'
|
||||||
import {FeedModel} from './feed-view'
|
import {FeedModel} from './feed-view'
|
||||||
import {MembershipsViewModel} from './memberships-view'
|
|
||||||
import {NotificationsViewModel} from './notifications-view'
|
import {NotificationsViewModel} from './notifications-view'
|
||||||
import {isObj, hasProp} from '../lib/type-guards'
|
import {isObj, hasProp} from '../lib/type-guards'
|
||||||
|
|
||||||
|
@ -12,7 +11,6 @@ export class MeModel {
|
||||||
description: string = ''
|
description: string = ''
|
||||||
avatar: string = ''
|
avatar: string = ''
|
||||||
notificationCount: number = 0
|
notificationCount: number = 0
|
||||||
memberships?: MembershipsViewModel
|
|
||||||
mainFeed: FeedModel
|
mainFeed: FeedModel
|
||||||
notifications: NotificationsViewModel
|
notifications: NotificationsViewModel
|
||||||
|
|
||||||
|
@ -35,7 +33,6 @@ export class MeModel {
|
||||||
this.description = ''
|
this.description = ''
|
||||||
this.avatar = ''
|
this.avatar = ''
|
||||||
this.notificationCount = 0
|
this.notificationCount = 0
|
||||||
this.memberships = undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
serialize(): unknown {
|
serialize(): unknown {
|
||||||
|
@ -99,13 +96,7 @@ export class MeModel {
|
||||||
algorithm: 'reverse-chronological',
|
algorithm: 'reverse-chronological',
|
||||||
})
|
})
|
||||||
this.notifications = new NotificationsViewModel(this.rootStore, {})
|
this.notifications = new NotificationsViewModel(this.rootStore, {})
|
||||||
this.memberships = new MembershipsViewModel(this.rootStore, {
|
|
||||||
actor: this.did,
|
|
||||||
})
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.memberships?.setup().catch(e => {
|
|
||||||
this.rootStore.log.error('Failed to setup memberships model', e)
|
|
||||||
}),
|
|
||||||
this.mainFeed.setup().catch(e => {
|
this.mainFeed.setup().catch(e => {
|
||||||
this.rootStore.log.error('Failed to setup main feed model', 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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,17 +4,15 @@ import {
|
||||||
AppBskyActorRef as ActorRef,
|
AppBskyActorRef as ActorRef,
|
||||||
AppBskyFeedPost,
|
AppBskyFeedPost,
|
||||||
AppBskyFeedRepost,
|
AppBskyFeedRepost,
|
||||||
AppBskyFeedTrend,
|
|
||||||
AppBskyFeedVote,
|
AppBskyFeedVote,
|
||||||
AppBskyGraphAssertion,
|
AppBskyGraphAssertion,
|
||||||
AppBskyGraphFollow,
|
AppBskyGraphFollow,
|
||||||
APP_BSKY_GRAPH,
|
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {RootStoreModel} from './root-store'
|
import {RootStoreModel} from './root-store'
|
||||||
import {PostThreadViewModel} from './post-thread-view'
|
import {PostThreadViewModel} from './post-thread-view'
|
||||||
import {cleanError} from '../../lib/strings'
|
import {cleanError} from '../../lib/strings'
|
||||||
|
|
||||||
const UNGROUPABLE_REASONS = ['trend', 'assertion']
|
const UNGROUPABLE_REASONS = ['assertion']
|
||||||
const PAGE_SIZE = 30
|
const PAGE_SIZE = 30
|
||||||
const MS_60MIN = 1e3 * 60 * 60
|
const MS_60MIN = 1e3 * 60 * 60
|
||||||
|
|
||||||
|
@ -27,7 +25,6 @@ export interface GroupedNotification extends ListNotifications.Notification {
|
||||||
type SupportedRecord =
|
type SupportedRecord =
|
||||||
| AppBskyFeedPost.Record
|
| AppBskyFeedPost.Record
|
||||||
| AppBskyFeedRepost.Record
|
| AppBskyFeedRepost.Record
|
||||||
| AppBskyFeedTrend.Record
|
|
||||||
| AppBskyFeedVote.Record
|
| AppBskyFeedVote.Record
|
||||||
| AppBskyGraphAssertion.Record
|
| AppBskyGraphAssertion.Record
|
||||||
| AppBskyGraphFollow.Record
|
| AppBskyGraphFollow.Record
|
||||||
|
@ -94,10 +91,6 @@ export class NotificationsViewItemModel {
|
||||||
return this.reason === 'repost'
|
return this.reason === 'repost'
|
||||||
}
|
}
|
||||||
|
|
||||||
get isTrend() {
|
|
||||||
return this.reason === 'trend'
|
|
||||||
}
|
|
||||||
|
|
||||||
get isMention() {
|
get isMention() {
|
||||||
return this.reason === 'mention'
|
return this.reason === 'mention'
|
||||||
}
|
}
|
||||||
|
@ -115,26 +108,12 @@ export class NotificationsViewItemModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
get needsAdditionalData() {
|
get needsAdditionalData() {
|
||||||
if (
|
if (this.isUpvote || this.isRepost || this.isReply || this.isMention) {
|
||||||
this.isUpvote ||
|
|
||||||
this.isRepost ||
|
|
||||||
this.isTrend ||
|
|
||||||
this.isReply ||
|
|
||||||
this.isMention
|
|
||||||
) {
|
|
||||||
return !this.additionalPost
|
return !this.additionalPost
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
get isInvite() {
|
|
||||||
return (
|
|
||||||
this.isAssertion &&
|
|
||||||
AppBskyGraphAssertion.isRecord(this.record) &&
|
|
||||||
this.record.assertion === APP_BSKY_GRAPH.AssertMember
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
get subjectUri(): string {
|
get subjectUri(): string {
|
||||||
if (this.reasonSubject) {
|
if (this.reasonSubject) {
|
||||||
return this.reasonSubject
|
return this.reasonSubject
|
||||||
|
@ -142,7 +121,6 @@ export class NotificationsViewItemModel {
|
||||||
const record = this.record
|
const record = this.record
|
||||||
if (
|
if (
|
||||||
AppBskyFeedRepost.isRecord(record) ||
|
AppBskyFeedRepost.isRecord(record) ||
|
||||||
AppBskyFeedTrend.isRecord(record) ||
|
|
||||||
AppBskyFeedVote.isRecord(record)
|
AppBskyFeedVote.isRecord(record)
|
||||||
) {
|
) {
|
||||||
return record.subject.uri
|
return record.subject.uri
|
||||||
|
@ -154,7 +132,6 @@ export class NotificationsViewItemModel {
|
||||||
for (const ns of [
|
for (const ns of [
|
||||||
AppBskyFeedPost,
|
AppBskyFeedPost,
|
||||||
AppBskyFeedRepost,
|
AppBskyFeedRepost,
|
||||||
AppBskyFeedTrend,
|
|
||||||
AppBskyFeedVote,
|
AppBskyFeedVote,
|
||||||
AppBskyGraphAssertion,
|
AppBskyGraphAssertion,
|
||||||
AppBskyGraphFollow,
|
AppBskyGraphFollow,
|
||||||
|
@ -185,7 +162,7 @@ export class NotificationsViewItemModel {
|
||||||
let postUri
|
let postUri
|
||||||
if (this.isReply || this.isMention) {
|
if (this.isReply || this.isMention) {
|
||||||
postUri = this.uri
|
postUri = this.uri
|
||||||
} else if (this.isUpvote || this.isRepost || this.isTrend) {
|
} else if (this.isUpvote || this.isRepost) {
|
||||||
postUri = this.subjectUri
|
postUri = this.subjectUri
|
||||||
}
|
}
|
||||||
if (postUri) {
|
if (postUri) {
|
||||||
|
|
|
@ -1,24 +1,14 @@
|
||||||
import {makeAutoObservable} from 'mobx'
|
import {makeAutoObservable} from 'mobx'
|
||||||
import {RootStoreModel} from './root-store'
|
import {RootStoreModel} from './root-store'
|
||||||
import {ProfileViewModel} from './profile-view'
|
import {ProfileViewModel} from './profile-view'
|
||||||
import {MembersViewModel} from './members-view'
|
|
||||||
import {MembershipsViewModel} from './memberships-view'
|
|
||||||
import {FeedModel} from './feed-view'
|
import {FeedModel} from './feed-view'
|
||||||
|
|
||||||
export enum Sections {
|
export enum Sections {
|
||||||
Posts = 'Posts',
|
Posts = 'Posts',
|
||||||
PostsWithReplies = 'Posts & replies',
|
PostsWithReplies = 'Posts & replies',
|
||||||
Scenes = 'Scenes',
|
|
||||||
Trending = 'Trending',
|
|
||||||
Members = 'Members',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const USER_SELECTOR_ITEMS = [
|
const USER_SELECTOR_ITEMS = [Sections.Posts, Sections.PostsWithReplies]
|
||||||
Sections.Posts,
|
|
||||||
Sections.PostsWithReplies,
|
|
||||||
Sections.Scenes,
|
|
||||||
]
|
|
||||||
const SCENE_SELECTOR_ITEMS = [Sections.Trending, Sections.Members]
|
|
||||||
|
|
||||||
export interface ProfileUiParams {
|
export interface ProfileUiParams {
|
||||||
user: string
|
user: string
|
||||||
|
@ -28,8 +18,6 @@ export class ProfileUiModel {
|
||||||
// data
|
// data
|
||||||
profile: ProfileViewModel
|
profile: ProfileViewModel
|
||||||
feed: FeedModel
|
feed: FeedModel
|
||||||
memberships: MembershipsViewModel
|
|
||||||
members: MembersViewModel
|
|
||||||
|
|
||||||
// ui state
|
// ui state
|
||||||
selectedViewIndex = 0
|
selectedViewIndex = 0
|
||||||
|
@ -51,24 +39,15 @@ export class ProfileUiModel {
|
||||||
author: params.user,
|
author: params.user,
|
||||||
limit: 10,
|
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 (
|
if (
|
||||||
this.selectedView === Sections.Posts ||
|
this.selectedView === Sections.Posts ||
|
||||||
this.selectedView === Sections.PostsWithReplies ||
|
this.selectedView === Sections.PostsWithReplies
|
||||||
this.selectedView === Sections.Trending
|
|
||||||
) {
|
) {
|
||||||
return this.feed
|
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}`)
|
throw new Error(`Invalid selector value: ${this.selectedViewIndex}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,15 +64,9 @@ export class ProfileUiModel {
|
||||||
return this.profile.isUser
|
return this.profile.isUser
|
||||||
}
|
}
|
||||||
|
|
||||||
get isScene() {
|
|
||||||
return this.profile.isScene
|
|
||||||
}
|
|
||||||
|
|
||||||
get selectorItems() {
|
get selectorItems() {
|
||||||
if (this.isUser) {
|
if (this.isUser) {
|
||||||
return USER_SELECTOR_ITEMS
|
return USER_SELECTOR_ITEMS
|
||||||
} else if (this.isScene) {
|
|
||||||
return SCENE_SELECTOR_ITEMS
|
|
||||||
} else {
|
} else {
|
||||||
return USER_SELECTOR_ITEMS
|
return USER_SELECTOR_ITEMS
|
||||||
}
|
}
|
||||||
|
@ -119,16 +92,6 @@ export class ProfileUiModel {
|
||||||
.setup()
|
.setup()
|
||||||
.catch(err => this.rootStore.log.error('Failed to fetch feed', err)),
|
.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() {
|
async update() {
|
||||||
|
|
|
@ -13,11 +13,9 @@ import {RootStoreModel} from './root-store'
|
||||||
import * as apilib from '../lib/api'
|
import * as apilib from '../lib/api'
|
||||||
|
|
||||||
export const ACTOR_TYPE_USER = 'app.bsky.system.actorUser'
|
export const ACTOR_TYPE_USER = 'app.bsky.system.actorUser'
|
||||||
export const ACTOR_TYPE_SCENE = 'app.bsky.system.actorScene'
|
|
||||||
|
|
||||||
export class ProfileViewMyStateModel {
|
export class ProfileViewMyStateModel {
|
||||||
follow?: string
|
follow?: string
|
||||||
member?: string
|
|
||||||
muted?: boolean
|
muted?: boolean
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -47,7 +45,6 @@ export class ProfileViewModel {
|
||||||
banner?: string
|
banner?: string
|
||||||
followersCount: number = 0
|
followersCount: number = 0
|
||||||
followsCount: number = 0
|
followsCount: number = 0
|
||||||
membersCount: number = 0
|
|
||||||
postsCount: number = 0
|
postsCount: number = 0
|
||||||
myState = new ProfileViewMyStateModel()
|
myState = new ProfileViewMyStateModel()
|
||||||
|
|
||||||
|
@ -85,10 +82,6 @@ export class ProfileViewModel {
|
||||||
return this.declaration.actorType === ACTOR_TYPE_USER
|
return this.declaration.actorType === ACTOR_TYPE_USER
|
||||||
}
|
}
|
||||||
|
|
||||||
get isScene() {
|
|
||||||
return this.declaration.actorType === ACTOR_TYPE_SCENE
|
|
||||||
}
|
|
||||||
|
|
||||||
// public api
|
// public api
|
||||||
// =
|
// =
|
||||||
|
|
||||||
|
@ -216,7 +209,6 @@ export class ProfileViewModel {
|
||||||
this.banner = res.data.banner
|
this.banner = res.data.banner
|
||||||
this.followersCount = res.data.followersCount
|
this.followersCount = res.data.followersCount
|
||||||
this.followsCount = res.data.followsCount
|
this.followsCount = res.data.followsCount
|
||||||
this.membersCount = res.data.membersCount
|
|
||||||
this.postsCount = res.data.postsCount
|
this.postsCount = res.data.postsCount
|
||||||
if (res.data.myState) {
|
if (res.data.myState) {
|
||||||
Object.assign(this.myState, res.data.myState)
|
Object.assign(this.myState, res.data.myState)
|
||||||
|
|
|
@ -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 {
|
export class ServerInputModal {
|
||||||
name = 'server-input'
|
name = 'server-input'
|
||||||
|
|
||||||
|
@ -143,7 +127,6 @@ export class ShellUiModel {
|
||||||
activeModal:
|
activeModal:
|
||||||
| ConfirmModal
|
| ConfirmModal
|
||||||
| EditProfileModal
|
| EditProfileModal
|
||||||
| CreateSceneModal
|
|
||||||
| ServerInputModal
|
| ServerInputModal
|
||||||
| ReportPostModal
|
| ReportPostModal
|
||||||
| ReportAccountModal
|
| ReportAccountModal
|
||||||
|
@ -191,7 +174,6 @@ export class ShellUiModel {
|
||||||
modal:
|
modal:
|
||||||
| ConfirmModal
|
| ConfirmModal
|
||||||
| EditProfileModal
|
| EditProfileModal
|
||||||
| CreateSceneModal
|
|
||||||
| ServerInputModal
|
| ServerInputModal
|
||||||
| ReportPostModal
|
| ReportPostModal
|
||||||
| ReportAccountModal,
|
| ReportAccountModal,
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,243 +0,0 @@
|
||||||
import React, {useState} from 'react'
|
|
||||||
import * as Toast from '../util/Toast'
|
|
||||||
import {
|
|
||||||
ActivityIndicator,
|
|
||||||
StyleSheet,
|
|
||||||
TouchableOpacity,
|
|
||||||
View,
|
|
||||||
} from 'react-native'
|
|
||||||
import LinearGradient from 'react-native-linear-gradient'
|
|
||||||
import {BottomSheetScrollView, BottomSheetTextInput} from '@gorhom/bottom-sheet'
|
|
||||||
import {AppBskyActorCreateScene} from '@atproto/api'
|
|
||||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
|
||||||
import {Text} from '../util/text/Text'
|
|
||||||
import {useStores} from '../../../state'
|
|
||||||
import {s, colors, gradients} from '../../lib/styles'
|
|
||||||
import {
|
|
||||||
makeValidHandle,
|
|
||||||
createFullHandle,
|
|
||||||
enforceLen,
|
|
||||||
MAX_DISPLAY_NAME,
|
|
||||||
MAX_DESCRIPTION,
|
|
||||||
} from '../../../lib/strings'
|
|
||||||
|
|
||||||
export const snapPoints = ['60%']
|
|
||||||
|
|
||||||
export function Component({}: {}) {
|
|
||||||
const store = useStores()
|
|
||||||
const [error, setError] = useState<string>('')
|
|
||||||
const [isProcessing, setIsProcessing] = useState<boolean>(false)
|
|
||||||
const [handle, setHandle] = useState<string>('')
|
|
||||||
const [displayName, setDisplayName] = useState<string>('')
|
|
||||||
const [description, setDescription] = useState<string>('')
|
|
||||||
const onPressSave = async () => {
|
|
||||||
setIsProcessing(true)
|
|
||||||
if (error) {
|
|
||||||
setError('')
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
if (!store.me.did) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const desc = await store.api.com.atproto.server.getAccountsConfig()
|
|
||||||
const fullHandle = createFullHandle(
|
|
||||||
handle,
|
|
||||||
desc.data.availableUserDomains[0],
|
|
||||||
)
|
|
||||||
// create scene actor
|
|
||||||
const createSceneRes = await store.api.app.bsky.actor.createScene({
|
|
||||||
handle: fullHandle,
|
|
||||||
})
|
|
||||||
// set the scene profile
|
|
||||||
await store.api.app.bsky.actor
|
|
||||||
.updateProfile({
|
|
||||||
did: createSceneRes.data.did,
|
|
||||||
displayName,
|
|
||||||
description,
|
|
||||||
})
|
|
||||||
.catch(e =>
|
|
||||||
// an error here is not critical
|
|
||||||
store.log.error('Failed to update scene profile during creation', e),
|
|
||||||
)
|
|
||||||
// follow the scene
|
|
||||||
await store.api.app.bsky.graph.follow
|
|
||||||
.create(
|
|
||||||
{
|
|
||||||
did: store.me.did,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
subject: {
|
|
||||||
did: createSceneRes.data.did,
|
|
||||||
declarationCid: createSceneRes.data.declaration.cid,
|
|
||||||
},
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.catch(e =>
|
|
||||||
// an error here is not critical
|
|
||||||
store.log.error('Failed to follow scene after creation', e),
|
|
||||||
)
|
|
||||||
Toast.show('Scene created')
|
|
||||||
store.shell.closeModal()
|
|
||||||
store.nav.navigate(`/profile/${fullHandle}`)
|
|
||||||
} catch (e: any) {
|
|
||||||
if (e instanceof AppBskyActorCreateScene.InvalidHandleError) {
|
|
||||||
setError(
|
|
||||||
'The handle can only contain letters, numbers, and dashes, and must start with a letter.',
|
|
||||||
)
|
|
||||||
} else if (e instanceof AppBskyActorCreateScene.HandleNotAvailableError) {
|
|
||||||
setError(`The handle "${handle}" is not available.`)
|
|
||||||
} else {
|
|
||||||
store.log.error('Failed to create scene', e)
|
|
||||||
setError(
|
|
||||||
'Failed to create the scene. Check your internet connection and try again.',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
setIsProcessing(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const onPressCancel = () => {
|
|
||||||
store.shell.closeModal()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={styles.outer}>
|
|
||||||
<BottomSheetScrollView style={styles.inner}>
|
|
||||||
<Text style={[styles.title, s.black]}>Create a scene</Text>
|
|
||||||
<Text style={styles.description}>
|
|
||||||
Scenes are invite-only groups which aggregate what's popular with
|
|
||||||
members.
|
|
||||||
</Text>
|
|
||||||
<View style={{paddingBottom: 50}}>
|
|
||||||
<View style={styles.group}>
|
|
||||||
<Text style={[styles.label, s.black]}>Scene Handle</Text>
|
|
||||||
<BottomSheetTextInput
|
|
||||||
style={styles.textInput}
|
|
||||||
placeholder="e.g. alices-friends"
|
|
||||||
placeholderTextColor={colors.gray4}
|
|
||||||
autoCorrect={false}
|
|
||||||
value={handle}
|
|
||||||
onChangeText={str => setHandle(makeValidHandle(str))}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<View style={styles.group}>
|
|
||||||
<Text style={[styles.label, s.black]}>Scene Display Name</Text>
|
|
||||||
<BottomSheetTextInput
|
|
||||||
style={styles.textInput}
|
|
||||||
placeholder="e.g. Alice's Friends"
|
|
||||||
placeholderTextColor={colors.gray4}
|
|
||||||
value={displayName}
|
|
||||||
onChangeText={v =>
|
|
||||||
setDisplayName(enforceLen(v, MAX_DISPLAY_NAME))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<View style={styles.group}>
|
|
||||||
<Text style={[styles.label, s.black]}>Scene Description</Text>
|
|
||||||
<BottomSheetTextInput
|
|
||||||
style={[styles.textArea]}
|
|
||||||
placeholder="e.g. Artists, dog-lovers, and memelords."
|
|
||||||
placeholderTextColor={colors.gray4}
|
|
||||||
multiline
|
|
||||||
value={description}
|
|
||||||
onChangeText={v => setDescription(enforceLen(v, MAX_DESCRIPTION))}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
{error !== '' && (
|
|
||||||
<View style={s.mb10}>
|
|
||||||
<ErrorMessage message={error} numberOfLines={3} />
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
{handle.length >= 2 && !isProcessing ? (
|
|
||||||
<TouchableOpacity style={s.mt10} onPress={onPressSave}>
|
|
||||||
<LinearGradient
|
|
||||||
colors={[gradients.primary.start, gradients.primary.end]}
|
|
||||||
start={{x: 0, y: 0}}
|
|
||||||
end={{x: 1, y: 1}}
|
|
||||||
style={[styles.btn]}>
|
|
||||||
<Text style={[s.white, s.bold, s.f18]}>Create Scene</Text>
|
|
||||||
</LinearGradient>
|
|
||||||
</TouchableOpacity>
|
|
||||||
) : (
|
|
||||||
<View style={s.mt10}>
|
|
||||||
<View style={[styles.btn]}>
|
|
||||||
{isProcessing ? (
|
|
||||||
<ActivityIndicator />
|
|
||||||
) : (
|
|
||||||
<Text style={[s.gray4, s.bold, s.f18]}>Create Scene</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
<TouchableOpacity style={s.mt10} onPress={onPressCancel}>
|
|
||||||
<View style={[styles.btn, {backgroundColor: colors.white}]}>
|
|
||||||
<Text style={[s.black, s.bold]}>Cancel</Text>
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</BottomSheetScrollView>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
outer: {
|
|
||||||
flex: 1,
|
|
||||||
// paddingTop: 20,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
textAlign: 'center',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
fontSize: 24,
|
|
||||||
marginBottom: 12,
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
textAlign: 'center',
|
|
||||||
fontSize: 17,
|
|
||||||
paddingHorizontal: 22,
|
|
||||||
color: colors.gray5,
|
|
||||||
marginBottom: 10,
|
|
||||||
},
|
|
||||||
inner: {
|
|
||||||
padding: 14,
|
|
||||||
height: 350,
|
|
||||||
},
|
|
||||||
group: {
|
|
||||||
marginBottom: 10,
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
paddingHorizontal: 4,
|
|
||||||
paddingBottom: 4,
|
|
||||||
},
|
|
||||||
textInput: {
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: colors.gray3,
|
|
||||||
borderRadius: 6,
|
|
||||||
paddingHorizontal: 14,
|
|
||||||
paddingVertical: 10,
|
|
||||||
fontSize: 16,
|
|
||||||
color: colors.black,
|
|
||||||
},
|
|
||||||
textArea: {
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: colors.gray3,
|
|
||||||
borderRadius: 6,
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
paddingTop: 10,
|
|
||||||
fontSize: 16,
|
|
||||||
color: colors.black,
|
|
||||||
height: 70,
|
|
||||||
textAlignVertical: 'top',
|
|
||||||
},
|
|
||||||
btn: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
width: '100%',
|
|
||||||
borderRadius: 32,
|
|
||||||
padding: 14,
|
|
||||||
backgroundColor: colors.gray1,
|
|
||||||
},
|
|
||||||
})
|
|
|
@ -1,308 +0,0 @@
|
||||||
import React, {useState, useEffect, useMemo} from 'react'
|
|
||||||
import {observer} from 'mobx-react-lite'
|
|
||||||
import * as Toast from '../util/Toast'
|
|
||||||
import {
|
|
||||||
ActivityIndicator,
|
|
||||||
FlatList,
|
|
||||||
StyleSheet,
|
|
||||||
useWindowDimensions,
|
|
||||||
View,
|
|
||||||
} from 'react-native'
|
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
|
||||||
import {
|
|
||||||
TabView,
|
|
||||||
SceneMap,
|
|
||||||
Route,
|
|
||||||
TabBar,
|
|
||||||
TabBarProps,
|
|
||||||
} from 'react-native-tab-view'
|
|
||||||
import _omit from 'lodash.omit'
|
|
||||||
import {AtUri} from '../../../third-party/uri'
|
|
||||||
import {ProfileCard} from '../profile/ProfileCard'
|
|
||||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
|
||||||
import {Text} from '../util/text/Text'
|
|
||||||
import {useStores} from '../../../state'
|
|
||||||
import * as apilib from '../../../state/lib/api'
|
|
||||||
import {ProfileViewModel} from '../../../state/models/profile-view'
|
|
||||||
import {SuggestedInvitesView} from '../../../state/models/suggested-invites-view'
|
|
||||||
import {Assertion} from '../../../state/models/get-assertions-view'
|
|
||||||
import {FollowItem} from '../../../state/models/user-follows-view'
|
|
||||||
import {s, colors} from '../../lib/styles'
|
|
||||||
|
|
||||||
export const snapPoints = ['70%']
|
|
||||||
|
|
||||||
export const Component = observer(function Component({
|
|
||||||
profileView,
|
|
||||||
}: {
|
|
||||||
profileView: ProfileViewModel
|
|
||||||
}) {
|
|
||||||
const store = useStores()
|
|
||||||
const layout = useWindowDimensions()
|
|
||||||
const [index, setIndex] = useState(0)
|
|
||||||
const tabRoutes = [
|
|
||||||
{key: 'suggestions', title: 'Suggestions'},
|
|
||||||
{key: 'pending', title: 'Pending Invites'},
|
|
||||||
]
|
|
||||||
const [hasSetup, setHasSetup] = useState<boolean>(false)
|
|
||||||
const [error, setError] = useState<string>('')
|
|
||||||
const suggestions = useMemo(
|
|
||||||
() => new SuggestedInvitesView(store, {sceneDid: profileView.did}),
|
|
||||||
[profileView.did],
|
|
||||||
)
|
|
||||||
const [createdInvites, setCreatedInvites] = useState<Record<string, string>>(
|
|
||||||
{},
|
|
||||||
)
|
|
||||||
// TODO: it would be much better if we just used the suggestions view for the deleted pending invites
|
|
||||||
// but mobx isnt picking up on the state change in suggestions.unconfirmed and I dont have
|
|
||||||
// time to debug that right now -prf
|
|
||||||
const [deletedPendingInvites, setDeletedPendingInvites] = useState<
|
|
||||||
Record<string, boolean>
|
|
||||||
>({})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let aborted = false
|
|
||||||
if (hasSetup) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
suggestions.setup().then(() => {
|
|
||||||
if (aborted) return
|
|
||||||
setHasSetup(true)
|
|
||||||
})
|
|
||||||
return () => {
|
|
||||||
aborted = true
|
|
||||||
}
|
|
||||||
}, [profileView.did])
|
|
||||||
|
|
||||||
const onPressInvite = async (follow: FollowItem) => {
|
|
||||||
setError('')
|
|
||||||
try {
|
|
||||||
const assertionUri = await apilib.inviteToScene(
|
|
||||||
store,
|
|
||||||
profileView.did,
|
|
||||||
follow.did,
|
|
||||||
follow.declaration.cid,
|
|
||||||
)
|
|
||||||
setCreatedInvites({[follow.did]: assertionUri, ...createdInvites})
|
|
||||||
Toast.show('Invite sent')
|
|
||||||
} catch (e: any) {
|
|
||||||
setError('There was an issue with the invite. Please try again.')
|
|
||||||
store.log.error('Failed to invite user to scene', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const onPressUndo = async (subjectDid: string, assertionUri: string) => {
|
|
||||||
setError('')
|
|
||||||
const urip = new AtUri(assertionUri)
|
|
||||||
try {
|
|
||||||
await store.api.app.bsky.graph.assertion.delete({
|
|
||||||
did: profileView.did,
|
|
||||||
rkey: urip.rkey,
|
|
||||||
})
|
|
||||||
setCreatedInvites(_omit(createdInvites, [subjectDid]))
|
|
||||||
} catch (e: any) {
|
|
||||||
setError('There was an issue with the invite. Please try again.')
|
|
||||||
store.log.error('Failed to delete a scene invite', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onPressDeleteInvite = async (assertion: Assertion) => {
|
|
||||||
setError('')
|
|
||||||
const urip = new AtUri(assertion.uri)
|
|
||||||
try {
|
|
||||||
await store.api.app.bsky.graph.assertion.delete({
|
|
||||||
did: profileView.did,
|
|
||||||
rkey: urip.rkey,
|
|
||||||
})
|
|
||||||
setDeletedPendingInvites({
|
|
||||||
[assertion.uri]: true,
|
|
||||||
...deletedPendingInvites,
|
|
||||||
})
|
|
||||||
Toast.show('Invite removed')
|
|
||||||
} catch (e: any) {
|
|
||||||
setError('There was an issue with the invite. Please try again.')
|
|
||||||
store.log.error('Failed to delete an invite', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderSuggestionItem = ({item}: {item: FollowItem}) => {
|
|
||||||
const createdInvite = createdInvites[item.did]
|
|
||||||
return (
|
|
||||||
<ProfileCard
|
|
||||||
did={item.did}
|
|
||||||
handle={item.handle}
|
|
||||||
displayName={item.displayName}
|
|
||||||
avatar={item.avatar}
|
|
||||||
renderButton={() =>
|
|
||||||
!createdInvite ? (
|
|
||||||
<>
|
|
||||||
<FontAwesomeIcon icon="user-plus" style={[s.mr5]} size={14} />
|
|
||||||
<Text style={[s.fw400, s.f14]}>Invite</Text>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<FontAwesomeIcon icon="x" style={[s.mr5]} size={14} />
|
|
||||||
<Text style={[s.fw400, s.f14]}>Undo invite</Text>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onPressButton={() =>
|
|
||||||
!createdInvite
|
|
||||||
? onPressInvite(item)
|
|
||||||
: onPressUndo(item.did, createdInvite)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderPendingInviteItem = ({item}: {item: Assertion}) => {
|
|
||||||
const wasDeleted = deletedPendingInvites[item.uri]
|
|
||||||
if (wasDeleted) {
|
|
||||||
return <View />
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<ProfileCard
|
|
||||||
did={item.subject.did}
|
|
||||||
handle={item.subject.handle}
|
|
||||||
displayName={item.subject.displayName}
|
|
||||||
avatar={item.subject.avatar}
|
|
||||||
renderButton={() => (
|
|
||||||
<>
|
|
||||||
<FontAwesomeIcon icon="x" style={[s.mr5]} size={14} />
|
|
||||||
<Text style={[s.fw400, s.f14]}>Undo invite</Text>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
onPressButton={() => onPressDeleteInvite(item)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const Suggestions = () => (
|
|
||||||
<View style={s.flex1}>
|
|
||||||
{hasSetup ? (
|
|
||||||
<View style={s.flex1}>
|
|
||||||
<View style={styles.todoContainer}>
|
|
||||||
<Text style={styles.todoLabel}>
|
|
||||||
User search is still being implemented. For now, you can pick from
|
|
||||||
your follows below.
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
{!suggestions.hasContent ? (
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
textAlign: 'center',
|
|
||||||
paddingTop: 10,
|
|
||||||
paddingHorizontal: 40,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: colors.gray5,
|
|
||||||
}}>
|
|
||||||
{suggestions.myFollowsView.follows.length
|
|
||||||
? 'Sorry! You dont follow anybody for us to suggest.'
|
|
||||||
: 'Sorry! All of the users you follow are members already.'}
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
<FlatList
|
|
||||||
data={suggestions.suggestions}
|
|
||||||
keyExtractor={item => item._reactKey}
|
|
||||||
renderItem={renderSuggestionItem}
|
|
||||||
style={s.flex1}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
) : !error ? (
|
|
||||||
<ActivityIndicator />
|
|
||||||
) : undefined}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
|
|
||||||
const PendingInvites = () => (
|
|
||||||
<View style={s.flex1}>
|
|
||||||
{suggestions.sceneAssertionsView.isLoading ? (
|
|
||||||
<ActivityIndicator />
|
|
||||||
) : undefined}
|
|
||||||
<View style={s.flex1}>
|
|
||||||
{!suggestions.unconfirmed.length ? (
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
textAlign: 'center',
|
|
||||||
paddingTop: 10,
|
|
||||||
paddingHorizontal: 40,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: colors.gray5,
|
|
||||||
}}>
|
|
||||||
No pending invites.
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
<FlatList
|
|
||||||
data={suggestions.unconfirmed}
|
|
||||||
keyExtractor={item => item._reactKey}
|
|
||||||
renderItem={renderPendingInviteItem}
|
|
||||||
style={s.flex1}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
|
|
||||||
const renderScene = SceneMap({
|
|
||||||
suggestions: Suggestions,
|
|
||||||
pending: PendingInvites,
|
|
||||||
})
|
|
||||||
|
|
||||||
const renderTabBar = (props: TabBarProps<Route>) => (
|
|
||||||
<TabBar
|
|
||||||
{...props}
|
|
||||||
style={{backgroundColor: 'white'}}
|
|
||||||
activeColor="black"
|
|
||||||
inactiveColor={colors.gray5}
|
|
||||||
labelStyle={{textTransform: 'none'}}
|
|
||||||
indicatorStyle={{backgroundColor: colors.purple3}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={s.flex1}>
|
|
||||||
<Text style={styles.title}>
|
|
||||||
Invite to {profileView.displayName || profileView.handle}
|
|
||||||
</Text>
|
|
||||||
{error !== '' ? (
|
|
||||||
<View style={s.p10}>
|
|
||||||
<ErrorMessage message={error} />
|
|
||||||
</View>
|
|
||||||
) : undefined}
|
|
||||||
<TabView
|
|
||||||
navigationState={{index, routes: tabRoutes}}
|
|
||||||
renderScene={renderScene}
|
|
||||||
renderTabBar={renderTabBar}
|
|
||||||
onIndexChange={setIndex}
|
|
||||||
initialLayout={{width: layout.width}}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
title: {
|
|
||||||
textAlign: 'center',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
fontSize: 18,
|
|
||||||
marginBottom: 4,
|
|
||||||
},
|
|
||||||
todoContainer: {
|
|
||||||
backgroundColor: colors.pink1,
|
|
||||||
margin: 10,
|
|
||||||
padding: 10,
|
|
||||||
borderRadius: 6,
|
|
||||||
},
|
|
||||||
todoLabel: {
|
|
||||||
color: colors.pink5,
|
|
||||||
textAlign: 'center',
|
|
||||||
},
|
|
||||||
|
|
||||||
tabBar: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
},
|
|
||||||
tabItem: {
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: 16,
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
})
|
|
|
@ -9,8 +9,6 @@ import * as models from '../../../state/models/shell-ui'
|
||||||
|
|
||||||
import * as ConfirmModal from './Confirm'
|
import * as ConfirmModal from './Confirm'
|
||||||
import * as EditProfileModal from './EditProfile'
|
import * as EditProfileModal from './EditProfile'
|
||||||
import * as CreateSceneModal from './CreateScene'
|
|
||||||
import * as InviteToSceneModal from './InviteToScene'
|
|
||||||
import * as ServerInputModal from './ServerInput'
|
import * as ServerInputModal from './ServerInput'
|
||||||
import * as ReportPostModal from './ReportPost'
|
import * as ReportPostModal from './ReportPost'
|
||||||
import * as ReportAccountModal from './ReportAccount'
|
import * as ReportAccountModal from './ReportAccount'
|
||||||
|
@ -55,16 +53,6 @@ export const Modal = observer(function Modal() {
|
||||||
{...(store.shell.activeModal as models.EditProfileModal)}
|
{...(store.shell.activeModal as models.EditProfileModal)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
} else if (store.shell.activeModal?.name === 'create-scene') {
|
|
||||||
snapPoints = CreateSceneModal.snapPoints
|
|
||||||
element = <CreateSceneModal.Component />
|
|
||||||
} else if (store.shell.activeModal?.name === 'invite-to-scene') {
|
|
||||||
snapPoints = InviteToSceneModal.snapPoints
|
|
||||||
element = (
|
|
||||||
<InviteToSceneModal.Component
|
|
||||||
{...(store.shell.activeModal as models.InviteToSceneModal)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
} else if (store.shell.activeModal?.name === 'server-input') {
|
} else if (store.shell.activeModal?.name === 'server-input') {
|
||||||
snapPoints = ServerInputModal.snapPoints
|
snapPoints = ServerInputModal.snapPoints
|
||||||
element = (
|
element = (
|
||||||
|
|
|
@ -14,7 +14,6 @@ import {UserAvatar} from '../util/UserAvatar'
|
||||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||||
import {Post} from '../post/Post'
|
import {Post} from '../post/Post'
|
||||||
import {Link} from '../util/Link'
|
import {Link} from '../util/Link'
|
||||||
import {InviteAccepter} from './InviteAccepter'
|
|
||||||
import {usePalette} from '../../lib/hooks/usePalette'
|
import {usePalette} from '../../lib/hooks/usePalette'
|
||||||
|
|
||||||
const MAX_AUTHORS = 8
|
const MAX_AUTHORS = 8
|
||||||
|
@ -26,7 +25,7 @@ export const FeedItem = observer(function FeedItem({
|
||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const itemHref = useMemo(() => {
|
const itemHref = useMemo(() => {
|
||||||
if (item.isUpvote || item.isRepost || item.isTrend) {
|
if (item.isUpvote || item.isRepost) {
|
||||||
const urip = new AtUri(item.subjectUri)
|
const urip = new AtUri(item.subjectUri)
|
||||||
return `/profile/${urip.host}/post/${urip.rkey}`
|
return `/profile/${urip.host}/post/${urip.rkey}`
|
||||||
} else if (item.isFollow || item.isAssertion) {
|
} else if (item.isFollow || item.isAssertion) {
|
||||||
|
@ -82,10 +81,6 @@ export const FeedItem = observer(function FeedItem({
|
||||||
action = 'reposted your post'
|
action = 'reposted your post'
|
||||||
icon = 'retweet'
|
icon = 'retweet'
|
||||||
iconStyle = [s.green3]
|
iconStyle = [s.green3]
|
||||||
} else if (item.isTrend) {
|
|
||||||
action = 'Your post is trending with'
|
|
||||||
icon = 'arrow-trend-up'
|
|
||||||
iconStyle = [s.red3]
|
|
||||||
} else if (item.isReply) {
|
} else if (item.isReply) {
|
||||||
action = 'replied to your post'
|
action = 'replied to your post'
|
||||||
icon = ['far', 'comment']
|
icon = ['far', 'comment']
|
||||||
|
@ -93,10 +88,6 @@ export const FeedItem = observer(function FeedItem({
|
||||||
action = 'followed you'
|
action = 'followed you'
|
||||||
icon = 'user-plus'
|
icon = 'user-plus'
|
||||||
iconStyle = [s.blue3]
|
iconStyle = [s.blue3]
|
||||||
} else if (item.isInvite) {
|
|
||||||
icon = 'users'
|
|
||||||
iconStyle = [s.blue3]
|
|
||||||
action = 'invited you to join their scene'
|
|
||||||
} else {
|
} else {
|
||||||
return <></>
|
return <></>
|
||||||
}
|
}
|
||||||
|
@ -173,9 +164,6 @@ export const FeedItem = observer(function FeedItem({
|
||||||
) : undefined}
|
) : undefined}
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.meta}>
|
<View style={styles.meta}>
|
||||||
{item.isTrend && (
|
|
||||||
<Text style={[styles.metaItem, pal.text]}>{action}</Text>
|
|
||||||
)}
|
|
||||||
<Link
|
<Link
|
||||||
key={authors[0].href}
|
key={authors[0].href}
|
||||||
style={styles.metaItem}
|
style={styles.metaItem}
|
||||||
|
@ -193,25 +181,17 @@ export const FeedItem = observer(function FeedItem({
|
||||||
</Text>
|
</Text>
|
||||||
</>
|
</>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
{!item.isTrend && (
|
|
||||||
<Text style={[styles.metaItem, pal.text]}>{action}</Text>
|
|
||||||
)}
|
|
||||||
<Text style={[styles.metaItem, pal.textLight]}>
|
<Text style={[styles.metaItem, pal.textLight]}>
|
||||||
{ago(item.indexedAt)}
|
{ago(item.indexedAt)}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
{item.isUpvote || item.isRepost || item.isTrend ? (
|
{item.isUpvote || item.isRepost ? (
|
||||||
<AdditionalPostText additionalPost={item.additionalPost} />
|
<AdditionalPostText additionalPost={item.additionalPost} />
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{item.isInvite && (
|
|
||||||
<View style={styles.addedContainer}>
|
|
||||||
<InviteAccepter item={item} />
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,96 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
|
||||||
import LinearGradient from 'react-native-linear-gradient'
|
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
|
||||||
import * as apilib from '../../../state/lib/api'
|
|
||||||
import {NotificationsViewItemModel} from '../../../state/models/notifications-view'
|
|
||||||
import {ConfirmModal} from '../../../state/models/shell-ui'
|
|
||||||
import {useStores} from '../../../state'
|
|
||||||
import {ProfileCard} from '../profile/ProfileCard'
|
|
||||||
import * as Toast from '../util/Toast'
|
|
||||||
import {Text} from '../util/text/Text'
|
|
||||||
import {s, colors, gradients} from '../../lib/styles'
|
|
||||||
|
|
||||||
export function InviteAccepter({item}: {item: NotificationsViewItemModel}) {
|
|
||||||
const store = useStores()
|
|
||||||
// Using default import (React.use...) instead of named import (use...) to be able to mock store's data in jest environment
|
|
||||||
const [confirmationUri, setConfirmationUri] = React.useState<string>('')
|
|
||||||
const isMember =
|
|
||||||
confirmationUri !== '' || store.me.memberships?.isMemberOf(item.author.did)
|
|
||||||
const onPressAccept = async () => {
|
|
||||||
store.shell.openModal(
|
|
||||||
new ConfirmModal(
|
|
||||||
'Join this scene?',
|
|
||||||
() => (
|
|
||||||
<View>
|
|
||||||
<View style={styles.profileCardContainer}>
|
|
||||||
<ProfileCard
|
|
||||||
did={item.author.did}
|
|
||||||
handle={item.author.handle}
|
|
||||||
displayName={item.author.displayName}
|
|
||||||
avatar={item.author.avatar}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
),
|
|
||||||
onPressConfirmAccept,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const onPressConfirmAccept = async () => {
|
|
||||||
const uri = await apilib.acceptSceneInvite(store, {
|
|
||||||
originator: {
|
|
||||||
did: item.author.did,
|
|
||||||
declarationCid: item.author.declaration.cid,
|
|
||||||
},
|
|
||||||
assertion: {
|
|
||||||
uri: item.uri,
|
|
||||||
cid: item.cid,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
store.me.refreshMemberships()
|
|
||||||
Toast.show('Invite accepted')
|
|
||||||
setConfirmationUri(uri)
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<View style={styles.container}>
|
|
||||||
{!isMember ? (
|
|
||||||
<TouchableOpacity testID="acceptInviteButton" onPress={onPressAccept}>
|
|
||||||
<LinearGradient
|
|
||||||
colors={[gradients.primary.start, gradients.primary.end]}
|
|
||||||
start={{x: 0, y: 0}}
|
|
||||||
end={{x: 1, y: 1}}
|
|
||||||
style={[styles.btn]}>
|
|
||||||
<Text style={[s.white, s.bold, s.f16]}>Accept Invite</Text>
|
|
||||||
</LinearGradient>
|
|
||||||
</TouchableOpacity>
|
|
||||||
) : (
|
|
||||||
<View testID="inviteAccepted" style={styles.inviteAccepted}>
|
|
||||||
<FontAwesomeIcon icon="check" size={14} style={s.mr5} />
|
|
||||||
<Text style={[s.gray5, s.f15]}>Invite accepted</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
},
|
|
||||||
btn: {
|
|
||||||
borderRadius: 32,
|
|
||||||
paddingHorizontal: 18,
|
|
||||||
paddingVertical: 8,
|
|
||||||
backgroundColor: colors.gray1,
|
|
||||||
},
|
|
||||||
profileCardContainer: {
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: colors.gray3,
|
|
||||||
borderRadius: 6,
|
|
||||||
},
|
|
||||||
inviteAccepted: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
})
|
|
|
@ -11,10 +11,9 @@ import {
|
||||||
import {TabView, SceneMap, Route, TabBarProps} from 'react-native-tab-view'
|
import {TabView, SceneMap, Route, TabBarProps} from 'react-native-tab-view'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import {Text} from '../util/text/Text'
|
import {Text} from '../util/text/Text'
|
||||||
import {UserGroupIcon} from '../../lib/icons'
|
|
||||||
import {useStores} from '../../../state'
|
import {useStores} from '../../../state'
|
||||||
import {s} from '../../lib/styles'
|
import {s} from '../../lib/styles'
|
||||||
import {SCENE_EXPLAINER, TABS_EXPLAINER} from '../../lib/assets'
|
import {TABS_EXPLAINER} from '../../lib/assets'
|
||||||
import {TABS_ENABLED} from '../../../build-flags'
|
import {TABS_ENABLED} from '../../../build-flags'
|
||||||
|
|
||||||
const Intro = () => (
|
const Intro = () => (
|
||||||
|
@ -28,25 +27,7 @@ const Intro = () => (
|
||||||
Welcome to <Text style={[s.bold, s.blue3, {fontSize: 56}]}>Bluesky</Text>
|
Welcome to <Text style={[s.bold, s.blue3, {fontSize: 56}]}>Bluesky</Text>
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={[styles.explainerDesc, {fontSize: 24}]}>
|
<Text style={[styles.explainerDesc, {fontSize: 24}]}>
|
||||||
Let's do a quick tour through the new features.
|
This is an early beta. Your feedback is appreciated!
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
|
|
||||||
const Scenes = () => (
|
|
||||||
<View style={styles.explainer}>
|
|
||||||
<View style={styles.explainerIcon}>
|
|
||||||
<View style={s.flex1} />
|
|
||||||
<UserGroupIcon style={s.black} size="48" />
|
|
||||||
<View style={s.flex1} />
|
|
||||||
</View>
|
|
||||||
<Text style={styles.explainerHeading}>Scenes</Text>
|
|
||||||
<Text style={styles.explainerDesc}>
|
|
||||||
Scenes are invite-only groups of users. Follow them to see what's trending
|
|
||||||
with the scene's members.
|
|
||||||
</Text>
|
|
||||||
<Text style={styles.explainerDesc}>
|
|
||||||
<Image source={SCENE_EXPLAINER} style={styles.explainerImg} />
|
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
@ -74,7 +55,6 @@ const Tabs = () => (
|
||||||
|
|
||||||
const SCENE_MAP = {
|
const SCENE_MAP = {
|
||||||
intro: Intro,
|
intro: Intro,
|
||||||
scenes: Scenes,
|
|
||||||
tabs: Tabs,
|
tabs: Tabs,
|
||||||
}
|
}
|
||||||
const renderScene = SceneMap(SCENE_MAP)
|
const renderScene = SceneMap(SCENE_MAP)
|
||||||
|
@ -85,7 +65,6 @@ export const FeatureExplainer = () => {
|
||||||
const [index, setIndex] = useState(0)
|
const [index, setIndex] = useState(0)
|
||||||
const routes = [
|
const routes = [
|
||||||
{key: 'intro', title: 'Intro'},
|
{key: 'intro', title: 'Intro'},
|
||||||
{key: 'scenes', title: 'Scenes'},
|
|
||||||
TABS_ENABLED ? {key: 'tabs', title: 'Tabs'} : undefined,
|
TABS_ENABLED ? {key: 'tabs', title: 'Tabs'} : undefined,
|
||||||
].filter(Boolean)
|
].filter(Boolean)
|
||||||
|
|
||||||
|
|
|
@ -155,23 +155,6 @@ export const FeedItem = observer(function ({
|
||||||
</Text>
|
</Text>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{item.reasonTrend && (
|
|
||||||
<Link
|
|
||||||
style={styles.includeReason}
|
|
||||||
href={`/profile/${item.reasonTrend.by.handle}`}
|
|
||||||
title={
|
|
||||||
item.reasonTrend.by.displayName || item.reasonTrend.by.handle
|
|
||||||
}>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon="arrow-trend-up"
|
|
||||||
style={styles.includeReasonIcon}
|
|
||||||
/>
|
|
||||||
<Text type="overline2" style={{color: pal.colors.actionLabel}}>
|
|
||||||
Trending with{' '}
|
|
||||||
{item.reasonTrend.by.displayName || item.reasonTrend.by.handle}
|
|
||||||
</Text>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
<View style={styles.layout}>
|
<View style={styles.layout}>
|
||||||
<View style={styles.layoutAvi}>
|
<View style={styles.layoutAvi}>
|
||||||
<Link href={authorHref} title={item.post.author.handle}>
|
<Link href={authorHref} title={item.post.author.handle}>
|
||||||
|
|
|
@ -1,15 +1,12 @@
|
||||||
import React, {useMemo} from 'react'
|
import React from 'react'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||||
import LinearGradient from 'react-native-linear-gradient'
|
import LinearGradient from 'react-native-linear-gradient'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import {AtUri} from '../../../third-party/uri'
|
|
||||||
import {ProfileViewModel} from '../../../state/models/profile-view'
|
import {ProfileViewModel} from '../../../state/models/profile-view'
|
||||||
import {useStores} from '../../../state'
|
import {useStores} from '../../../state'
|
||||||
import {
|
import {
|
||||||
ConfirmModal,
|
|
||||||
EditProfileModal,
|
EditProfileModal,
|
||||||
InviteToSceneModal,
|
|
||||||
ReportAccountModal,
|
ReportAccountModal,
|
||||||
ProfileImageLightbox,
|
ProfileImageLightbox,
|
||||||
} from '../../../state/models/shell-ui'
|
} from '../../../state/models/shell-ui'
|
||||||
|
@ -23,7 +20,6 @@ import {Text} from '../util/text/Text'
|
||||||
import {RichText} from '../util/text/RichText'
|
import {RichText} from '../util/text/RichText'
|
||||||
import {UserAvatar} from '../util/UserAvatar'
|
import {UserAvatar} from '../util/UserAvatar'
|
||||||
import {UserBanner} from '../util/UserBanner'
|
import {UserBanner} from '../util/UserBanner'
|
||||||
import {UserInfoText} from '../util/UserInfoText'
|
|
||||||
import {usePalette} from '../../lib/hooks/usePalette'
|
import {usePalette} from '../../lib/hooks/usePalette'
|
||||||
|
|
||||||
export const ProfileHeader = observer(function ProfileHeader({
|
export const ProfileHeader = observer(function ProfileHeader({
|
||||||
|
@ -35,10 +31,6 @@ export const ProfileHeader = observer(function ProfileHeader({
|
||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const isMember = useMemo(
|
|
||||||
() => view.isScene && view.myState.member,
|
|
||||||
[view.myState.member],
|
|
||||||
)
|
|
||||||
|
|
||||||
const onPressAvi = () => {
|
const onPressAvi = () => {
|
||||||
store.shell.openLightbox(new ProfileImageLightbox(view))
|
store.shell.openLightbox(new ProfileImageLightbox(view))
|
||||||
|
@ -64,31 +56,6 @@ export const ProfileHeader = observer(function ProfileHeader({
|
||||||
const onPressFollows = () => {
|
const onPressFollows = () => {
|
||||||
store.nav.navigate(`/profile/${view.handle}/follows`)
|
store.nav.navigate(`/profile/${view.handle}/follows`)
|
||||||
}
|
}
|
||||||
const onPressMembers = () => {
|
|
||||||
store.nav.navigate(`/profile/${view.handle}/members`)
|
|
||||||
}
|
|
||||||
const onPressInviteMembers = () => {
|
|
||||||
store.shell.openModal(new InviteToSceneModal(view))
|
|
||||||
}
|
|
||||||
const onPressLeaveScene = () => {
|
|
||||||
store.shell.openModal(
|
|
||||||
new ConfirmModal(
|
|
||||||
'Leave this scene?',
|
|
||||||
`You'll be able to come back unless your invite is revoked.`,
|
|
||||||
onPressConfirmLeaveScene,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const onPressConfirmLeaveScene = async () => {
|
|
||||||
if (view.myState.member) {
|
|
||||||
await store.api.app.bsky.graph.confirmation.delete({
|
|
||||||
did: store.me.did || '',
|
|
||||||
rkey: new AtUri(view.myState.member).rkey,
|
|
||||||
})
|
|
||||||
Toast.show(`Scene left`)
|
|
||||||
}
|
|
||||||
onRefreshAll()
|
|
||||||
}
|
|
||||||
const onPressMuteAccount = async () => {
|
const onPressMuteAccount = async () => {
|
||||||
try {
|
try {
|
||||||
await view.muteAccount()
|
await view.muteAccount()
|
||||||
|
@ -157,7 +124,6 @@ export const ProfileHeader = observer(function ProfileHeader({
|
||||||
// =
|
// =
|
||||||
const gradient = getGradient(view.handle)
|
const gradient = getGradient(view.handle)
|
||||||
const isMe = store.me.did === view.did
|
const isMe = store.me.did === view.did
|
||||||
const isCreator = view.isScene && view.creator === store.me.did
|
|
||||||
let dropdownItems: DropdownItem[] | undefined
|
let dropdownItems: DropdownItem[] | undefined
|
||||||
if (!isMe) {
|
if (!isMe) {
|
||||||
dropdownItems = dropdownItems || []
|
dropdownItems = dropdownItems || []
|
||||||
|
@ -170,21 +136,6 @@ export const ProfileHeader = observer(function ProfileHeader({
|
||||||
onPress: onPressReportAccount,
|
onPress: onPressReportAccount,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (isCreator || isMember) {
|
|
||||||
dropdownItems = dropdownItems || []
|
|
||||||
if (isCreator) {
|
|
||||||
dropdownItems.push({
|
|
||||||
label: 'Edit Profile',
|
|
||||||
onPress: onPressEditProfile,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (isMember) {
|
|
||||||
dropdownItems.push({
|
|
||||||
label: 'Leave Scene...',
|
|
||||||
onPress: onPressLeaveScene,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<View style={pal.view}>
|
<View style={pal.view}>
|
||||||
<UserBanner handle={view.handle} banner={view.banner} />
|
<UserBanner handle={view.handle} banner={view.banner} />
|
||||||
|
@ -247,15 +198,6 @@ export const ProfileHeader = observer(function ProfileHeader({
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.handleLine}>
|
<View style={styles.handleLine}>
|
||||||
{view.isScene ? (
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.typeLabelWrapper,
|
|
||||||
{backgroundColor: pal.colors.backgroundLight},
|
|
||||||
]}>
|
|
||||||
<Text style={[styles.typeLabel, pal.textLight]}>Scene</Text>
|
|
||||||
</View>
|
|
||||||
) : undefined}
|
|
||||||
<Text style={pal.textLight}>@{view.handle}</Text>
|
<Text style={pal.textLight}>@{view.handle}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.metricsLine}>
|
<View style={styles.metricsLine}>
|
||||||
|
@ -283,19 +225,6 @@ export const ProfileHeader = observer(function ProfileHeader({
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
{view.isScene ? (
|
|
||||||
<TouchableOpacity
|
|
||||||
testID="profileHeaderMembersButton"
|
|
||||||
style={[s.flexRow, s.mr10]}
|
|
||||||
onPress={onPressMembers}>
|
|
||||||
<Text type="body2" style={[s.bold, s.mr2, pal.text]}>
|
|
||||||
{view.membersCount}
|
|
||||||
</Text>
|
|
||||||
<Text type="body2" style={[pal.textLight]}>
|
|
||||||
{pluralize(view.membersCount, 'member')}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
) : undefined}
|
|
||||||
<View style={[s.flexRow, s.mr10]}>
|
<View style={[s.flexRow, s.mr10]}>
|
||||||
<Text type="body2" style={[s.bold, s.mr2, pal.text]}>
|
<Text type="body2" style={[s.bold, s.mr2, pal.text]}>
|
||||||
{view.postsCount}
|
{view.postsCount}
|
||||||
|
@ -313,35 +242,6 @@ export const ProfileHeader = observer(function ProfileHeader({
|
||||||
entities={view.descriptionEntities}
|
entities={view.descriptionEntities}
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
{view.isScene && view.creator ? (
|
|
||||||
<View style={styles.detailLine}>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={['far', 'user']}
|
|
||||||
style={[pal.textLight, s.mr5]}
|
|
||||||
/>
|
|
||||||
<Text type="body2" style={[s.mr2, pal.textLight]}>
|
|
||||||
Created by
|
|
||||||
</Text>
|
|
||||||
<UserInfoText
|
|
||||||
type="body2"
|
|
||||||
style={[pal.link]}
|
|
||||||
did={view.creator}
|
|
||||||
prefix="@"
|
|
||||||
asLink
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
) : undefined}
|
|
||||||
{view.isScene && view.myState.member ? (
|
|
||||||
<View style={styles.detailLine}>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={['far', 'circle-check']}
|
|
||||||
style={[pal.textLight, s.mr5]}
|
|
||||||
/>
|
|
||||||
<Text type="body2" style={[s.mr2, pal.textLight]}>
|
|
||||||
You are a member
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
) : undefined}
|
|
||||||
{view.myState.muted ? (
|
{view.myState.muted ? (
|
||||||
<View style={[styles.detailLine, pal.btn, s.p5]}>
|
<View style={[styles.detailLine, pal.btn, s.p5]}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
|
@ -354,28 +254,6 @@ export const ProfileHeader = observer(function ProfileHeader({
|
||||||
</View>
|
</View>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
</View>
|
</View>
|
||||||
{view.isScene && view.creator === store.me.did ? (
|
|
||||||
<View style={[styles.sceneAdminContainer, pal.border]}>
|
|
||||||
<TouchableOpacity
|
|
||||||
testID="profileHeaderInviteMembersButton"
|
|
||||||
onPress={onPressInviteMembers}>
|
|
||||||
<LinearGradient
|
|
||||||
colors={[gradient[1], gradient[0]]}
|
|
||||||
start={{x: 0, y: 0}}
|
|
||||||
end={{x: 1, y: 1}}
|
|
||||||
style={[styles.btn, styles.gradientBtn, styles.sceneAdminBtn]}>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon="user-plus"
|
|
||||||
style={[s.mr5, s.white]}
|
|
||||||
size={15}
|
|
||||||
/>
|
|
||||||
<Text type="button" style={[s.bold, s.white]}>
|
|
||||||
Invite Members
|
|
||||||
</Text>
|
|
||||||
</LinearGradient>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
) : undefined}
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="profileHeaderAviButton"
|
testID="profileHeaderAviButton"
|
||||||
style={[pal.view, {borderColor: pal.colors.background}, styles.avi]}
|
style={[pal.view, {borderColor: pal.colors.background}, styles.avi]}
|
||||||
|
@ -444,15 +322,6 @@ const styles = StyleSheet.create({
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
},
|
},
|
||||||
typeLabelWrapper: {
|
|
||||||
paddingHorizontal: 4,
|
|
||||||
borderRadius: 4,
|
|
||||||
marginRight: 5,
|
|
||||||
},
|
|
||||||
typeLabel: {
|
|
||||||
fontSize: 15,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
},
|
|
||||||
|
|
||||||
metricsLine: {
|
metricsLine: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
@ -468,14 +337,4 @@ const styles = StyleSheet.create({
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: 5,
|
marginBottom: 5,
|
||||||
},
|
},
|
||||||
|
|
||||||
sceneAdminContainer: {
|
|
||||||
borderTopWidth: 1,
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
paddingVertical: 12,
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
},
|
|
||||||
sceneAdminBtn: {
|
|
||||||
paddingVertical: 8,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,80 +0,0 @@
|
||||||
import React, {useEffect} from 'react'
|
|
||||||
import {observer} from 'mobx-react-lite'
|
|
||||||
import {ActivityIndicator, FlatList, View} from 'react-native'
|
|
||||||
import {MembersViewModel, MemberItem} from '../../../state/models/members-view'
|
|
||||||
import {ProfileCard} from './ProfileCard'
|
|
||||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
|
||||||
import {useStores} from '../../../state'
|
|
||||||
|
|
||||||
export const ProfileMembers = observer(function ProfileMembers({
|
|
||||||
name,
|
|
||||||
}: {
|
|
||||||
name: string
|
|
||||||
}) {
|
|
||||||
const store = useStores()
|
|
||||||
// Using default import (React.use...) instead of named import (use...) to be able to mock store's data in jest environment
|
|
||||||
const [view, setView] = React.useState<MembersViewModel | undefined>()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (view?.params.actor === name) {
|
|
||||||
return // no change needed? or trigger refresh?
|
|
||||||
}
|
|
||||||
const newView = new MembersViewModel(store, {actor: name})
|
|
||||||
setView(newView)
|
|
||||||
newView
|
|
||||||
.setup()
|
|
||||||
.catch(err => store.log.error('Failed to fetch members', err))
|
|
||||||
}, [name, view?.params.actor, store])
|
|
||||||
|
|
||||||
const onRefresh = () => {
|
|
||||||
view?.refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
// loading
|
|
||||||
// =
|
|
||||||
if (
|
|
||||||
!view ||
|
|
||||||
(view.isLoading && !view.isRefreshing) ||
|
|
||||||
view.params.actor !== name
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<View testID="profileMembersActivityIndicatorView">
|
|
||||||
<ActivityIndicator />
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// error
|
|
||||||
// =
|
|
||||||
if (view.hasError) {
|
|
||||||
return (
|
|
||||||
<View>
|
|
||||||
<ErrorMessage
|
|
||||||
message={view.error}
|
|
||||||
style={{margin: 6}}
|
|
||||||
onPressTryAgain={onRefresh}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// loaded
|
|
||||||
// =
|
|
||||||
const renderItem = ({item}: {item: MemberItem}) => (
|
|
||||||
<ProfileCard
|
|
||||||
did={item.did}
|
|
||||||
handle={item.handle}
|
|
||||||
displayName={item.displayName}
|
|
||||||
avatar={item.avatar}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
<View testID="profileMembersFlatList">
|
|
||||||
<FlatList
|
|
||||||
data={view.members}
|
|
||||||
keyExtractor={item => item._reactKey}
|
|
||||||
renderItem={renderItem}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
})
|
|
|
@ -12,7 +12,6 @@ import {faArrowRightFromBracket} from '@fortawesome/free-solid-svg-icons'
|
||||||
import {faArrowUpFromBracket} from '@fortawesome/free-solid-svg-icons/faArrowUpFromBracket'
|
import {faArrowUpFromBracket} from '@fortawesome/free-solid-svg-icons/faArrowUpFromBracket'
|
||||||
import {faArrowUpRightFromSquare} from '@fortawesome/free-solid-svg-icons/faArrowUpRightFromSquare'
|
import {faArrowUpRightFromSquare} from '@fortawesome/free-solid-svg-icons/faArrowUpRightFromSquare'
|
||||||
import {faArrowsRotate} from '@fortawesome/free-solid-svg-icons/faArrowsRotate'
|
import {faArrowsRotate} from '@fortawesome/free-solid-svg-icons/faArrowsRotate'
|
||||||
import {faArrowTrendUp} from '@fortawesome/free-solid-svg-icons/faArrowTrendUp'
|
|
||||||
import {faAt} from '@fortawesome/free-solid-svg-icons/faAt'
|
import {faAt} from '@fortawesome/free-solid-svg-icons/faAt'
|
||||||
import {faBars} from '@fortawesome/free-solid-svg-icons/faBars'
|
import {faBars} from '@fortawesome/free-solid-svg-icons/faBars'
|
||||||
import {faBell} from '@fortawesome/free-solid-svg-icons/faBell'
|
import {faBell} from '@fortawesome/free-solid-svg-icons/faBell'
|
||||||
|
@ -81,7 +80,6 @@ export function setup() {
|
||||||
faArrowUpFromBracket,
|
faArrowUpFromBracket,
|
||||||
faArrowUpRightFromSquare,
|
faArrowUpRightFromSquare,
|
||||||
faArrowsRotate,
|
faArrowsRotate,
|
||||||
faArrowTrendUp,
|
|
||||||
faAt,
|
faAt,
|
||||||
faBars,
|
faBars,
|
||||||
faBell,
|
faBell,
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import {ImageSourcePropType} from 'react-native'
|
import {ImageSourcePropType} from 'react-native'
|
||||||
|
|
||||||
export const DEF_AVATAR: ImageSourcePropType = require('../../../public/img/default-avatar.jpg')
|
export const DEF_AVATAR: ImageSourcePropType = require('../../../public/img/default-avatar.jpg')
|
||||||
export const SCENE_EXPLAINER: ImageSourcePropType = require('../../../public/img/scene-explainer.jpg')
|
|
||||||
export const TABS_EXPLAINER: ImageSourcePropType = require('../../../public/img/tabs-explainer.jpg')
|
export const TABS_EXPLAINER: ImageSourcePropType = require('../../../public/img/tabs-explainer.jpg')
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
import {ImageSourcePropType} from 'react-native'
|
import {ImageSourcePropType} from 'react-native'
|
||||||
|
|
||||||
export const DEF_AVATAR: ImageSourcePropType = {uri: '/img/default-avatar.jpg'}
|
export const DEF_AVATAR: ImageSourcePropType = {uri: '/img/default-avatar.jpg'}
|
||||||
export const SCENE_EXPLAINER: ImageSourcePropType = {
|
|
||||||
uri: '/img/scene-explainer.jpg',
|
|
||||||
}
|
|
||||||
export const TABS_EXPLAINER: ImageSourcePropType = {
|
export const TABS_EXPLAINER: ImageSourcePropType = {
|
||||||
uri: '/img/tabs-explainer.jpg',
|
uri: '/img/tabs-explainer.jpg',
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,6 @@ import {PostRepostedBy} from './screens/PostRepostedBy'
|
||||||
import {Profile} from './screens/Profile'
|
import {Profile} from './screens/Profile'
|
||||||
import {ProfileFollowers} from './screens/ProfileFollowers'
|
import {ProfileFollowers} from './screens/ProfileFollowers'
|
||||||
import {ProfileFollows} from './screens/ProfileFollows'
|
import {ProfileFollows} from './screens/ProfileFollows'
|
||||||
import {ProfileMembers} from './screens/ProfileMembers'
|
|
||||||
import {Settings} from './screens/Settings'
|
import {Settings} from './screens/Settings'
|
||||||
import {Debug} from './screens/Debug'
|
import {Debug} from './screens/Debug'
|
||||||
import {Log} from './screens/Log'
|
import {Log} from './screens/Log'
|
||||||
|
@ -48,7 +47,6 @@ export const routes: Route[] = [
|
||||||
r('/profile/(?<name>[^/]+)/followers'),
|
r('/profile/(?<name>[^/]+)/followers'),
|
||||||
],
|
],
|
||||||
[ProfileFollows, 'Follows', 'users', r('/profile/(?<name>[^/]+)/follows')],
|
[ProfileFollows, 'Follows', 'users', r('/profile/(?<name>[^/]+)/follows')],
|
||||||
[ProfileMembers, 'Members', 'users', r('/profile/(?<name>[^/]+)/members')],
|
|
||||||
[
|
[
|
||||||
PostThread,
|
PostThread,
|
||||||
'Post',
|
'Post',
|
||||||
|
|
|
@ -15,7 +15,6 @@ export const Notifications = ({navIdx, visible}: ScreenParams) => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
store.log.debug('Updating notifications feed')
|
store.log.debug('Updating notifications feed')
|
||||||
store.me.refreshMemberships() // needed for the invite notifications
|
|
||||||
store.me.notifications
|
store.me.notifications
|
||||||
.update()
|
.update()
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
|
|
|
@ -1,16 +1,13 @@
|
||||||
import React, {useEffect, useState} from 'react'
|
import React, {useEffect, useState} from 'react'
|
||||||
import {ActivityIndicator, StyleSheet, View} from 'react-native'
|
import {ActivityIndicator, StyleSheet, View} from 'react-native'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
|
||||||
import {ViewSelector} from '../com/util/ViewSelector'
|
import {ViewSelector} from '../com/util/ViewSelector'
|
||||||
import {ScreenParams} from '../routes'
|
import {ScreenParams} from '../routes'
|
||||||
import {ProfileUiModel, Sections} from '../../state/models/profile-ui'
|
import {ProfileUiModel, Sections} from '../../state/models/profile-ui'
|
||||||
import {MembershipItem} from '../../state/models/memberships-view'
|
|
||||||
import {useStores} from '../../state'
|
import {useStores} from '../../state'
|
||||||
import {ConfirmModal} from '../../state/models/shell-ui'
|
import {ConfirmModal} from '../../state/models/shell-ui'
|
||||||
import {ProfileHeader} from '../com/profile/ProfileHeader'
|
import {ProfileHeader} from '../com/profile/ProfileHeader'
|
||||||
import {FeedItem} from '../com/posts/FeedItem'
|
import {FeedItem} from '../com/posts/FeedItem'
|
||||||
import {ProfileCard} from '../com/profile/ProfileCard'
|
|
||||||
import {PostFeedLoadingPlaceholder} from '../com/util/LoadingPlaceholder'
|
import {PostFeedLoadingPlaceholder} from '../com/util/LoadingPlaceholder'
|
||||||
import {ErrorScreen} from '../com/util/error/ErrorScreen'
|
import {ErrorScreen} from '../com/util/error/ErrorScreen'
|
||||||
import {ErrorMessage} from '../com/util/error/ErrorMessage'
|
import {ErrorMessage} from '../com/util/error/ErrorMessage'
|
||||||
|
@ -77,18 +74,6 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => {
|
||||||
const onPressTryAgain = () => {
|
const onPressTryAgain = () => {
|
||||||
uiState.setup()
|
uiState.setup()
|
||||||
}
|
}
|
||||||
const onPressRemoveMember = (membership: MembershipItem) => {
|
|
||||||
store.shell.openModal(
|
|
||||||
new ConfirmModal(
|
|
||||||
`Remove ${membership.displayName || membership.handle}?`,
|
|
||||||
`You'll be able to invite them again if you change your mind.`,
|
|
||||||
async () => {
|
|
||||||
await uiState.members.removeMember(membership.did)
|
|
||||||
Toast.show(`User removed`)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onPressCompose = () => {
|
const onPressCompose = () => {
|
||||||
store.shell.openComposer({})
|
store.shell.openComposer({})
|
||||||
|
@ -97,9 +82,6 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => {
|
||||||
// rendering
|
// rendering
|
||||||
// =
|
// =
|
||||||
|
|
||||||
const isSceneCreator =
|
|
||||||
uiState.isScene && store.me.did === uiState.profile.creator
|
|
||||||
|
|
||||||
const renderHeader = () => {
|
const renderHeader = () => {
|
||||||
if (!uiState) {
|
if (!uiState) {
|
||||||
return <View />
|
return <View />
|
||||||
|
@ -131,8 +113,7 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => {
|
||||||
} else {
|
} else {
|
||||||
if (
|
if (
|
||||||
uiState.selectedView === Sections.Posts ||
|
uiState.selectedView === Sections.Posts ||
|
||||||
uiState.selectedView === Sections.PostsWithReplies ||
|
uiState.selectedView === Sections.PostsWithReplies
|
||||||
uiState.selectedView === Sections.Trending
|
|
||||||
) {
|
) {
|
||||||
if (uiState.feed.hasContent) {
|
if (uiState.feed.hasContent) {
|
||||||
if (uiState.selectedView === Sections.Posts) {
|
if (uiState.selectedView === Sections.Posts) {
|
||||||
|
@ -153,14 +134,6 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => {
|
||||||
}
|
}
|
||||||
} else if (uiState.feed.isEmpty) {
|
} else if (uiState.feed.isEmpty) {
|
||||||
items = items.concat([EMPTY_ITEM])
|
items = items.concat([EMPTY_ITEM])
|
||||||
if (uiState.profile.isScene) {
|
|
||||||
renderItem = () => (
|
|
||||||
<EmptyState
|
|
||||||
icon="user-group"
|
|
||||||
message="As members upvote posts, they will trend here. Follow the scene to see its trending posts in your timeline."
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
renderItem = () => (
|
renderItem = () => (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
icon={['far', 'message']}
|
icon={['far', 'message']}
|
||||||
|
@ -169,67 +142,6 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => {
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else if (uiState.selectedView === Sections.Scenes) {
|
|
||||||
if (uiState.memberships.hasContent) {
|
|
||||||
items = uiState.memberships.memberships.slice()
|
|
||||||
renderItem = (item: any) => {
|
|
||||||
return (
|
|
||||||
<ProfileCard
|
|
||||||
did={item.did}
|
|
||||||
handle={item.handle}
|
|
||||||
displayName={item.displayName}
|
|
||||||
avatar={item.avatar}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if (uiState.memberships.isEmpty) {
|
|
||||||
items = items.concat([EMPTY_ITEM])
|
|
||||||
renderItem = () => (
|
|
||||||
<EmptyState
|
|
||||||
icon="user-group"
|
|
||||||
message="This user hasn't joined any scenes."
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if (uiState.selectedView === Sections.Members) {
|
|
||||||
if (uiState.members.hasContent) {
|
|
||||||
items = uiState.members.members.slice()
|
|
||||||
renderItem = (item: any) => {
|
|
||||||
const shouldAdmin = isSceneCreator && item.did !== store.me.did
|
|
||||||
const renderButton = shouldAdmin
|
|
||||||
? () => (
|
|
||||||
<>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
testID="shouldAdminButton"
|
|
||||||
icon="user-xmark"
|
|
||||||
style={[s.mr5]}
|
|
||||||
size={14}
|
|
||||||
/>
|
|
||||||
<Text style={[s.fw400, s.f14]}>Remove</Text>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
: undefined
|
|
||||||
return (
|
|
||||||
<ProfileCard
|
|
||||||
did={item.did}
|
|
||||||
handle={item.handle}
|
|
||||||
displayName={item.displayName}
|
|
||||||
avatar={item.avatar}
|
|
||||||
renderButton={renderButton}
|
|
||||||
onPressButton={() => onPressRemoveMember(item)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if (uiState.members.isEmpty) {
|
|
||||||
items = items.concat([EMPTY_ITEM])
|
|
||||||
renderItem = () => (
|
|
||||||
<EmptyState
|
|
||||||
icon="user-group"
|
|
||||||
message="This scene doesn't have any members."
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
items = items.concat([EMPTY_ITEM])
|
items = items.concat([EMPTY_ITEM])
|
||||||
renderItem = () => <Text>TODO</Text>
|
renderItem = () => <Text>TODO</Text>
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
import React, {useEffect} from 'react'
|
|
||||||
import {View} from 'react-native'
|
|
||||||
import {ViewHeader} from '../com/util/ViewHeader'
|
|
||||||
import {ProfileMembers as ProfileMembersComponent} from '../com/profile/ProfileMembers'
|
|
||||||
import {ScreenParams} from '../routes'
|
|
||||||
import {useStores} from '../../state'
|
|
||||||
|
|
||||||
export const ProfileMembers = ({navIdx, visible, params}: ScreenParams) => {
|
|
||||||
const store = useStores()
|
|
||||||
const {name} = params
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (visible) {
|
|
||||||
store.nav.setTitle(navIdx, `Members of ${name}`)
|
|
||||||
store.shell.setMinimalShellMode(false)
|
|
||||||
}
|
|
||||||
}, [store, visible, name])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View>
|
|
||||||
<ViewHeader title="Members" subtitle={`of ${name}`} />
|
|
||||||
<ProfileMembersComponent name={name} />
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, {useEffect} from 'react'
|
import React from 'react'
|
||||||
import {
|
import {
|
||||||
ScrollView,
|
ScrollView,
|
||||||
StyleProp,
|
StyleProp,
|
||||||
|
@ -11,17 +11,10 @@ import {observer} from 'mobx-react-lite'
|
||||||
import VersionNumber from 'react-native-version-number'
|
import VersionNumber from 'react-native-version-number'
|
||||||
import {s, colors} from '../../lib/styles'
|
import {s, colors} from '../../lib/styles'
|
||||||
import {useStores} from '../../../state'
|
import {useStores} from '../../../state'
|
||||||
import {
|
import {HomeIcon, BellIcon, CogIcon, MagnifyingGlassIcon} from '../../lib/icons'
|
||||||
HomeIcon,
|
|
||||||
UserGroupIcon,
|
|
||||||
BellIcon,
|
|
||||||
CogIcon,
|
|
||||||
MagnifyingGlassIcon,
|
|
||||||
} from '../../lib/icons'
|
|
||||||
import {UserAvatar} from '../../com/util/UserAvatar'
|
import {UserAvatar} from '../../com/util/UserAvatar'
|
||||||
import {Text} from '../../com/util/text/Text'
|
import {Text} from '../../com/util/text/Text'
|
||||||
import {ToggleButton} from '../../com/util/forms/ToggleButton'
|
import {ToggleButton} from '../../com/util/forms/ToggleButton'
|
||||||
import {CreateSceneModal} from '../../../state/models/shell-ui'
|
|
||||||
import {usePalette} from '../../lib/hooks/usePalette'
|
import {usePalette} from '../../lib/hooks/usePalette'
|
||||||
|
|
||||||
export const Menu = observer(
|
export const Menu = observer(
|
||||||
|
@ -29,14 +22,6 @@ export const Menu = observer(
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (visible) {
|
|
||||||
// trigger a refresh in case memberships have changed recently
|
|
||||||
// TODO this impacts performance, need to find the right time to do this
|
|
||||||
// store.me.refreshMemberships()
|
|
||||||
}
|
|
||||||
}, [store, visible])
|
|
||||||
|
|
||||||
// events
|
// events
|
||||||
// =
|
// =
|
||||||
|
|
||||||
|
@ -51,10 +36,6 @@ export const Menu = observer(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const onPressCreateScene = () => {
|
|
||||||
onClose()
|
|
||||||
store.shell.openModal(new CreateSceneModal())
|
|
||||||
}
|
|
||||||
|
|
||||||
// rendering
|
// rendering
|
||||||
// =
|
// =
|
||||||
|
@ -152,40 +133,6 @@ export const Menu = observer(
|
||||||
url="/notifications"
|
url="/notifications"
|
||||||
count={store.me.notificationCount}
|
count={store.me.notificationCount}
|
||||||
/>
|
/>
|
||||||
</View>
|
|
||||||
<View style={[styles.section, pal.border]}>
|
|
||||||
<Text type="h5" style={[pal.text, styles.heading]}>
|
|
||||||
Scenes
|
|
||||||
</Text>
|
|
||||||
{store.me.memberships
|
|
||||||
? store.me.memberships.memberships.map((membership, i) => (
|
|
||||||
<MenuItem
|
|
||||||
key={i}
|
|
||||||
icon={
|
|
||||||
<UserAvatar
|
|
||||||
size={34}
|
|
||||||
displayName={membership.displayName}
|
|
||||||
handle={membership.handle}
|
|
||||||
avatar={membership.avatar}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label={membership.displayName || membership.handle}
|
|
||||||
url={`/profile/${membership.handle}`}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
: undefined}
|
|
||||||
</View>
|
|
||||||
<View style={[styles.section, pal.border]}>
|
|
||||||
<MenuItem
|
|
||||||
icon={
|
|
||||||
<UserGroupIcon
|
|
||||||
style={pal.text as StyleProp<ViewStyle>}
|
|
||||||
size="30"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Create a scene"
|
|
||||||
onPress={onPressCreateScene}
|
|
||||||
/>
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={
|
icon={
|
||||||
<CogIcon
|
<CogIcon
|
||||||
|
|
Loading…
Reference in New Issue