Implement scenes listing in main menu

zio/stable
Paul Frazee 2022-11-08 15:56:22 -06:00
parent 1fbc4cf1f2
commit e6429182a1
12 changed files with 498 additions and 43 deletions

View File

@ -22,6 +22,7 @@
"@zxing/text-encoding": "^0.9.0",
"base64-js": "^1.5.1",
"email-validator": "^2.0.4",
"lodash.chunk": "^4.2.0",
"lodash.omit": "^4.5.0",
"mobx": "^6.6.1",
"mobx-react-lite": "^3.4.0",
@ -50,6 +51,7 @@
"@babel/runtime": "^7.12.5",
"@react-native-community/eslint-config": "^2.0.0",
"@types/jest": "^26.0.23",
"@types/lodash.chunk": "^4.2.7",
"@types/lodash.omit": "^4.5.7",
"@types/react-native": "^0.67.3",
"@types/react-test-renderer": "^17.0.1",

View File

@ -1,5 +1,6 @@
import {makeAutoObservable, runInAction} from 'mobx'
import {RootStoreModel} from './root-store'
import {MembershipsViewModel} from './memberships-view'
export class MeModel {
did?: string
@ -7,6 +8,7 @@ export class MeModel {
displayName?: string
description?: string
notificationCount: number = 0
memberships?: MembershipsViewModel
constructor(public rootStore: RootStoreModel) {
makeAutoObservable(this, {rootStore: false}, {autoBind: true})
@ -18,6 +20,7 @@ export class MeModel {
this.displayName = undefined
this.description = undefined
this.notificationCount = 0
this.memberships = undefined
}
async load() {
@ -37,6 +40,10 @@ export class MeModel {
this.description = ''
}
})
this.memberships = new MembershipsViewModel(this.rootStore, {
actor: this.did,
})
await this.memberships?.setup()
} else {
this.clear()
}
@ -48,4 +55,8 @@ export class MeModel {
this.notificationCount = res.data.count
})
}
async refreshMemberships() {
return this.memberships?.refresh()
}
}

View File

@ -0,0 +1,111 @@
import {makeAutoObservable} from 'mobx'
import * as GetMemberships from '../../third-party/api/src/client/types/app/bsky/graph/getMemberships'
import {RootStoreModel} from './root-store'
type Subject = GetMemberships.OutputSchema['subject']
export type MembershipItem =
GetMemberships.OutputSchema['memberships'][number] & {
_reactKey: string
}
export class MembershipsViewModel {
// state
isLoading = false
isRefreshing = false
hasLoaded = false
error = ''
params: GetMemberships.QueryParams
// data
subject: Subject = {did: '', handle: '', displayName: ''}
memberships: MembershipItem[] = []
constructor(
public rootStore: RootStoreModel,
params: GetMemberships.QueryParams,
) {
makeAutoObservable(
this,
{
rootStore: false,
params: false,
},
{autoBind: true},
)
this.params = params
}
get hasContent() {
return this.subject.did !== ''
}
get hasError() {
return this.error !== ''
}
get isEmpty() {
return this.hasLoaded && !this.hasContent
}
// 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: string = '') {
this.isLoading = false
this.isRefreshing = false
this.hasLoaded = true
this.error = 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(`Failed to load feed: ${e.toString()}`)
}
}
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.memberships.length = 0
let counter = 0
for (const item of res.data.memberships) {
this._append({_reactKey: `item-${counter++}`, ...item})
}
}
private _append(item: MembershipItem) {
this.memberships.push(item)
}
}

View File

