Implement scene invitation and membership controls

zio/stable
Paul Frazee 2022-11-10 16:30:14 -06:00
parent ecf56729b0
commit d3707f30e3
49 changed files with 2603 additions and 462 deletions

View File

@ -8,6 +8,7 @@ import {sessionClient as AtpApi} from '../../third-party/api'
import * as Profile from '../../third-party/api/src/client/types/app/bsky/actor/profile' import * as Profile from '../../third-party/api/src/client/types/app/bsky/actor/profile'
import * as Post from '../../third-party/api/src/client/types/app/bsky/feed/post' import * as Post from '../../third-party/api/src/client/types/app/bsky/feed/post'
import {AtUri} from '../../third-party/uri' import {AtUri} from '../../third-party/uri'
import {APP_BSKY_GRAPH} from '../../third-party/api'
import {RootStoreModel} from '../models/root-store' import {RootStoreModel} from '../models/root-store'
import {extractEntities} from '../../view/lib/strings' import {extractEntities} from '../../view/lib/strings'
@ -156,6 +157,54 @@ export async function updateProfile(
} }
} }
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>

View File

@ -0,0 +1,4 @@
export interface Declaration {
cid: string
actorType: string
}

View File

@ -21,8 +21,13 @@ export class FeedItemModel implements GetTimeline.FeedItem {
// data // data
uri: string = '' uri: string = ''
cid: string = '' cid: string = ''
author: GetTimeline.User = {did: '', handle: '', displayName: ''} author: GetTimeline.Actor = {
repostedBy?: GetTimeline.User did: '',
handle: '',
displayName: '',
declaration: {cid: '', actorType: ''},
}
repostedBy?: GetTimeline.Actor
record: Record<string, unknown> = {} record: Record<string, unknown> = {}
embed?: embed?:
| GetTimeline.RecordEmbed | GetTimeline.RecordEmbed

View File

@ -47,6 +47,10 @@ export class MembershipsViewModel {
return this.hasLoaded && !this.hasContent return this.hasLoaded && !this.hasContent
} }
isMemberOf(did: string) {
return !!this.memberships.find(m => m.did === did)
}
// public api // public api
// = // =

View File