@ -6960,6 +6960,8 @@ __export(src_exports, {
AppBskyGraphFollow: () => follow_exports,
AppBskyGraphGetFollowers: () => getFollowers_exports,
AppBskyGraphGetFollows: () => getFollows_exports,
AppBskyGraphGetMembers: () => getMembers_exports,
AppBskyGraphGetMemberships: () => getMemberships_exports,
AppBskyNotificationGetCount: () => getCount_exports,
AppBskyNotificationList: () => list_exports,
AppBskyNotificationUpdateSeen: () => updateSeen_exports,
@ -10494,7 +10496,7 @@ var methodSchemaDict = {
lexicon: 1,
id: "com.atproto.account.requestPasswordReset",
type: "procedure",
description: "Initiate a user account password reset via email",
description: "Initiate a user account password reset via email.",
input: {
encoding: "application/json",
schema: {
@ -10521,7 +10523,7 @@ var methodSchemaDict = {
lexicon: 1,
id: "com.atproto.account.resetPassword",
type: "procedure",
description: "Reset a user account password using a token",
description: "Reset a user account password using a token.",
input: {
encoding: "application/json",
schema: {
@ -10694,7 +10696,7 @@ var methodSchemaDict = {
},
record: {
type: "object",
description: "The record to create"
description: "The record to create."
}
},
$defs: {}
@ -10931,7 +10933,7 @@ var methodSchemaDict = {
},
record: {
type: "object",
description: "The record to create"
description: "The record to create."
}
},
$defs: {}
@ -11100,7 +11102,7 @@ var methodSchemaDict = {
},
from: {
type: "string",
description: "A past commit CID"
description: "A past commit CID."
}
},
output: {
@ -11368,7 +11370,7 @@ var methodSchemaDict = {
lexicon: 1,
id: "app.bsky.actor.search",
type: "query",
description: "Find users matching search criteria",
description: "Find users matching search criteria.",
parameters: {
term: {
type: "string",
@ -11426,7 +11428,7 @@ var methodSchemaDict = {
lexicon: 1,
id: "app.bsky.actor.searchTypeahead",
type: "query",
description: "Find user suggestions for a search term",
description: "Find user suggestions for a search term.",
parameters: {
term: {
type: "string",
@ -11471,7 +11473,7 @@ var methodSchemaDict = {
lexicon: 1,
id: "app.bsky.actor.updateProfile",
type: "procedure",
description: "Notify server that the user has seen notifications",
description: "Notify server that the user has seen notifications.",
input: {
encoding: "application/json",
schema: {
@ -11514,7 +11516,7 @@ var methodSchemaDict = {
lexicon: 1,
id: "app.bsky.feed.getAuthorFeed",
type: "query",
description: "A view of a user's feed",
description: "A view of a user's feed.",
parameters: {
author: {
type: "string",
@ -12217,7 +12219,7 @@ var methodSchemaDict = {
lexicon: 1,
id: "app.bsky.feed.getTimeline",
type: "query",
description: "A view of the user's home timeline",
description: "A view of the user's home timeline.",
parameters: {
algorithm: {
type: "string"
@ -12783,6 +12785,180 @@ var methodSchemaDict = {
}
}
},
"app.bsky.graph.getMembers": {
lexicon: 1,
id: "app.bsky.graph.getMembers",
type: "query",
description: "Who is a member of the group?",
parameters: {
actor: {
type: "string",
required: true
},
limit: {
type: "number",
maximum: 100
},
before: {
type: "string"
}
},
output: {
encoding: "application/json",
schema: {
type: "object",
required: ["subject", "members"],
properties: {
subject: {
type: "object",
required: ["did", "handle"],
properties: {
did: {
type: "string"
},
handle: {
type: "string"
},
displayName: {
type: "string",
maxLength: 64
}
}
},
cursor: {
type: "string"
},
members: {
type: "array",
items: {
type: "object",
required: ["did", "handle", "declaration", "indexedAt"],
properties: {
did: {
type: "string"
},
handle: {
type: "string"
},
displayName: {
type: "string",
maxLength: 64
},
declaration: {
type: "object",
required: ["cid", "actorType"],
properties: {
cid: {
type: "string"
},
actorType: {
type: "string"
}
}
},
createdAt: {
type: "string",
format: "date-time"
},
indexedAt: {
type: "string",
format: "date-time"
}
}
}
}
},
$defs: {}
}
}
},
"app.bsky.graph.getMemberships": {
lexicon: 1,
id: "app.bsky.graph.getMemberships",
type: "query",
description: "Which groups is the actor a member of?",
parameters: {
actor: {
type: "string",
required: true
},
limit: {
type: "number",
maximum: 100
},
before: {
type: "string"
}
},
output: {
encoding: "application/json",
schema: {
type: "object",
required: ["subject", "memberships"],
properties: {
subject: {
type: "object",
required: ["did", "handle"],
properties: {
did: {
type: "string"
},
handle: {
type: "string"
},
displayName: {
type: "string",
maxLength: 64
}
}
},
cursor: {
type: "string"
},
memberships: {
type: "array",
items: {
type: "object",
required: ["did", "handle", "declaration", "indexedAt"],
properties: {
did: {
type: "string"
},
handle: {
type: "string"
},
displayName: {
type: "string",
maxLength: 64
},
declaration: {
type: "object",
required: ["cid", "actorType"],
properties: {
cid: {
type: "string"
},
actorType: {
type: "string"
}
}
},
createdAt: {
type: "string",
format: "date-time"
},
indexedAt: {
type: "string",
format: "date-time"
}
}
}
}
},
$defs: {}
}
}
},
"app.bsky.notification.getCount": {
lexicon: 1,
id: "app.bsky.notification.getCount",
@ -12950,7 +13126,7 @@ var methodSchemaDict = {
lexicon: 1,
id: "app.bsky.notification.updateSeen",
type: "procedure",
description: "Notify server that the user has seen notifications",
description: "Notify server that the user has seen notifications.",
input: {
encoding: "application/json",
schema: {
@ -13374,7 +13550,7 @@ var recordSchemaDict = {
lexicon: 1,
id: "app.bsky.graph.follow",
type: "record",
description: "A social follow",
description: "A social follow.",
key: "tid",
record: {
type: "object",
@ -13899,9 +14075,9 @@ function toKnownErr35(e) {
return e;
}
// src/client/types/app/bsky/notification/getCount.ts
var getCount_exports = {};
__export(getCount_exports, {
// src/client/types/app/bsky/graph/getMembers.ts
var getMembers_exports = {};
__export(getMembers_exports, {
toKnownErr: () => toKnownErr36
});
function toKnownErr36(e) {
@ -13910,9 +14086,9 @@ function toKnownErr36(e) {
return e;
}
// src/client/types/app/bsky/notification/list.ts
var list_exports = {};
__export(list_exports, {
// src/client/types/app/bsky/graph/getMemberships.ts
var getMemberships_exports = {};
__export(getMemberships_exports, {
toKnownErr: () => toKnownErr37
});
function toKnownErr37(e) {
@ -13921,9 +14097,9 @@ function toKnownErr37(e) {
return e;
}
// src/client/types/app/bsky/notification/updateSeen.ts
var updateSeen_exports = {};
__export(updateSeen_exports, {
// src/client/types/app/bsky/notification/getCount.ts
var getCount_exports = {};
__export(getCount_exports, {
toKnownErr: () => toKnownErr38
});
function toKnownErr38(e) {
@ -13932,6 +14108,28 @@ function toKnownErr38(e) {
return e;
}
// src/client/types/app/bsky/notification/list.ts
var list_exports = {};
__export(list_exports, {
toKnownErr: () => toKnownErr39
});
function toKnownErr39(e) {
if (e instanceof XRPCError) {
}
return e;
}
// src/client/types/app/bsky/notification/updateSeen.ts
var updateSeen_exports = {};
__export(updateSeen_exports, {
toKnownErr: () => toKnownErr40
});
function toKnownErr40(e) {
if (e instanceof XRPCError) {
}
return e;
}
// src/client/types/app/bsky/actor/profile.ts
var profile_exports = {};
@ -14435,6 +14633,16 @@ var GraphNS = class {
throw toKnownErr35(e);
});
}
getMembers(params, opts) {
return this._service.xrpc.call("app.bsky.graph.getMembers", params, void 0, opts).catch((e) => {
throw toKnownErr36(e);
});
}
getMemberships(params, opts) {
return this._service.xrpc.call("app.bsky.graph.getMemberships", params, void 0, opts).catch((e) => {
throw toKnownErr37(e);
});
}
};
var AssertionRecord = class {
constructor(service) {
@ -14553,17 +14761,17 @@ var NotificationNS = class {
}
getCount(params, opts) {
return this._service.xrpc.call("app.bsky.notification.getCount", params, void 0, opts).catch((e) => {
throw toKnownErr36(e);
throw toKnownErr38(e);
});
}
list(params, opts) {
return this._service.xrpc.call("app.bsky.notification.list", params, void 0, opts).catch((e) => {
throw toKnownErr37(e);
throw toKnownErr39(e);
});
}
updateSeen(data, opts) {
return this._service.xrpc.call("app.bsky.notification.updateSeen", opts?.qp, data, opts).catch((e) => {
throw toKnownErr38(e);
throw toKnownErr40(e);
});
}
};
@ -14761,6 +14969,8 @@ var SessionManager = class extends import_events.default {
AppBskyGraphFollow,
AppBskyGraphGetFollowers,
AppBskyGraphGetFollows,
AppBskyGraphGetMembers,
AppBskyGraphGetMemberships,
AppBskyNotificationGetCount,
AppBskyNotificationList,
AppBskyNotificationUpdateSeen,

File diff suppressed because one or more lines are too long

View File

@ -42,6 +42,8 @@ import * as AppBskyGraphConfirmation from './types/app/bsky/graph/confirmation';
import * as AppBskyGraphFollow from './types/app/bsky/graph/follow';
import * as AppBskyGraphGetFollowers from './types/app/bsky/graph/getFollowers';
import * as AppBskyGraphGetFollows from './types/app/bsky/graph/getFollows';
import * as AppBskyGraphGetMembers from './types/app/bsky/graph/getMembers';
import * as AppBskyGraphGetMemberships from './types/app/bsky/graph/getMemberships';
import * as AppBskyNotificationGetCount from './types/app/bsky/notification/getCount';
import * as AppBskyNotificationList from './types/app/bsky/notification/list';
import * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/updateSeen';
@ -89,6 +91,8 @@ export * as AppBskyGraphConfirmation from './types/app/bsky/graph/confirmation';
export * as AppBskyGraphFollow from './types/app/bsky/graph/follow';
export * as AppBskyGraphGetFollowers from './types/app/bsky/graph/getFollowers';
export * as AppBskyGraphGetFollows from './types/app/bsky/graph/getFollows';
export * as AppBskyGraphGetMembers from './types/app/bsky/graph/getMembers';
export * as AppBskyGraphGetMemberships from './types/app/bsky/graph/getMemberships';
export * as AppBskyNotificationGetCount from './types/app/bsky/notification/getCount';
export * as AppBskyNotificationList from './types/app/bsky/notification/list';
export * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/updateSeen';
@ -328,6 +332,8 @@ export declare class GraphNS {
constructor(service: ServiceClient);
getFollowers(params?: AppBskyGraphGetFollowers.QueryParams, opts?: AppBskyGraphGetFollowers.CallOptions): Promise<AppBskyGraphGetFollowers.Response>;
getFollows(params?: AppBskyGraphGetFollows.QueryParams, opts?: AppBskyGraphGetFollows.CallOptions): Promise<AppBskyGraphGetFollows.Response>;
getMembers(params?: AppBskyGraphGetMembers.QueryParams, opts?: AppBskyGraphGetMembers.CallOptions): Promise<AppBskyGraphGetMembers.Response>;
getMemberships(params?: AppBskyGraphGetMemberships.QueryParams, opts?: AppBskyGraphGetMemberships.CallOptions): Promise<AppBskyGraphGetMemberships.Response>;
}
export declare class AssertionRecord {
_service: ServiceClient;

View File

@ -0,0 +1,35 @@
import { Headers } from '@atproto/xrpc';
export interface QueryParams {
actor: string;
limit?: number;
before?: string;
}
export interface CallOptions {
headers?: Headers;
}
export declare type InputSchema = undefined;
export interface OutputSchema {
subject: {
did: string;
handle: string;
displayName?: string;
};
cursor?: string;
members: {
did: string;
handle: string;
displayName?: string;
declaration: {
cid: string;
actorType: string;
};
createdAt?: string;
indexedAt: string;
}[];
}
export interface Response {
success: boolean;
headers: Headers;
data: OutputSchema;
}
export declare function toKnownErr(e: any): any;

View File

@ -0,0 +1,35 @@
import { Headers } from '@atproto/xrpc';
export interface QueryParams {
actor: string;
limit?: number;
before?: string;
}
export interface CallOptions {
headers?: Headers;
}
export declare type InputSchema = undefined;
export interface OutputSchema {
subject: {
did: string;
handle: string;
displayName?: string;
};
cursor?: string;
memberships: {
did: string;
handle: string;
displayName?: string;
declaration: {
cid: string;
actorType: string;
};
createdAt?: string;
indexedAt: string;
}[];
}
export interface Response {
success: boolean;
headers: Headers;
data: OutputSchema;
}
export declare function toKnownErr(e: any): any;

File diff suppressed because one or more lines are too long

View File

@ -16,6 +16,7 @@ import Animated, {
} from 'react-native-reanimated'
import {IconProp} from '@fortawesome/fontawesome-svg-core'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import _chunk from 'lodash.chunk'
import {HomeIcon, UserGroupIcon, BellIcon} from '../../lib/icons'
import {UserAvatar} from '../../com/util/UserAvatar'
import {useStores} from '../../../state'
@ -26,6 +27,12 @@ export const MainMenu = observer(
const store = useStores()
const initInterp = useSharedValue<number>(0)
useEffect(() => {
if (active) {
// trigger a refresh in case memberships have changed recently
store.me.refreshMemberships()
}
}, [active])
useEffect(() => {
if (active) {
initInterp.value = withTiming(1, {duration: 150})
@ -124,6 +131,29 @@ export const MainMenu = observer(
return <View />
}
const MenuItems = ({
children,
}: {
children: (JSX.Element | JSX.Element[])[]
}) => {
const groups = _chunk(children.flat(), 4)
const lastGroup = groups.at(-1)
while (lastGroup && lastGroup.length < 4) {
lastGroup.push(<MenuItemBlank />)
}
return (
<>
{groups.map((group, i) => (
<View key={i} style={[styles.menuItems]}>
{group.map((el, j) => (
<React.Fragment key={j}>{el}</React.Fragment>
))}
</View>
))}
</>
)
}
return (
<>
<TouchableWithoutFeedback onPress={onClose}>
@ -163,7 +193,7 @@ export const MainMenu = observer(
styles.menuItemsAnimContainer,
menuItemsAnimStyle,
]}>
<View style={[styles.menuItems]}>
<MenuItems>
<MenuItem icon="home" label="Home" url="/" />
<MenuItem
icon="bell"
@ -171,27 +201,28 @@ export const MainMenu = observer(
url="/notifications"
count={store.me.notificationCount}
/>
<MenuItemBlank />
<MenuItemBlank />
</View>
</MenuItems>
<Text style={styles.heading}>Scenes</Text>
<View style={[styles.menuItems]}>
<MenuItems>
<MenuItem icon={['far', 'compass']} label="Discover" url="/" />
<MenuItem
icon={'user-group'}
label="Create Scene"
url="/contacts"
/>
<MenuItemActor label="Galaxy Brain" url="/" />
<MenuItemActor label="Paul's Friends" url="/" />
</View>
<View style={[styles.menuItems]}>
<MenuItemActor label="Cool People Only" url="/" />
<MenuItemActor label="Techsky" url="/" />
<MenuItemBlank />
<MenuItemBlank />
</View>
{store.me.memberships ? (
store.me.memberships.memberships.map((membership, i) => (
<MenuItemActor
key={i}
label={membership.displayName || membership.handle}
url={`/profile/${membership.handle}`}
/>
))
) : (
<MenuItemBlank />
)}
</MenuItems>
</Animated.View>
</SafeAreaView>
</Animated.View>

View File

@ -42,5 +42,7 @@ Paul's todo list
- Followers list
- Follows list
- Bugs
- Follows are broken
- Auth token refresh seems broken
- Check that sub components arent reloading too much
- Titles are getting screwed up (possibly swipe related)

View File

@ -2502,6 +2502,13 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
"@types/lodash.chunk@^4.2.7":
version "4.2.7"
resolved "https://registry.yarnpkg.com/@types/lodash.chunk/-/lodash.chunk-4.2.7.tgz#df52478072f0673d9fe8d3dd43af8f490d07a56a"
integrity sha512-//tmaWHiANgToom/YYYKKqiCtlNz11fwYtMUUbaemNSbWTI+2zHtYW5nt1PHNCRWHPAJHHhn4UVFD9LKUFvatA==
dependencies:
"@types/lodash" "*"
"@types/lodash.omit@^4.5.7":
version "4.5.7"
resolved "https://registry.yarnpkg.com/@types/lodash.omit/-/lodash.omit-4.5.7.tgz#2357ed2412b4164344e8ee41f85bb0b2920304ba"
@ -8098,6 +8105,11 @@ locate-path@^6.0.0:
dependencies:
p-locate "^5.0.0"
lodash.chunk@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.chunk/-/lodash.chunk-4.2.0.tgz#66e5ce1f76ed27b4303d8c6512e8d1216e8106bc"
integrity sha512-ZzydJKfUHJwHa+hF5X66zLFCBrWn5GeF28OHEr4WVWtNDXlQ/IjWKPBiikqKo2ne0+v6JgCgJ0GzJp8k8bHC7w==
lodash.debounce@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"