@ -1,7 +1,11 @@
import {makeAutoObservable} from 'mobx' import {makeAutoObservable} from 'mobx'
import * as ListNotifications from '../../third-party/api/src/client/types/app/bsky/notification/list' import * as ListNotifications from '../../third-party/api/src/client/types/app/bsky/notification/list'
import {RootStoreModel} from './root-store' import {RootStoreModel} from './root-store'
import {Declaration} from './_common'
import {hasProp} from '../lib/type-guards' import {hasProp} from '../lib/type-guards'
import {APP_BSKY_GRAPH} from '../../third-party/api'
const UNGROUPABLE_REASONS = ['trend', 'assertion']
export interface GroupedNotification extends ListNotifications.Notification { export interface GroupedNotification extends ListNotifications.Notification {
additional?: ListNotifications.Notification[] additional?: ListNotifications.Notification[]
@ -18,7 +22,8 @@ export class NotificationsViewItemModel implements GroupedNotification {
did: string did: string
handle: string handle: string
displayName?: string displayName?: string
} = {did: '', handle: ''} declaration: Declaration
} = {did: '', handle: '', declaration: {cid: '', actorType: ''}}
reason: string = '' reason: string = ''
reasonSubject?: string reasonSubject?: string
record: any = {} record: any = {}
@ -65,6 +70,10 @@ export class NotificationsViewItemModel implements GroupedNotification {
return this.reason === 'repost' return this.reason === 'repost'
} }
get isTrend() {
return this.reason === 'trend'
}
get isReply() { get isReply() {
return this.reason === 'reply' return this.reason === 'reply'
} }
@ -73,6 +82,16 @@ export class NotificationsViewItemModel implements GroupedNotification {
return this.reason === 'follow' return this.reason === 'follow'
} }
get isAssertion() {
return this.reason === 'assertion'
}
get isInvite() {
return (
this.isAssertion && this.record.assertion === APP_BSKY_GRAPH.AssertMember
)
}
get subjectUri() { get subjectUri() {
if (this.reasonSubject) { if (this.reasonSubject) {
return this.reasonSubject return this.reasonSubject
@ -316,16 +335,18 @@ function groupNotifications(
const items2: GroupedNotification[] = [] const items2: GroupedNotification[] = []
for (const item of items) { for (const item of items) {
let grouped = false let grouped = false
for (const item2 of items2) { if (!UNGROUPABLE_REASONS.includes(item.reason)) {
if ( for (const item2 of items2) {
item.reason === item2.reason && if (
item.reasonSubject === item2.reasonSubject && item.reason === item2.reason &&
item.author.did !== item2.author.did item.reasonSubject === item2.reasonSubject &&
) { item.author.did !== item2.author.did
item2.additional = item2.additional || [] ) {
item2.additional.push(item) item2.additional = item2.additional || []
grouped = true item2.additional.push(item)
break grouped = true
break
}
} }
} }
if (!grouped) { if (!grouped) {

View File

@ -31,7 +31,12 @@ export class PostThreadViewPostModel implements GetPostThread.Post {
// data // data
uri: string = '' uri: string = ''
cid: string = '' cid: string = ''
author: GetPostThread.User = {did: '', handle: '', displayName: ''} author: GetPostThread.User = {
did: '',
handle: '',
displayName: '',
declaration: {cid: '', actorType: ''},
}
record: Record<string, unknown> = {} record: Record<string, unknown> = {}
embed?: embed?:
| GetPostThread.RecordEmbed | GetPostThread.RecordEmbed

View File

@ -1,6 +1,7 @@
import {makeAutoObservable, runInAction} from 'mobx' import {makeAutoObservable, runInAction} from 'mobx'
import * as GetProfile from '../../third-party/api/src/client/types/app/bsky/actor/getProfile' import * as GetProfile from '../../third-party/api/src/client/types/app/bsky/actor/getProfile'
import * as Profile from '../../third-party/api/src/client/types/app/bsky/actor/profile' import * as Profile from '../../third-party/api/src/client/types/app/bsky/actor/profile'
import {Declaration} from './_common'
import {RootStoreModel} from './root-store' import {RootStoreModel} from './root-store'
import * as apilib from '../lib/api' import * as apilib from '../lib/api'
@ -9,6 +10,7 @@ export const ACTOR_TYPE_SCENE = 'app.bsky.system.actorScene'
export class ProfileViewMyStateModel { export class ProfileViewMyStateModel {
follow?: string follow?: string
member?: string
constructor() { constructor() {
makeAutoObservable(this) makeAutoObservable(this)
@ -26,7 +28,10 @@ export class ProfileViewModel {
// data // data
did: string = '' did: string = ''
handle: string = '' handle: string = ''
actorType = ACTOR_TYPE_USER declaration: Declaration = {
cid: '',
actorType: '',
}
creator: string = '' creator: string = ''
displayName?: string displayName?: string
description?: string description?: string
@ -64,11 +69,11 @@ export class ProfileViewModel {
} }
get isUser() { get isUser() {
return this.actorType === ACTOR_TYPE_USER return this.declaration.actorType === ACTOR_TYPE_USER
} }
get isScene() { get isScene() {
return this.actorType === ACTOR_TYPE_SCENE return this.declaration.actorType === ACTOR_TYPE_SCENE
} }
// public api // public api
@ -145,7 +150,7 @@ export class ProfileViewModel {
console.log(res.data) console.log(res.data)
this.did = res.data.did this.did = res.data.did
this.handle = res.data.handle this.handle = res.data.handle
this.actorType = res.data.actorType Object.assign(this.declaration, res.data.declaration)
this.creator = res.data.creator this.creator = res.data.creator
this.displayName = res.data.displayName this.displayName = res.data.displayName
this.description = res.data.description this.description = res.data.description

View File

@ -2,6 +2,7 @@ import {makeAutoObservable, runInAction} from 'mobx'
import {AtUri} from '../../third-party/uri' import {AtUri} from '../../third-party/uri'
import * as GetRepostedBy from '../../third-party/api/src/client/types/app/bsky/feed/getRepostedBy' import * as GetRepostedBy from '../../third-party/api/src/client/types/app/bsky/feed/getRepostedBy'
import {RootStoreModel} from './root-store' import {RootStoreModel} from './root-store'
import {Declaration} from './_common'
type RepostedByItem = GetRepostedBy.OutputSchema['repostedBy'][number] type RepostedByItem = GetRepostedBy.OutputSchema['repostedBy'][number]
@ -13,6 +14,7 @@ export class RepostedByViewItemModel implements RepostedByItem {
did: string = '' did: string = ''
handle: string = '' handle: string = ''
displayName: string = '' displayName: string = ''
declaration: Declaration = {cid: '', actorType: ''}
createdAt?: string createdAt?: string
indexedAt: string = '' indexedAt: string = ''

View File

@ -0,0 +1,126 @@
import {makeAutoObservable, runInAction} from 'mobx'
import {RootStoreModel} from './root-store'
import {MembersViewModel} from './members-view'
import {UserFollowsViewModel, FollowItem} from './user-follows-view'
export interface SceneInviteSuggestionsParams {
sceneDid: string
}
export class SceneInviteSuggestions {
// state
isLoading = false
isRefreshing = false
hasLoaded = false
error = ''
params: SceneInviteSuggestionsParams
sceneMembersView: MembersViewModel
myFollowsView: UserFollowsViewModel
// data
suggestions: FollowItem[] = []
constructor(
public rootStore: RootStoreModel,
params: SceneInviteSuggestionsParams,
) {
makeAutoObservable(
this,
{
rootStore: false,
params: false,
},
{autoBind: true},
)
this.params = params
this.sceneMembersView = new MembersViewModel(rootStore, {
actor: params.sceneDid,
})
this.myFollowsView = new UserFollowsViewModel(rootStore, {
user: rootStore.me.did || '',
})
}
get hasContent() {
return this.suggestions.length > 0
}
get hasError() {
return this.error !== ''
}
get isEmpty() {
return this.hasLoaded && !this.hasContent
}
// public api
// =
async setup() {
await this._fetch(false)
}
async refresh() {
await this._fetch(true)
}
async loadMore() {
// TODO
}
// state transitions
// =
private _xLoading(isRefreshing = false) {
this.isLoading = true
this.isRefreshing = isRefreshing
this.error = ''
}
private _xIdle(err: string = '') {
this.isLoading = false
this.isRefreshing = false
this.hasLoaded = true
this.error = err
}
// loader functions
// =
private async _fetch(isRefreshing = false) {
this._xLoading(isRefreshing)
try {
await this.sceneMembersView.setup()
} catch (e) {
console.error(e)
this._xIdle(
'Failed to fetch the current scene members. Check your internet connection and try again.',
)
return
}
try {
await this.myFollowsView.setup()
} catch (e) {
console.error(e)
this._xIdle(
'Failed to fetch the your current followers. Check your internet connection and try again.',
)
return
}
// collect all followed users that arent already in the scene
const newSuggestions: FollowItem[] = []
for (const follow of this.myFollowsView.follows) {
// TODO: filter out scenes
if (
!this.sceneMembersView.members.find(member => member.did === follow.did)
) {
newSuggestions.push(follow)
}
}
runInAction(() => {
this.suggestions = newSuggestions
})
this._xIdle()
}
}

View File

@ -19,6 +19,18 @@ export class LinkActionsModel {
} }
} }
export class ConfirmModel {
name = 'confirm'
constructor(
public title: string,
public message: string | (() => JSX.Element),
public onPressConfirm: () => void | Promise<void>,
) {
makeAutoObservable(this)
}
}
export class SharePostModel { export class SharePostModel {
name = 'share-post' name = 'share-post'
@ -43,6 +55,14 @@ export class CreateSceneModel {
} }
} }
export class InviteToSceneModel {
name = 'invite-to-scene'
constructor(public profileView: ProfileViewModel) {
makeAutoObservable(this)
}
}
export interface ComposerOpts { export interface ComposerOpts {
replyTo?: Post.PostRef replyTo?: Post.PostRef
onPost?: () => void onPost?: () => void
@ -52,6 +72,7 @@ export class ShellUiModel {
isModalActive = false isModalActive = false
activeModal: activeModal:
| LinkActionsModel | LinkActionsModel
| ConfirmModel
| SharePostModel | SharePostModel
| EditProfileModel | EditProfileModel
| CreateSceneModel | CreateSceneModel
@ -66,6 +87,7 @@ export class ShellUiModel {
openModal( openModal(
modal: modal:
| LinkActionsModel | LinkActionsModel
| ConfirmModel
| SharePostModel | SharePostModel
| EditProfileModel | EditProfileModel
| CreateSceneModel, | CreateSceneModel,

View File

@ -1,5 +1,6 @@
import {makeAutoObservable} from 'mobx' import {makeAutoObservable} from 'mobx'
import {RootStoreModel} from './root-store' import {RootStoreModel} from './root-store'
import {Declaration} from './_common'
interface Response { interface Response {
data: { data: {
@ -9,6 +10,7 @@ interface Response {
export type ResponseSuggestedActor = { export type ResponseSuggestedActor = {
did: string did: string
handle: string handle: string
declaration: Declaration
displayName?: string displayName?: string
description?: string description?: string
createdAt?: string createdAt?: string
@ -109,7 +111,6 @@ export class SuggestedActorsViewModel {
for (const item of res.data.suggestions) { for (const item of res.data.suggestions) {
this._append({ this._append({
_reactKey: `item-${counter++}`, _reactKey: `item-${counter++}`,
description: 'Just another cool person using Bluesky',
...item, ...item,
}) })
} }

View File

@ -16,7 +16,12 @@ export class UserFollowersViewModel {
params: GetFollowers.QueryParams params: GetFollowers.QueryParams
// data // data
subject: Subject = {did: '', handle: '', displayName: ''} subject: Subject = {
did: '',
handle: '',
displayName: '',
declaration: {cid: '', actorType: ''},
}
followers: FollowerItem[] = [] followers: FollowerItem[] = []
constructor( constructor(

View File

@ -16,7 +16,12 @@ export class UserFollowsViewModel {
params: GetFollows.QueryParams params: GetFollows.QueryParams
// data // data
subject: Subject = {did: '', handle: '', displayName: ''} subject: Subject = {
did: '',
handle: '',
displayName: '',
declaration: {cid: '', actorType: ''},
}
follows: FollowItem[] = [] follows: FollowItem[] = []
constructor( constructor(

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -36,6 +36,8 @@ import * as AppBskyFeedGetVotes from './types/app/bsky/feed/getVotes';
import * as AppBskyFeedMediaEmbed from './types/app/bsky/feed/mediaEmbed'; import * as AppBskyFeedMediaEmbed from './types/app/bsky/feed/mediaEmbed';
import * as AppBskyFeedPost from './types/app/bsky/feed/post'; import * as AppBskyFeedPost from './types/app/bsky/feed/post';
import * as AppBskyFeedRepost from './types/app/bsky/feed/repost'; import * as AppBskyFeedRepost from './types/app/bsky/feed/repost';
import * as AppBskyFeedSetVote from './types/app/bsky/feed/setVote';
import * as AppBskyFeedTrend from './types/app/bsky/feed/trend';
import * as AppBskyFeedVote from './types/app/bsky/feed/vote'; import * as AppBskyFeedVote from './types/app/bsky/feed/vote';
import * as AppBskyGraphAssertion from './types/app/bsky/graph/assertion'; import * as AppBskyGraphAssertion from './types/app/bsky/graph/assertion';
import * as AppBskyGraphConfirmation from './types/app/bsky/graph/confirmation'; import * as AppBskyGraphConfirmation from './types/app/bsky/graph/confirmation';
@ -85,6 +87,8 @@ export * as AppBskyFeedGetVotes from './types/app/bsky/feed/getVotes';
export * as AppBskyFeedMediaEmbed from './types/app/bsky/feed/mediaEmbed'; export * as AppBskyFeedMediaEmbed from './types/app/bsky/feed/mediaEmbed';
export * as AppBskyFeedPost from './types/app/bsky/feed/post'; export * as AppBskyFeedPost from './types/app/bsky/feed/post';
export * as AppBskyFeedRepost from './types/app/bsky/feed/repost'; export * as AppBskyFeedRepost from './types/app/bsky/feed/repost';
export * as AppBskyFeedSetVote from './types/app/bsky/feed/setVote';
export * as AppBskyFeedTrend from './types/app/bsky/feed/trend';
export * as AppBskyFeedVote from './types/app/bsky/feed/vote'; export * as AppBskyFeedVote from './types/app/bsky/feed/vote';
export * as AppBskyGraphAssertion from './types/app/bsky/graph/assertion'; export * as AppBskyGraphAssertion from './types/app/bsky/graph/assertion';
export * as AppBskyGraphConfirmation from './types/app/bsky/graph/confirmation'; export * as AppBskyGraphConfirmation from './types/app/bsky/graph/confirmation';
@ -225,13 +229,14 @@ export declare class ProfileRecord {
uri: string; uri: string;
cid: string; cid: string;
}>; }>;
delete(params: Omit<ComAtprotoRepoDeleteRecord.QueryParams, 'collection'>, headers?: Record<string, string>): Promise<void>; delete(params: Omit<ComAtprotoRepoDeleteRecord.InputSchema, 'collection'>, headers?: Record<string, string>): Promise<void>;
} }
export declare class FeedNS { export declare class FeedNS {
_service: ServiceClient; _service: ServiceClient;
mediaEmbed: MediaEmbedRecord; mediaEmbed: MediaEmbedRecord;
post: PostRecord; post: PostRecord;
repost: RepostRecord; repost: RepostRecord;
trend: TrendRecord;
vote: VoteRecord; vote: VoteRecord;
constructor(service: ServiceClient); constructor(service: ServiceClient);
getAuthorFeed(params?: AppBskyFeedGetAuthorFeed.QueryParams, opts?: AppBskyFeedGetAuthorFeed.CallOptions): Promise<AppBskyFeedGetAuthorFeed.Response>; getAuthorFeed(params?: AppBskyFeedGetAuthorFeed.QueryParams, opts?: AppBskyFeedGetAuthorFeed.CallOptions): Promise<AppBskyFeedGetAuthorFeed.Response>;
@ -239,6 +244,7 @@ export declare class FeedNS {
getRepostedBy(params?: AppBskyFeedGetRepostedBy.QueryParams, opts?: AppBskyFeedGetRepostedBy.CallOptions): Promise<AppBskyFeedGetRepostedBy.Response>; getRepostedBy(params?: AppBskyFeedGetRepostedBy.QueryParams, opts?: AppBskyFeedGetRepostedBy.CallOptions): Promise<AppBskyFeedGetRepostedBy.Response>;
getTimeline(params?: AppBskyFeedGetTimeline.QueryParams, opts?: AppBskyFeedGetTimeline.CallOptions): Promise<AppBskyFeedGetTimeline.Response>; getTimeline(params?: AppBskyFeedGetTimeline.QueryParams, opts?: AppBskyFeedGetTimeline.CallOptions): Promise<AppBskyFeedGetTimeline.Response>;
getVotes(params?: AppBskyFeedGetVotes.QueryParams, opts?: AppBskyFeedGetVotes.CallOptions): Promise<AppBskyFeedGetVotes.Response>; getVotes(params?: AppBskyFeedGetVotes.QueryParams, opts?: AppBskyFeedGetVotes.CallOptions): Promise<AppBskyFeedGetVotes.Response>;
setVote(data?: AppBskyFeedSetVote.InputSchema, opts?: AppBskyFeedSetVote.CallOptions): Promise<AppBskyFeedSetVote.Response>;
} }
export declare class MediaEmbedRecord { export declare class MediaEmbedRecord {
_service: ServiceClient; _service: ServiceClient;
@ -259,7 +265,7 @@ export declare class MediaEmbedRecord {
uri: string; uri: string;
cid: string; cid: string;
}>; }>;
delete(params: Omit<ComAtprotoRepoDeleteRecord.QueryParams, 'collection'>, headers?: Record<string, string>): Promise<void>; delete(params: Omit<ComAtprotoRepoDeleteRecord.InputSchema, 'collection'>, headers?: Record<string, string>): Promise<void>;
} }
export declare class PostRecord { export declare class PostRecord {
_service: ServiceClient; _service: ServiceClient;
@ -280,7 +286,7 @@ export declare class PostRecord {
uri: string; uri: string;
cid: string; cid: string;
}>; }>;
delete(params: Omit<ComAtprotoRepoDeleteRecord.QueryParams, 'collection'>, headers?: Record<string, string>): Promise<void>; delete(params: Omit<ComAtprotoRepoDeleteRecord.InputSchema, 'collection'>, headers?: Record<string, string>): Promise<void>;
} }
export declare class RepostRecord { export declare class RepostRecord {
_service: ServiceClient; _service: ServiceClient;
@ -301,7 +307,28 @@ export declare class RepostRecord {
uri: string; uri: string;
cid: string; cid: string;
}>; }>;
delete(params: Omit<ComAtprotoRepoDeleteRecord.QueryParams, 'collection'>, headers?: Record<string, string>): Promise<void>; delete(params: Omit<ComAtprotoRepoDeleteRecord.InputSchema, 'collection'>, headers?: Record<string, string>): Promise<void>;
}
export declare class TrendRecord {
_service: ServiceClient;
constructor(service: ServiceClient);
list(params: Omit<ComAtprotoRepoListRecords.QueryParams, 'collection'>): Promise<{
cursor?: string;
records: {
uri: string;
value: AppBskyFeedTrend.Record;
}[];
}>;
get(params: Omit<ComAtprotoRepoGetRecord.QueryParams, 'collection'>): Promise<{
uri: string;
cid: string;
value: AppBskyFeedTrend.Record;
}>;
create(params: Omit<ComAtprotoRepoCreateRecord.InputSchema, 'collection' | 'record'>, record: AppBskyFeedTrend.Record, headers?: Record<string, string>): Promise<{
uri: string;
cid: string;
}>;
delete(params: Omit<ComAtprotoRepoDeleteRecord.InputSchema, 'collection'>, headers?: Record<string, string>): Promise<void>;
} }
export declare class VoteRecord { export declare class VoteRecord {
_service: ServiceClient; _service: ServiceClient;
@ -322,7 +349,7 @@ export declare class VoteRecord {
uri: string; uri: string;
cid: string; cid: string;
}>; }>;
delete(params: Omit<ComAtprotoRepoDeleteRecord.QueryParams, 'collection'>, headers?: Record<string, string>): Promise<void>; delete(params: Omit<ComAtprotoRepoDeleteRecord.InputSchema, 'collection'>, headers?: Record<string, string>): Promise<void>;
} }
export declare class GraphNS { export declare class GraphNS {
_service: ServiceClient; _service: ServiceClient;
@ -354,7 +381,7 @@ export declare class AssertionRecord {
uri: string; uri: string;
cid: string; cid: string;
}>; }>;
delete(params: Omit<ComAtprotoRepoDeleteRecord.QueryParams, 'collection'>, headers?: Record<string, string>): Promise<void>; delete(params: Omit<ComAtprotoRepoDeleteRecord.InputSchema, 'collection'>, headers?: Record<string, string>): Promise<void>;
} }
export declare class ConfirmationRecord { export declare class ConfirmationRecord {
_service: ServiceClient; _service: ServiceClient;
@ -375,7 +402,7 @@ export declare class ConfirmationRecord {
uri: string; uri: string;
cid: string; cid: string;
}>; }>;
delete(params: Omit<ComAtprotoRepoDeleteRecord.QueryParams, 'collection'>, headers?: Record<string, string>): Promise<void>; delete(params: Omit<ComAtprotoRepoDeleteRecord.InputSchema, 'collection'>, headers?: Record<string, string>): Promise<void>;
} }
export declare class FollowRecord { export declare class FollowRecord {
_service: ServiceClient; _service: ServiceClient;
@ -396,7 +423,7 @@ export declare class FollowRecord {
uri: string; uri: string;
cid: string; cid: string;
}>; }>;
delete(params: Omit<ComAtprotoRepoDeleteRecord.QueryParams, 'collection'>, headers?: Record<string, string>): Promise<void>; delete(params: Omit<ComAtprotoRepoDeleteRecord.InputSchema, 'collection'>, headers?: Record<string, string>): Promise<void>;
} }
export declare class NotificationNS { export declare class NotificationNS {
_service: ServiceClient; _service: ServiceClient;
@ -429,5 +456,5 @@ export declare class DeclarationRecord {
uri: string; uri: string;
cid: string; cid: string;
}>; }>;
delete(params: Omit<ComAtprotoRepoDeleteRecord.QueryParams, 'collection'>, headers?: Record<string, string>): Promise<void>; delete(params: Omit<ComAtprotoRepoDeleteRecord.InputSchema, 'collection'>, headers?: Record<string, string>): Promise<void>;
} }

View File

@ -6,6 +6,7 @@ export declare const ids: {
AppBskyFeedMediaEmbed: string; AppBskyFeedMediaEmbed: string;
AppBskyFeedPost: string; AppBskyFeedPost: string;
AppBskyFeedRepost: string; AppBskyFeedRepost: string;
AppBskyFeedTrend: string;
AppBskyFeedVote: string; AppBskyFeedVote: string;
AppBskyGraphAssertion: string; AppBskyGraphAssertion: string;
AppBskyGraphConfirmation: string; AppBskyGraphConfirmation: string;

View File

@ -10,10 +10,16 @@ export interface InputSchema {
handle: string; handle: string;
recoveryKey?: string; recoveryKey?: string;
} }
export declare type ActorKnown = 'app.bsky.system.actorUser' | 'app.bsky.system.actorScene';
export declare type ActorUnknown = string;
export interface OutputSchema { export interface OutputSchema {
handle: string; handle: string;
did: string; did: string;
declarationCid: string; declaration: Declaration;
}
export interface Declaration {
cid: string;
actorType: ActorKnown | ActorUnknown;
} }
export interface Response { export interface Response {
success: boolean; success: boolean;

View File

@ -10,8 +10,8 @@ export declare type ActorKnown = 'app.bsky.system.actorUser' | 'app.bsky.system.
export declare type ActorUnknown = string; export declare type ActorUnknown = string;
export interface OutputSchema { export interface OutputSchema {
did: string; did: string;
declaration: Declaration;
handle: string; handle: string;
actorType: ActorKnown | ActorUnknown;
creator: string; creator: string;
displayName?: string; displayName?: string;
description?: string; description?: string;
@ -21,8 +21,13 @@ export interface OutputSchema {
postsCount: number; postsCount: number;
myState?: { myState?: {
follow?: string; follow?: string;
member?: string;
}; };
} }
export interface Declaration {
cid: string;
actorType: ActorKnown | ActorUnknown;
}
export interface Response { export interface Response {
success: boolean; success: boolean;
headers: Headers; headers: Headers;

View File

@ -7,12 +7,14 @@ export interface CallOptions {
headers?: Headers; headers?: Headers;
} }
export declare type InputSchema = undefined; export declare type InputSchema = undefined;
export declare type ActorKnown = 'app.bsky.system.actorUser' | 'app.bsky.system.actorScene';
export declare type ActorUnknown = string;
export interface OutputSchema { export interface OutputSchema {
cursor?: string; cursor?: string;
actors: { actors: {
did: string; did: string;
declaration: Declaration;
handle: string; handle: string;
actorType: string;
displayName?: string; displayName?: string;
description?: string; description?: string;
indexedAt?: string; indexedAt?: string;
@ -21,6 +23,10 @@ export interface OutputSchema {
}; };
}[]; }[];
} }
export interface Declaration {
cid: string;
actorType: ActorKnown | ActorUnknown;
}
export interface Response { export interface Response {
success: boolean; success: boolean;
headers: Headers; headers: Headers;

View File

@ -8,16 +8,23 @@ export interface CallOptions {
headers?: Headers; headers?: Headers;
} }
export declare type InputSchema = undefined; export declare type InputSchema = undefined;
export declare type ActorKnown = 'app.bsky.system.actorUser' | 'app.bsky.system.actorScene';
export declare type ActorUnknown = string;
export interface OutputSchema { export interface OutputSchema {
cursor?: string; cursor?: string;
users: { users: {
did: string; did: string;
declaration: Declaration;
handle: string; handle: string;
displayName?: string; displayName?: string;
description?: string; description?: string;
indexedAt?: string; indexedAt?: string;
}[]; }[];
} }
export interface Declaration {
cid: string;
actorType: ActorKnown | ActorUnknown;
}
export interface Response { export interface Response {
success: boolean; success: boolean;
headers: Headers; headers: Headers;

View File

@ -7,13 +7,20 @@ export interface CallOptions {
headers?: Headers; headers?: Headers;
} }
export declare type InputSchema = undefined; export declare type InputSchema = undefined;
export declare type ActorKnown = 'app.bsky.system.actorUser' | 'app.bsky.system.actorScene';
export declare type ActorUnknown = string;
export interface OutputSchema { export interface OutputSchema {
users: { users: {
did: string; did: string;
declaration: Declaration;
handle: string; handle: string;
displayName?: string; displayName?: string;
}[]; }[];
} }
export interface Declaration {
cid: string;
actorType: ActorKnown | ActorUnknown;
}
export interface Response { export interface Response {
success: boolean; success: boolean;
headers: Headers; headers: Headers;

View File

@ -8,6 +8,8 @@ export interface CallOptions {
headers?: Headers; headers?: Headers;
} }
export declare type InputSchema = undefined; export declare type InputSchema = undefined;
export declare type ActorKnown = 'app.bsky.system.actorUser' | 'app.bsky.system.actorScene';
export declare type ActorUnknown = string;
export interface OutputSchema { export interface OutputSchema {
cursor?: string; cursor?: string;
feed: FeedItem[]; feed: FeedItem[];
@ -15,8 +17,9 @@ export interface OutputSchema {
export interface FeedItem { export interface FeedItem {
uri: string; uri: string;
cid: string; cid: string;
author: User; author: Actor;
repostedBy?: User; trendedBy?: Actor;
repostedBy?: Actor;
record: {}; record: {};
embed?: RecordEmbed | ExternalEmbed | UnknownEmbed; embed?: RecordEmbed | ExternalEmbed | UnknownEmbed;
replyCount: number; replyCount: number;
@ -30,14 +33,19 @@ export interface FeedItem {
downvote?: string; downvote?: string;
}; };
} }
export interface User { export interface Actor {
did: string; did: string;
declaration: Declaration;
handle: string; handle: string;
displayName?: string; displayName?: string;
} }
export interface Declaration {
cid: string;
actorType: ActorKnown | ActorUnknown;
}
export interface RecordEmbed { export interface RecordEmbed {
type: 'record'; type: 'record';
author: User; author: Actor;
record: {}; record: {};
} }
export interface ExternalEmbed { export interface ExternalEmbed {

View File

@ -7,6 +7,8 @@ export interface CallOptions {
headers?: Headers; headers?: Headers;
} }
export declare type InputSchema = undefined; export declare type InputSchema = undefined;
export declare type ActorKnown = 'app.bsky.system.actorUser' | 'app.bsky.system.actorScene';
export declare type ActorUnknown = string;
export interface OutputSchema { export interface OutputSchema {
thread: Post; thread: Post;
} }
@ -31,9 +33,14 @@ export interface Post {
} }
export interface User { export interface User {
did: string; did: string;
declaration: Declaration;
handle: string; handle: string;
displayName?: string; displayName?: string;
} }
export interface Declaration {
cid: string;
actorType: ActorKnown | ActorUnknown;
}
export interface RecordEmbed { export interface RecordEmbed {
type: 'record'; type: 'record';
author: User; author: User;

View File

@ -9,18 +9,25 @@ export interface CallOptions {
headers?: Headers; headers?: Headers;
} }
export declare type InputSchema = undefined; export declare type InputSchema = undefined;
export declare type ActorKnown = 'app.bsky.system.actorUser' | 'app.bsky.system.actorScene';
export declare type ActorUnknown = string;
export interface OutputSchema { export interface OutputSchema {
uri: string; uri: string;
cid?: string; cid?: string;
cursor?: string; cursor?: string;
repostedBy: { repostedBy: {
did: string; did: string;
declaration: Declaration;
handle: string; handle: string;
displayName?: string; displayName?: string;
createdAt?: string; createdAt?: string;
indexedAt: string; indexedAt: string;
}[]; }[];
} }
export interface Declaration {
cid: string;
actorType: ActorKnown | ActorUnknown;
}
export interface Response { export interface Response {
success: boolean; success: boolean;
headers: Headers; headers: Headers;

View File

@ -8,6 +8,8 @@ export interface CallOptions {
headers?: Headers; headers?: Headers;
} }
export declare type InputSchema = undefined; export declare type InputSchema = undefined;
export declare type ActorKnown = 'app.bsky.system.actorUser' | 'app.bsky.system.actorScene';
export declare type ActorUnknown = string;
export interface OutputSchema { export interface OutputSchema {
cursor?: string; cursor?: string;
feed: FeedItem[]; feed: FeedItem[];
@ -15,8 +17,9 @@ export interface OutputSchema {
export interface FeedItem { export interface FeedItem {
uri: string; uri: string;
cid: string; cid: string;
author: User; author: Actor;
repostedBy?: User; trendedBy?: Actor;
repostedBy?: Actor;
record: {}; record: {};
embed?: RecordEmbed | ExternalEmbed | UnknownEmbed; embed?: RecordEmbed | ExternalEmbed | UnknownEmbed;
replyCount: number; replyCount: number;
@ -30,14 +33,20 @@ export interface FeedItem {
downvote?: string; downvote?: string;
}; };
} }
export interface User { export interface Actor {
did: string; did: string;
declaration: Declaration;
handle: string; handle: string;
actorType?: string;
displayName?: string; displayName?: string;
} }
export interface Declaration {
cid: string;
actorType: ActorKnown | ActorUnknown;
}
export interface RecordEmbed { export interface RecordEmbed {
type: 'record'; type: 'record';
author: User; author: Actor;
record: {}; record: {};
} }
export interface ExternalEmbed { export interface ExternalEmbed {

View File

@ -2,7 +2,7 @@ import { Headers } from '@atproto/xrpc';
export interface QueryParams { export interface QueryParams {
uri: string; uri: string;
cid?: string; cid?: string;
direction?: string; direction?: 'up' | 'down';
limit?: number; limit?: number;
before?: string; before?: string;
} }
@ -10,6 +10,8 @@ export interface CallOptions {
headers?: Headers; headers?: Headers;
} }
export declare type InputSchema = undefined; export declare type InputSchema = undefined;
export declare type ActorKnown = 'app.bsky.system.actorUser' | 'app.bsky.system.actorScene';
export declare type ActorUnknown = string;
export interface OutputSchema { export interface OutputSchema {
uri: string; uri: string;
cid?: string; cid?: string;
@ -23,9 +25,14 @@ export interface OutputSchema {
} }
export interface Actor { export interface Actor {
did: string; did: string;
declaration: Declaration;
handle: string; handle: string;
displayName?: string; displayName?: string;
} }
export interface Declaration {
cid: string;
actorType: ActorKnown | ActorUnknown;
}
export interface Response { export interface Response {
success: boolean; success: boolean;
headers: Headers; headers: Headers;

View File

@ -0,0 +1,26 @@
import { Headers } from '@atproto/xrpc';
export interface QueryParams {
}
export interface CallOptions {
headers?: Headers;
qp?: QueryParams;
encoding: 'application/json';
}
export interface InputSchema {
subject: Subject;
direction: 'up' | 'down' | 'none';
}
export interface Subject {
uri: string;
cid: string;
}
export interface OutputSchema {
upvote?: string;
downvote?: string;
}
export interface Response {
success: boolean;
headers: Headers;
data: OutputSchema;
}
export declare function toKnownErr(e: any): any;

View File

@ -0,0 +1,10 @@
export interface Record {
subject: Subject;
createdAt: string;
[k: string]: unknown;
}
export interface Subject {
uri: string;
cid: string;
[k: string]: unknown;
}

View File

@ -8,21 +8,29 @@ export interface CallOptions {
headers?: Headers; headers?: Headers;
} }
export declare type InputSchema = undefined; export declare type InputSchema = undefined;
export declare type ActorKnown = 'app.bsky.system.actorUser' | 'app.bsky.system.actorScene';
export declare type ActorUnknown = string;
export interface OutputSchema { export interface OutputSchema {
subject: { subject: {
did: string; did: string;
declaration: Declaration;
handle: string; handle: string;
displayName?: string; displayName?: string;
}; };
cursor?: string; cursor?: string;
followers: { followers: {
did: string; did: string;
declaration: Declaration;
handle: string; handle: string;
displayName?: string; displayName?: string;
createdAt?: string; createdAt?: string;
indexedAt: string; indexedAt: string;
}[]; }[];
} }
export interface Declaration {
cid: string;
actorType: ActorKnown | ActorUnknown;
}
export interface Response { export interface Response {
success: boolean; success: boolean;
headers: Headers; headers: Headers;

View File

@ -8,21 +8,29 @@ export interface CallOptions {
headers?: Headers; headers?: Headers;
} }
export declare type InputSchema = undefined; export declare type InputSchema = undefined;
export declare type ActorKnown = 'app.bsky.system.actorUser' | 'app.bsky.system.actorScene';
export declare type ActorUnknown = string;
export interface OutputSchema { export interface OutputSchema {
subject: { subject: {
did: string; did: string;
declaration: Declaration;
handle: string; handle: string;
displayName?: string; displayName?: string;
}; };
cursor?: string; cursor?: string;
follows: { follows: {
did: string; did: string;
declaration: Declaration;
handle: string; handle: string;
displayName?: string; displayName?: string;
createdAt?: string; createdAt?: string;
indexedAt: string; indexedAt: string;
}[]; }[];
} }
export interface Declaration {
cid: string;
actorType: ActorKnown | ActorUnknown;
}
export interface Response { export interface Response {
success: boolean; success: boolean;
headers: Headers; headers: Headers;

View File

@ -8,25 +8,29 @@ export interface CallOptions {
headers?: Headers; headers?: Headers;
} }
export declare type InputSchema = undefined; export declare type InputSchema = undefined;
export declare type ActorKnown = 'app.bsky.system.actorUser' | 'app.bsky.system.actorScene';
export declare type ActorUnknown = string;
export interface OutputSchema { export interface OutputSchema {
subject: { subject: {
did: string; did: string;
declaration: Declaration;
handle: string; handle: string;
displayName?: string; displayName?: string;
}; };
cursor?: string; cursor?: string;
members: { members: {
did: string; did: string;
declaration: Declaration;
handle: string; handle: string;
displayName?: string; displayName?: string;
declaration: {
cid: string;
actorType: string;
};
createdAt?: string; createdAt?: string;
indexedAt: string; indexedAt: string;
}[]; }[];
} }
export interface Declaration {
cid: string;
actorType: ActorKnown | ActorUnknown;
}
export interface Response { export interface Response {
success: boolean; success: boolean;
headers: Headers; headers: Headers;

View File

@ -8,25 +8,29 @@ export interface CallOptions {
headers?: Headers; headers?: Headers;
} }
export declare type InputSchema = undefined; export declare type InputSchema = undefined;
export declare type ActorKnown = 'app.bsky.system.actorUser' | 'app.bsky.system.actorScene';
export declare type ActorUnknown = string;
export interface OutputSchema { export interface OutputSchema {
subject: { subject: {
did: string; did: string;
declaration: Declaration;
handle: string; handle: string;
displayName?: string; displayName?: string;
}; };
cursor?: string; cursor?: string;
memberships: { memberships: {
did: string; did: string;
declaration: Declaration;
handle: string; handle: string;
displayName?: string; displayName?: string;
declaration: {
cid: string;
actorType: string;
};
createdAt?: string; createdAt?: string;
indexedAt: string; indexedAt: string;
}[]; }[];
} }
export interface Declaration {
cid: string;
actorType: ActorKnown | ActorUnknown;
}
export interface Response { export interface Response {
success: boolean; success: boolean;
headers: Headers; headers: Headers;

View File

@ -7,6 +7,8 @@ export interface CallOptions {
headers?: Headers; headers?: Headers;
} }
export declare type InputSchema = undefined; export declare type InputSchema = undefined;
export declare type ActorKnown = 'app.bsky.system.actorUser' | 'app.bsky.system.actorScene';
export declare type ActorUnknown = string;
export interface OutputSchema { export interface OutputSchema {
cursor?: string; cursor?: string;
notifications: Notification[]; notifications: Notification[];
@ -16,6 +18,7 @@ export interface Notification {
cid: string; cid: string;
author: { author: {
did: string; did: string;
declaration: Declaration;
handle: string; handle: string;
displayName?: string; displayName?: string;
}; };
@ -25,6 +28,10 @@ export interface Notification {
isRead: boolean; isRead: boolean;
indexedAt: string; indexedAt: string;
} }
export interface Declaration {
cid: string;
actorType: ActorKnown | ActorUnknown;
}
export interface Response { export interface Response {
success: boolean; success: boolean;
headers: Headers; headers: Headers;

View File

@ -18,7 +18,6 @@ export interface OutputSchema {
refreshJwt: string; refreshJwt: string;
handle: string; handle: string;
did: string; did: string;
declarationCid: string;
} }
export interface Response { export interface Response {
success: boolean; success: boolean;

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,95 @@
import React, {useState} from 'react'
import {
ActivityIndicator,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
import {useStores} from '../../../state'
import {s, colors, gradients} from '../../lib/styles'
import {ErrorMessage} from '../util/ErrorMessage'
export const snapPoints = ['50%']
export function Component({
title,
message,
onPressConfirm,
}: {
title: string
message: string | (() => JSX.Element)
onPressConfirm: () => void | Promise<void>
}) {
const store = useStores()
const [isProcessing, setIsProcessing] = useState<boolean>(false)
const [error, setError] = useState<string>('')
const onPress = async () => {
setError('')
setIsProcessing(true)
try {
await onPressConfirm()
store.shell.closeModal()
return
} catch (e: any) {
setError(e.toString())
setIsProcessing(false)
}
}
return (
<View style={[s.flex1, s.pl10, s.pr10]}>
<Text style={styles.title}>{title}</Text>
{typeof message === 'string' ? (
<Text style={styles.description}>{message}</Text>
) : (
message()
)}
{error ? (
<View style={s.mt10}>
<ErrorMessage message={error} />
</View>
) : undefined}
{isProcessing ? (
<View style={[styles.btn, s.mt10]}>
<ActivityIndicator />
</View>
) : (
<TouchableOpacity style={s.mt10} onPress={onPress}>
<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]}>Confirm</Text>
</LinearGradient>
</TouchableOpacity>
)}
</View>
)
}
const styles = StyleSheet.create({
title: {
textAlign: 'center',
fontWeight: 'bold',
fontSize: 24,
marginBottom: 12,
},
description: {
textAlign: 'center',
fontSize: 17,
paddingHorizontal: 22,
color: colors.gray5,
marginBottom: 10,
},
btn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
borderRadius: 32,
padding: 14,
backgroundColor: colors.gray1,
},
})

View File

@ -53,7 +53,7 @@ export function Component({}: {}) {
{ {
subject: { subject: {
did: createSceneRes.data.did, did: createSceneRes.data.did,
declarationCid: createSceneRes.data.declarationCid, declarationCid: createSceneRes.data.declaration.cid,
}, },
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
}, },

View File

@ -0,0 +1,238 @@
import React, {useState, useEffect, useMemo} from 'react'
import Toast from '../util/Toast'
import {
ActivityIndicator,
FlatList,
StyleSheet,
Text,
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/ErrorMessage'
import {useStores} from '../../../state'
import * as apilib from '../../../state/lib/api'
import {ProfileViewModel} from '../../../state/models/profile-view'
import {SceneInviteSuggestions} from '../../../state/models/scene-invite-suggestions'
import {FollowItem} from '../../../state/models/user-follows-view'
import {s, colors} from '../../lib/styles'
export const snapPoints = ['70%']
export 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 SceneInviteSuggestions(store, {sceneDid: profileView.did}),
[profileView.did],
)
const [createdInvites, setCreatedInvites] = useState<Record<string, string>>(
{},
)
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', {
duration: Toast.durations.LONG,
position: Toast.positions.TOP,
})
} catch (e) {
setError('There was an issue with the invite. Please try again.')
console.error(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) {
setError('There was an issue with the invite. Please try again.')
console.error(e)
}
}
const renderSuggestionItem = ({item}: {item: FollowItem}) => {
const createdInvite = createdInvites[item.did]
return (
<ProfileCard
did={item.did}
handle={item.handle}
displayName={item.displayName}
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 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>
<View style={styles.todoContainer}>
<Text style={styles.todoLabel}>
Pending invites are still being implemented. Check back soon!
</Text>
</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,
},
})

View File

@ -8,9 +8,11 @@ import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop'
import * as models from '../../../state/models/shell-ui' import * as models from '../../../state/models/shell-ui'
import * as LinkActionsModal from './LinkActions' import * as LinkActionsModal from './LinkActions'
import * as ConfirmModal from './Confirm'
import * as SharePostModal from './SharePost.native' import * as SharePostModal from './SharePost.native'
import * as EditProfile from './EditProfile' import * as EditProfileModal from './EditProfile'
import * as CreateScene from './CreateScene' import * as CreateSceneModal from './CreateScene'
import * as InviteToSceneModal from './InviteToScene'
const CLOSED_SNAPPOINTS = ['10%'] const CLOSED_SNAPPOINTS = ['10%']
@ -44,6 +46,13 @@ export const Modal = observer(function Modal() {
{...(store.shell.activeModal as models.LinkActionsModel)} {...(store.shell.activeModal as models.LinkActionsModel)}
/> />
) )
} else if (store.shell.activeModal?.name === 'confirm') {
snapPoints = ConfirmModal.snapPoints
element = (
<ConfirmModal.Component
{...(store.shell.activeModal as models.ConfirmModel)}
/>
)
} else if (store.shell.activeModal?.name === 'share-post') { } else if (store.shell.activeModal?.name === 'share-post') {
snapPoints = SharePostModal.snapPoints snapPoints = SharePostModal.snapPoints
element = ( element = (
@ -52,15 +61,22 @@ export const Modal = observer(function Modal() {
/> />
) )
} else if (store.shell.activeModal?.name === 'edit-profile') { } else if (store.shell.activeModal?.name === 'edit-profile') {
snapPoints = EditProfile.snapPoints snapPoints = EditProfileModal.snapPoints
element = ( element = (
<EditProfile.Component <EditProfileModal.Component
{...(store.shell.activeModal as models.EditProfileModel)} {...(store.shell.activeModal as models.EditProfileModel)}
/> />
) )
} else if (store.shell.activeModal?.name === 'create-scene') { } else if (store.shell.activeModal?.name === 'create-scene') {
snapPoints = CreateScene.snapPoints snapPoints = CreateSceneModal.snapPoints
element = <CreateScene.Component /> element = <CreateSceneModal.Component />
} else if (store.shell.activeModal?.name === 'invite-to-scene') {
snapPoints = InviteToSceneModal.snapPoints
element = (
<InviteToSceneModal.Component
{...(store.shell.activeModal as models.InviteToSceneModel)}
/>
)
} else { } else {
element = <View /> element = <View />
} }

View File

@ -1,6 +1,6 @@
import React, {useMemo} from 'react' import React, {useMemo} from 'react'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {Image, StyleSheet, Text, View} from 'react-native' import {StyleSheet, Text, View} from 'react-native'
import {AtUri} from '../../../third-party/uri' import {AtUri} from '../../../third-party/uri'
import {FontAwesomeIcon, Props} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon, Props} from '@fortawesome/react-native-fontawesome'
import {NotificationsViewItemModel} from '../../../state/models/notifications-view' import {NotificationsViewItemModel} from '../../../state/models/notifications-view'
@ -11,6 +11,7 @@ import {UserAvatar} from '../util/UserAvatar'
import {PostText} from '../post/PostText' import {PostText} from '../post/PostText'
import {Post} from '../post/Post' import {Post} from '../post/Post'
import {Link} from '../util/Link' import {Link} from '../util/Link'
import {InviteAccepter} from './InviteAccepter'
const MAX_AUTHORS = 8 const MAX_AUTHORS = 8
@ -20,10 +21,10 @@ export const FeedItem = observer(function FeedItem({
item: NotificationsViewItemModel item: NotificationsViewItemModel
}) { }) {
const itemHref = useMemo(() => { const itemHref = useMemo(() => {
if (item.isUpvote || item.isRepost) { if (item.isUpvote || item.isRepost || item.isTrend) {
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) { } else if (item.isFollow || item.isAssertion) {
return `/profile/${item.author.handle}` return `/profile/${item.author.handle}`
} else if (item.isReply) { } else if (item.isReply) {
const urip = new AtUri(item.uri) const urip = new AtUri(item.uri)
@ -34,7 +35,7 @@ export const FeedItem = observer(function FeedItem({
const itemTitle = useMemo(() => { const itemTitle = useMemo(() => {
if (item.isUpvote || item.isRepost) { if (item.isUpvote || item.isRepost) {
return 'Post' return 'Post'
} else if (item.isFollow) { } else if (item.isFollow || item.isAssertion) {
return item.author.handle return item.author.handle
} else if (item.isReply) { } else if (item.isReply) {
return 'Post' return 'Post'
@ -66,6 +67,10 @@ 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.blue3]
} else if (item.isReply) { } else if (item.isReply) {
action = 'replied to your post' action = 'replied to your post'
icon = ['far', 'comment'] icon = ['far', 'comment']
@ -73,6 +78,10 @@ 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 <></>
} }
@ -133,6 +142,9 @@ export const FeedItem = observer(function FeedItem({
) : undefined} ) : undefined}
</View> </View>
<View style={styles.meta}> <View style={styles.meta}>
{item.isTrend && (
<Text style={[styles.metaItem, s.f15]}>{action}</Text>
)}
<Link <Link
key={authors[0].href} key={authors[0].href}
style={styles.metaItem} style={styles.metaItem}
@ -150,7 +162,9 @@ export const FeedItem = observer(function FeedItem({
</Text> </Text>
</> </>
) : undefined} ) : undefined}
<Text style={[styles.metaItem, s.f15]}>{action}</Text> {!item.isTrend && (
<Text style={[styles.metaItem, s.f15]}>{action}</Text>
)}
<Text style={[styles.metaItem, s.f15, s.gray5]}> <Text style={[styles.metaItem, s.f15, s.gray5]}>
{ago(item.indexedAt)} {ago(item.indexedAt)}
</Text> </Text>
@ -162,6 +176,11 @@ export const FeedItem = observer(function FeedItem({
)} )}
</View> </View>
</View> </View>
{item.isInvite && (
<View style={styles.addedContainer}>
<InviteAccepter item={item} />
</View>
)}
{item.isReply ? ( {item.isReply ? (
<View style={s.pt5}> <View style={s.pt5}>
<Post uri={item.uri} /> <Post uri={item.uri} />
@ -216,6 +235,7 @@ const styles = StyleSheet.create({
}, },
meta: { meta: {
flexDirection: 'row', flexDirection: 'row',
flexWrap: 'wrap',
paddingTop: 6, paddingTop: 6,
paddingBottom: 2, paddingBottom: 2,
}, },
@ -225,4 +245,9 @@ const styles = StyleSheet.create({
postText: { postText: {
paddingBottom: 5, paddingBottom: 5,
}, },
addedContainer: {
paddingTop: 4,
paddingLeft: 36,
},
}) })

View File

@ -0,0 +1,96 @@
import React, {useState} from 'react'
import {StyleSheet, Text, 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 {ConfirmModel} from '../../../state/models/shell-ui'
import {useStores} from '../../../state'
import {ProfileCard} from '../profile/ProfileCard'
import Toast from '../util/Toast'
import {s, colors, gradients} from '../../lib/styles'
export function InviteAccepter({item}: {item: NotificationsViewItemModel}) {
const store = useStores()
const [confirmationUri, setConfirmationUri] = useState<string>('')
const isMember =
confirmationUri !== '' || store.me.memberships?.isMemberOf(item.author.did)
const onPressAccept = async () => {
store.shell.openModal(
new ConfirmModel(
'Join this scene?',
() => (
<View>
<View style={styles.profileCardContainer}>
<ProfileCard
did={item.author.did}
handle={item.author.handle}
displayName={item.author.displayName}
/>
</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', {
duration: Toast.durations.LONG,
position: Toast.positions.TOP,
})
setConfirmationUri(uri)
}
return (
<View style={styles.container}>
{!isMember ? (
<TouchableOpacity 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 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',
},
})

View File

@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import {StyleSheet, Text, View} from 'react-native' import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'
import {Link} from '../util/Link' import {Link} from '../util/Link'
import {UserAvatar} from '../util/UserAvatar' import {UserAvatar} from '../util/UserAvatar'
import {s, colors} from '../../lib/styles' import {s, colors} from '../../lib/styles'
@ -9,11 +9,15 @@ export function ProfileCard({
handle, handle,
displayName, displayName,
description, description,
renderButton,
onPressButton,
}: { }: {
did: string did: string
handle: string handle: string
displayName?: string displayName?: string
description?: string description?: string
renderButton?: () => JSX.Element
onPressButton?: () => void
}) { }) {
return ( return (
<Link style={styles.outer} href={`/profile/${handle}`} title={handle}> <Link style={styles.outer} href={`/profile/${handle}`} title={handle}>
@ -22,9 +26,20 @@ export function ProfileCard({
<UserAvatar size={40} displayName={displayName} handle={handle} /> <UserAvatar size={40} displayName={displayName} handle={handle} />
</View> </View>
<View style={styles.layoutContent}> <View style={styles.layoutContent}>
<Text style={[s.f16, s.bold]}>{displayName || handle}</Text> <Text style={[s.f16, s.bold]} numberOfLines={1}>
<Text style={[s.f15, s.gray5]}>@{handle}</Text> {displayName || handle}
</Text>
<Text style={[s.f15, s.gray5]} numberOfLines={1}>
@{handle}
</Text>
</View> </View>
{renderButton ? (
<View style={styles.layoutButton}>
<TouchableOpacity onPress={onPressButton} style={styles.btn}>
{renderButton()}
</TouchableOpacity>
</View>
) : undefined}
</View> </View>
</Link> </Link>
) )
@ -34,9 +49,11 @@ const styles = StyleSheet.create({
outer: { outer: {
marginTop: 1, marginTop: 1,
backgroundColor: colors.white, backgroundColor: colors.white,
borderRadius: 6,
}, },
layout: { layout: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center',
}, },
layoutAvi: { layoutAvi: {
width: 60, width: 60,
@ -56,4 +73,17 @@ const styles = StyleSheet.create({
paddingTop: 12, paddingTop: 12,
paddingBottom: 10, paddingBottom: 10,
}, },
layoutButton: {
paddingRight: 10,
},
btn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 7,
paddingHorizontal: 14,
borderRadius: 50,
backgroundColor: colors.gray1,
marginLeft: 6,
},
}) })

View File

@ -1,4 +1,4 @@
import React from 'react' import React, {useMemo} from 'react'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import { import {
ActivityIndicator, ActivityIndicator,
@ -9,12 +9,18 @@ import {
} from 'react-native' } 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 {EditProfileModel} from '../../../state/models/shell-ui' import {
ConfirmModel,
EditProfileModel,
InviteToSceneModel,
} from '../../../state/models/shell-ui'
import {pluralize} from '../../lib/strings' import {pluralize} from '../../lib/strings'
import {s, colors} from '../../lib/styles' import {s, colors} from '../../lib/styles'
import {getGradient} from '../../lib/asset-gen' import {getGradient} from '../../lib/asset-gen'
import {DropdownBtn, DropdownItem} from '../util/DropdownBtn'
import Toast from '../util/Toast' import Toast from '../util/Toast'
import {UserAvatar} from '../util/UserAvatar' import {UserAvatar} from '../util/UserAvatar'
import {UserBanner} from '../util/UserBanner' import {UserBanner} from '../util/UserBanner'
@ -22,10 +28,16 @@ import {UserInfoText} from '../util/UserInfoText'
export const ProfileHeader = observer(function ProfileHeader({ export const ProfileHeader = observer(function ProfileHeader({
view, view,
onRefreshAll,
}: { }: {
view: ProfileViewModel view: ProfileViewModel
onRefreshAll: () => void
}) { }) {
const store = useStores() const store = useStores()
const isMember = useMemo(
() => view.isScene && view.myState.member,
[view.myState.member],
)
const onPressBack = () => { const onPressBack = () => {
store.nav.tab.goBack() store.nav.tab.goBack()
@ -49,9 +61,6 @@ export const ProfileHeader = observer(function ProfileHeader({
const onPressEditProfile = () => { const onPressEditProfile = () => {
store.shell.openModal(new EditProfileModel(view)) store.shell.openModal(new EditProfileModel(view))
} }
const onPressMenu = () => {
// TODO
}
const onPressFollowers = () => { const onPressFollowers = () => {
store.nav.navigate(`/profile/${view.handle}/followers`) store.nav.navigate(`/profile/${view.handle}/followers`)
} }
@ -61,6 +70,31 @@ export const ProfileHeader = observer(function ProfileHeader({
const onPressMembers = () => { const onPressMembers = () => {
store.nav.navigate(`/profile/${view.handle}/members`) store.nav.navigate(`/profile/${view.handle}/members`)
} }
const onPressInviteMembers = () => {
store.shell.openModal(new InviteToSceneModel(view))
}
const onPressLeaveScene = () => {
store.shell.openModal(
new ConfirmModel(
'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`, {
duration: Toast.durations.LONG,
position: Toast.positions.TOP,
})
}
onRefreshAll()
}
// loading // loading
// = // =
@ -86,6 +120,23 @@ 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
if (isCreator || isMember) {
dropdownItems = []
if (isCreator) {
dropdownItems.push({
label: 'Edit Profile',
onPress: () => {}, // TODO
})
}
if (isMember) {
dropdownItems.push({
label: 'Leave Scene...',
onPress: onPressLeaveScene,
})
}
}
return ( return (
<View style={styles.outer}> <View style={styles.outer}>
<UserBanner handle={view.handle} /> <UserBanner handle={view.handle} />
@ -136,11 +187,14 @@ export const ProfileHeader = observer(function ProfileHeader({
)} )}
</> </>
)} )}
<TouchableOpacity {view.isScene &&
onPress={onPressMenu} (view.myState.member || view.creator === store.me.did) ? (
style={[styles.btn, styles.secondaryBtn]}> <DropdownBtn
<FontAwesomeIcon icon="ellipsis" style={[s.gray5]} /> items={dropdownItems}
</TouchableOpacity> style={[styles.btn, styles.secondaryBtn]}>
<FontAwesomeIcon icon="ellipsis" style={[s.gray5]} />
</DropdownBtn>
) : undefined}
</View> </View>
<View style={styles.displayNameLine}> <View style={styles.displayNameLine}>
<Text style={styles.displayName}> <Text style={styles.displayName}>
@ -224,6 +278,24 @@ export const ProfileHeader = observer(function ProfileHeader({
</View> </View>
) : undefined} ) : undefined}
</View> </View>
{view.isScene && view.creator === store.me.did ? (
<View style={styles.sceneAdminContainer}>
<TouchableOpacity 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 style={[s.bold, s.f15, s.white]}>Invite Members</Text>
</LinearGradient>
</TouchableOpacity>
</View>
) : undefined}
</View> </View>
) )
}) })
@ -340,4 +412,15 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
marginBottom: 5, marginBottom: 5,
}, },
sceneAdminContainer: {
borderColor: colors.gray1,
borderTopWidth: 1,
borderBottomWidth: 1,
paddingVertical: 12,
paddingHorizontal: 12,
},
sceneAdminBtn: {
paddingVertical: 8,
},
}) })

View File

@ -16,7 +16,10 @@ export const Link = observer(function Link({
children?: React.ReactNode children?: React.ReactNode
}) { }) {
const store = useStores() const store = useStores()
const onPress = () => store.nav.navigate(href) const onPress = () => {
store.shell.closeModal() // close any active modals
store.nav.navigate(href)
}
const onLongPress = () => { const onLongPress = () => {
store.shell.openModal(new LinkActionsModel(href, title || href)) store.shell.openModal(new LinkActionsModel(href, title || href))
} }

View File

@ -9,6 +9,7 @@ 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'
@ -47,6 +48,7 @@ import {faUser} from '@fortawesome/free-regular-svg-icons/faUser'
import {faUsers} from '@fortawesome/free-solid-svg-icons/faUsers' import {faUsers} from '@fortawesome/free-solid-svg-icons/faUsers'
import {faUserCheck} from '@fortawesome/free-solid-svg-icons/faUserCheck' import {faUserCheck} from '@fortawesome/free-solid-svg-icons/faUserCheck'
import {faUserPlus} from '@fortawesome/free-solid-svg-icons/faUserPlus' import {faUserPlus} from '@fortawesome/free-solid-svg-icons/faUserPlus'
import {faUserXmark} from '@fortawesome/free-solid-svg-icons/faUserXmark'
import {faTicket} from '@fortawesome/free-solid-svg-icons/faTicket' import {faTicket} from '@fortawesome/free-solid-svg-icons/faTicket'
import {faX} from '@fortawesome/free-solid-svg-icons/faX' import {faX} from '@fortawesome/free-solid-svg-icons/faX'
@ -61,6 +63,7 @@ export function setup() {
faArrowUpFromBracket, faArrowUpFromBracket,
faArrowUpRightFromSquare, faArrowUpRightFromSquare,
faArrowsRotate, faArrowsRotate,
faArrowTrendUp,
faAt, faAt,
faBars, faBars,
faBell, faBell,
@ -99,6 +102,7 @@ export function setup() {
faUsers, faUsers,
faUserCheck, faUserCheck,
faUserPlus, faUserPlus,
faUserXmark,
faTicket, faTicket,
faX, faX,
) )

View File

@ -18,6 +18,7 @@ export const Notifications = ({visible}: ScreenParams) => {
if (!visible) { if (!visible) {
return return
} }
store.me.refreshMemberships() // needed for the invite notifications
if (hasSetup) { if (hasSetup) {
console.log('Updating notifications feed') console.log('Updating notifications feed')
notesView?.update() notesView?.update()

View File

@ -73,7 +73,7 @@ export const Profile = observer(({visible, params}: ScreenParams) => {
if (!uiState) { if (!uiState) {
return <View /> return <View />
} }
return <ProfileHeader view={uiState.profile} /> return <ProfileHeader view={uiState.profile} onRefreshAll={onRefresh} />
} }
let renderItem let renderItem
let items: any[] = [] let items: any[] = []

View File

@ -6,24 +6,22 @@ Paul's todo list
- Cursor behaviors on all views - Cursor behaviors on all views
- Update swipe behaviors: edge always goes back, leftmost always goes back, main connects to selector if present - Update swipe behaviors: edge always goes back, leftmost always goes back, main connects to selector if present
- Onboarding flow - Onboarding flow
- * - Confirm email
- Avatars - Setup rpfoile?
- SVG generate
- Create scene - Create scene
- Set profile during creation - Set profile during creation
- Discover scenes view - Discover scenes view
- * - *
- Invite to scene
- User search
- Filter out scenes from suggestions
- Filter out unconfirmed invites from suggestions
- Use pagination to make sure there are suggestions
- Unconfirmed invites
- User profile - User profile
- User
- Invite to scene
- Remove from scene
- Scene - Scene
- Trending - Trending
- Invite to scene
- Remove from scene
- Edit profile - Edit profile
- Notifications
- Scene invite
- Reply gating - Reply gating
- Composer - Composer
- View on post - View on post
@ -37,6 +35,7 @@ Paul's todo list
- Follows list - Follows list
- Members list - Members list
- Bugs - Bugs
- Create account broken
- Follows are broken - Follows are broken
- Auth token refresh seems broken - Auth token refresh seems broken
- Check that sub components arent reloading too much - Check that sub components arent reloading too much