Compare commits
131 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a8304131b | |||
| c6f2465ba4 | |||
| d636ec6339 | |||
| 98c9a6ad32 | |||
| 33a4d2cbe5 | |||
| 8a5bc60819 | |||
| f492fd4d41 | |||
| ed7204ab71 | |||
| 9aeaa38f3b | |||
| b3f9ed7981 | |||
| b8ab064fcc | |||
| 58bf9cb8cd | |||
| 8a67e8d102 | |||
| 5d6fc56349 | |||
| 77ed2129d3 | |||
| fcebe8e0b0 | |||
| 6df617003a | |||
| 878d577cab | |||
| c11d9ef64b | |||
| beb74aee37 | |||
| 6d58dc9db3 | |||
| c7758c33d1 | |||
| 840373fcf4 | |||
| ff7f73cd8c | |||
| fd6ebf3dfe | |||
| 6953bf9d22 | |||
| 9396595595 | |||
| cf13b5cd4c | |||
| e24ab48b73 | |||
| bbee08eb22 | |||
| c68f291128 | |||
| 32b3575074 | |||
| 11f972c640 | |||
| 61cf754fd5 | |||
| 00b50c71b5 | |||
| 66eb6f2888 | |||
| bc571ee998 | |||
| c758996034 | |||
| 1f26494c94 | |||
| 74bfc10e36 | |||
| e194e5de91 | |||
| 09f96da982 | |||
| 6e48d63dda | |||
| eec636897c | |||
| 777f30562b | |||
| 16e4bc66bd | |||
| 98c5312a29 | |||
| 9971f1f035 | |||
| dec9d97171 | |||
| 355749e605 | |||
| 6268736bfa | |||
| a270d750c8 | |||
| 0eb4c7495e | |||
| b438318b92 | |||
| 01aaa6123f | |||
| 486a1f082c | |||
| f943cbd04a | |||
| c54fa5b2e4 | |||
| 54c0115181 | |||
| bb3a2fddfc | |||
| 5135a13812 | |||
| 059c3b362d | |||
| 090ceb4c53 | |||
| 7796203c25 | |||
| 44ae514d89 | |||
| 12d8e06a1e | |||
| de65d9198a | |||
| 41bf8e15cd | |||
| adeb1d5cec | |||
| 61511d5161 | |||
| aee205b0ab | |||
| e59763b50a | |||
| 5168d44926 | |||
| def3156c33 | |||
| a290891f70 | |||
| a96562e89c | |||
| 279d41fcd1 | |||
| 4932505c5e | |||
| 629b7d8c9b | |||
| 5c71f8e32a | |||
| 395a8838be | |||
| 17903b817d | |||
| 54f05bfcf6 | |||
| b1a058d098 | |||
| 27657541f2 | |||
| 73ea67f5c0 | |||
| d065b62f52 | |||
| eb03170736 | |||
| 6f734f7978 | |||
| 9235d6c13b | |||
| bc54c8d7c6 | |||
| 3b8dbbff12 | |||
| e8bb545023 | |||
| f0b30e26d4 | |||
| cbfa9f62a4 | |||
| fefd8cc8a0 | |||
| 09765a5d36 | |||
| e325b72c62 | |||
| 055a4d8e98 | |||
| 1e28ae6d2a | |||
| 16343a2936 | |||
| 18cc2183cc | |||
| 5e711c799b | |||
| ca4b9d3a60 | |||
| ebd03ffe2a | |||
| ee4f0673f1 | |||
| 33145f2e71 | |||
| 721fd84a7e | |||
| a06fd7dfd4 | |||
| bd6bb4df06 | |||
| bb4dc3c3a5 | |||
| 7b7fe7ccd8 | |||
| 316679ae68 | |||
| b34cae6db4 | |||
| 52e31394ab | |||
| 84f18e03fa | |||
| 30761594ec | |||
| 87258a2439 | |||
| 6e31e928a9 | |||
| d0587c81f8 | |||
| 29018f06b9 | |||
| 171cb6b3d3 | |||
| 3e37c27310 | |||
| 06c3ebe2d4 | |||
| d336cd58b1 | |||
| 684f67a8e0 | |||
| a7b23613d3 | |||
| a6db1a9ee3 | |||
| 0f065b2c27 | |||
| 7399e37bba | |||
| 7ce4c821e5 |
@@ -59,3 +59,6 @@ buck-out/
|
||||
# Ruby / CocoaPods
|
||||
/ios/Pods/
|
||||
/vendor/bundle/
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
@@ -16,8 +16,10 @@ Uses:
|
||||
- After initial setup:
|
||||
- `cd ios ; pod install`
|
||||
- Start the dev servers
|
||||
- `yarn dev-pds`
|
||||
- `yarn dev-wallet`
|
||||
- `git clone git@github.com:bluesky-social/atproto.git`
|
||||
- `cd atproto`
|
||||
- `yarn`
|
||||
- `cd packages/dev-env && yarn start`
|
||||
- Run the dev app
|
||||
- iOS: `yarn ios`
|
||||
- Android: `yarn android`
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export const CameraRoll = {
|
||||
getPhotos: jest.fn().mockResolvedValue({
|
||||
edges: [
|
||||
{node: {image: {uri: 'path/to/image1.jpg'}}},
|
||||
{node: {image: {uri: 'path/to/image2.jpg'}}},
|
||||
{node: {image: {uri: 'path/to/image3.jpg'}}},
|
||||
],
|
||||
}),
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export default from '@react-native-async-storage/async-storage/jest/async-storage-mock'
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
export const openPicker = jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve({uri: ''}))
|
||||
export const openCamera = jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve({uri: ''}))
|
||||
export const openCropper = jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve({uri: ''}))
|
||||
@@ -0,0 +1,10 @@
|
||||
jest.mock('rn-fetch-blob', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
default: {
|
||||
fs: {
|
||||
unlink: jest.fn(),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,934 @@
|
||||
import {LogModel} from './../src/state/models/log'
|
||||
import {LRUMap} from 'lru_map'
|
||||
import {RootStoreModel} from './../src/state/models/root-store'
|
||||
import {NavigationTabModel} from './../src/state/models/navigation'
|
||||
import {SessionModel} from '../src/state/models/session'
|
||||
import {NavigationModel} from '../src/state/models/navigation'
|
||||
import {ShellUiModel} from '../src/state/models/shell-ui'
|
||||
import {MeModel} from '../src/state/models/me'
|
||||
import {OnboardModel} from '../src/state/models/onboard'
|
||||
import {ProfilesViewModel} from '../src/state/models/profiles-view'
|
||||
import {LinkMetasViewModel} from '../src/state/models/link-metas-view'
|
||||
import {MembershipsViewModel} from '../src/state/models/memberships-view'
|
||||
import {FeedModel} from '../src/state/models/feed-view'
|
||||
import {NotificationsViewModel} from '../src/state/models/notifications-view'
|
||||
import {ProfileViewModel} from '../src/state/models/profile-view'
|
||||
import {MembersViewModel} from '../src/state/models/members-view'
|
||||
import {ProfileUiModel, Sections} from '../src/state/models/profile-ui'
|
||||
import {SessionServiceClient} from '@atproto/api'
|
||||
import {UserAutocompleteViewModel} from '../src/state/models/user-autocomplete-view'
|
||||
import {UserLocalPhotosModel} from '../src/state/models/user-local-photos'
|
||||
import {SuggestedActorsViewModel} from '../src/state/models/suggested-actors-view'
|
||||
import {UserFollowersViewModel} from '../src/state/models/user-followers-view'
|
||||
import {UserFollowsViewModel} from '../src/state/models/user-follows-view'
|
||||
import {NotificationsViewItemModel} from './../src/state/models/notifications-view'
|
||||
import {
|
||||
PostThreadViewModel,
|
||||
PostThreadViewPostModel,
|
||||
} from '../src/state/models/post-thread-view'
|
||||
import {FeedItemModel} from '../src/state/models/feed-view'
|
||||
import {RepostedByViewModel} from '../src/state/models/reposted-by-view'
|
||||
import {VotesViewModel} from '../src/state/models/votes-view'
|
||||
|
||||
export const mockedProfileStore = {
|
||||
isLoading: false,
|
||||
isRefreshing: false,
|
||||
hasLoaded: true,
|
||||
error: '',
|
||||
params: {
|
||||
actor: '',
|
||||
},
|
||||
did: 'test did',
|
||||
handle: 'testhandle',
|
||||
declaration: {
|
||||
cid: '',
|
||||
actorType: '',
|
||||
},
|
||||
creator: 'test did',
|
||||
displayName: '',
|
||||
description: '',
|
||||
avatar: '',
|
||||
banner: '',
|
||||
followersCount: 0,
|
||||
followsCount: 0,
|
||||
membersCount: 0,
|
||||
postsCount: 0,
|
||||
myState: {
|
||||
follow: '',
|
||||
member: '',
|
||||
},
|
||||
rootStore: {} as RootStoreModel,
|
||||
hasContent: true,
|
||||
hasError: false,
|
||||
isEmpty: false,
|
||||
isUser: true,
|
||||
isScene: false,
|
||||
setup: jest.fn().mockResolvedValue({aborted: false}),
|
||||
refresh: jest.fn(),
|
||||
toggleFollowing: jest.fn().mockResolvedValue({}),
|
||||
updateProfile: jest.fn(),
|
||||
// unknown required because of the missing private methods: _xLoading, _xIdle, _load, _replaceAll
|
||||
} as unknown as ProfileViewModel
|
||||
|
||||
export const mockedMembersStore = {
|
||||
isLoading: false,
|
||||
isRefreshing: false,
|
||||
hasLoaded: true,
|
||||
error: '',
|
||||
params: {
|
||||
actor: 'test actor',
|
||||
},
|
||||
subject: {
|
||||
did: 'test did',
|
||||
handle: '',
|
||||
displayName: '',
|
||||
declaration: {
|
||||
cid: '',
|
||||
actorType: '',
|
||||
},
|
||||
avatar: undefined,
|
||||
},
|
||||
members: [
|
||||
{
|
||||
did: 'test did2',
|
||||
declaration: {
|
||||
cid: '',
|
||||
actorType: '',
|
||||
},
|
||||
handle: 'testhandle',
|
||||
displayName: 'test name',
|
||||
indexedAt: '',
|
||||
},
|
||||
],
|
||||
rootStore: {} as RootStoreModel,
|
||||
hasContent: true,
|
||||
hasError: false,
|
||||
isEmpty: false,
|
||||
isMember: jest.fn(),
|
||||
setup: jest.fn().mockResolvedValue({aborted: false}),
|
||||
refresh: jest.fn(),
|
||||
loadMore: jest.fn(),
|
||||
removeMember: jest.fn(),
|
||||
// unknown required because of the missing private methods: _xLoading, _xIdle, _fetch, _replaceAll, _append
|
||||
} as unknown as MembersViewModel
|
||||
|
||||
export const mockedMembershipsStore = {
|
||||
isLoading: false,
|
||||
isRefreshing: false,
|
||||
hasLoaded: true,
|
||||
error: '',
|
||||
params: {
|
||||
actor: '',
|
||||
limit: 1,
|
||||
before: '',
|
||||
},
|
||||
subject: {
|
||||
did: 'test did',
|
||||
handle: '',
|
||||
displayName: '',
|
||||
declaration: {cid: '', actorType: ''},
|
||||
avatar: undefined,
|
||||
},
|
||||
memberships: [
|
||||
{
|
||||
did: 'test did',
|
||||
declaration: {
|
||||
cid: '',
|
||||
actorType: 'app.bsky.system.actorUser',
|
||||
},
|
||||
handle: ',',
|
||||
displayName: '',
|
||||
createdAt: '',
|
||||
indexedAt: '',
|
||||
_reactKey: 'item-1',
|
||||
},
|
||||
],
|
||||
rootStore: {} as RootStoreModel,
|
||||
hasContent: true,
|
||||
hasError: false,
|
||||
isEmpty: false,
|
||||
isMemberOf: jest.fn(),
|
||||
setup: jest.fn().mockResolvedValue({aborted: false}),
|
||||
refresh: jest.fn(),
|
||||
loadMore: jest.fn(),
|
||||
// unknown required because of the missing private methods: _xLoading, _xIdle, _fetch, _replaceAll, _append
|
||||
} as unknown as MembershipsViewModel
|
||||
|
||||
export const mockedFeedItemStore = {
|
||||
_reactKey: 'item-1',
|
||||
_isThreadParent: false,
|
||||
_isThreadChildElided: false,
|
||||
_isThreadChild: false,
|
||||
post: {
|
||||
uri: 'testuri',
|
||||
cid: 'test cid',
|
||||
author: {
|
||||
did: 'test did',
|
||||
handle: 'test.handle',
|
||||
displayName: 'test name',
|
||||
declaration: {cid: '', actorType: ''},
|
||||
},
|
||||
record: {
|
||||
$type: 'app.bsky.feed.post',
|
||||
createdAt: '2022-12-29T16:39:57.919Z',
|
||||
text: 'Sup',
|
||||
},
|
||||
replyCount: 0,
|
||||
repostCount: 0,
|
||||
upvoteCount: 0,
|
||||
downvoteCount: 0,
|
||||
indexedAt: '2022-12-29T16:39:57.919Z',
|
||||
viewer: {},
|
||||
},
|
||||
postRecord: {
|
||||
$type: 'app.bsky.feed.post',
|
||||
text: 'test text',
|
||||
createdAt: '1',
|
||||
reply: {
|
||||
root: {
|
||||
uri: 'testuri',
|
||||
cid: 'tes cid',
|
||||
},
|
||||
parent: {
|
||||
uri: 'testuri',
|
||||
cid: 'tes cid',
|
||||
},
|
||||
},
|
||||
},
|
||||
rootStore: {} as RootStoreModel,
|
||||
copy: jest.fn(),
|
||||
toggleUpvote: jest.fn().mockResolvedValue({}),
|
||||
toggleDownvote: jest.fn(),
|
||||
toggleRepost: jest.fn().mockResolvedValue({}),
|
||||
delete: jest.fn().mockResolvedValue({}),
|
||||
reasonRepost: {
|
||||
by: {
|
||||
did: 'test did',
|
||||
handle: 'test.handle',
|
||||
declaration: {cid: '', actorType: ''},
|
||||
},
|
||||
indexedAt: '',
|
||||
},
|
||||
reasonTrend: {
|
||||
by: {
|
||||
did: 'test did',
|
||||
handle: 'test.handle',
|
||||
declaration: {cid: '', actorType: ''},
|
||||
},
|
||||
indexedAt: '',
|
||||
},
|
||||
reply: {
|
||||
parent: {
|
||||
author: {
|
||||
did: 'test did',
|
||||
handle: 'test.handle',
|
||||
displayName: 'test name',
|
||||
declaration: {cid: '', actorType: ''},
|
||||
},
|
||||
cid: '',
|
||||
downvoteCount: 0,
|
||||
indexedAt: '2023-01-10T11:17:46.945Z',
|
||||
record: {},
|
||||
replyCount: 1,
|
||||
repostCount: 0,
|
||||
upvoteCount: 0,
|
||||
uri: 'testuri',
|
||||
viewer: {},
|
||||
},
|
||||
root: {
|
||||
author: {
|
||||
did: 'test did',
|
||||
handle: 'test.handle',
|
||||
displayName: 'test name',
|
||||
declaration: {cid: '', actorType: ''},
|
||||
},
|
||||
cid: '',
|
||||
downvoteCount: 0,
|
||||
indexedAt: '2023-01-10T11:17:46.739Z',
|
||||
record: {},
|
||||
replyCount: 1,
|
||||
repostCount: 0,
|
||||
upvoteCount: 1,
|
||||
uri: 'testuri',
|
||||
viewer: {},
|
||||
},
|
||||
},
|
||||
} as FeedItemModel
|
||||
|
||||
export const mockedFeedStore = {
|
||||
isLoading: false,
|
||||
isRefreshing: false,
|
||||
hasNewLatest: false,
|
||||
hasLoaded: true,
|
||||
error: '',
|
||||
hasMore: true,
|
||||
params: {
|
||||
actor: '',
|
||||
limit: 1,
|
||||
before: '',
|
||||
},
|
||||
feed: [],
|
||||
rootStore: {} as RootStoreModel,
|
||||
feedType: 'home',
|
||||
hasContent: true,
|
||||
hasError: false,
|
||||
isEmpty: false,
|
||||
nonReplyFeed: [
|
||||
{
|
||||
_reactKey: 'item-1',
|
||||
post: {
|
||||
author: {
|
||||
handle: 'handle.test',
|
||||
displayName: 'test name',
|
||||
avatar: '',
|
||||
},
|
||||
cid: 'bafyreihkwjoy2vbfqld2lp3tv4ce6yfr354sqgp32qoplrudso4gyyjiwe',
|
||||
downvoteCount: 0,
|
||||
indexedAt: '2022-12-29T16:35:55.270Z',
|
||||
record: {
|
||||
$type: 'app.bsky.feed.post',
|
||||
createdAt: '2022-12-29T16:39:57.919Z',
|
||||
text: 'Sup',
|
||||
},
|
||||
replyCount: 0,
|
||||
repostCount: 0,
|
||||
upvoteCount: 0,
|
||||
uri: 'at://did:plc:wcizmlgv3rdslk64t6q4silu/app.bsky.feed.post/3jkzce5kfvn2h',
|
||||
viewer: {
|
||||
handle: 'handle.test',
|
||||
displayName: 'test name',
|
||||
avatar: '',
|
||||
},
|
||||
},
|
||||
reason: undefined,
|
||||
reply: undefined,
|
||||
},
|
||||
],
|
||||
setHasNewLatest: jest.fn(),
|
||||
setup: jest.fn().mockResolvedValue({}),
|
||||
refresh: jest.fn().mockResolvedValue({}),
|
||||
loadMore: jest.fn().mockResolvedValue({}),
|
||||
loadLatest: jest.fn(),
|
||||
update: jest.fn(),
|
||||
checkForLatest: jest.fn().mockRejectedValue('Error checking for latest'),
|
||||
// unknown required because of the missing private methods: _xLoading, _xIdle, _pendingWork, _initialLoad, _loadLatest, _loadMore, _update, _replaceAll, _appendAll, _prependAll, _updateAll, _getFeed, loadMoreCursor, pollCursor, _loadPromise, _updatePromise, _loadLatestPromise, _loadMorePromise
|
||||
} as unknown as FeedModel
|
||||
|
||||
export const mockedPostThreadViewPostStore = {
|
||||
_reactKey: 'item-1',
|
||||
_depth: 0,
|
||||
_isHighlightedPost: false,
|
||||
_hasMore: false,
|
||||
postRecord: {
|
||||
text: 'test text',
|
||||
createdAt: '',
|
||||
reply: {
|
||||
root: {
|
||||
uri: 'testuri',
|
||||
cid: 'tes cid',
|
||||
},
|
||||
parent: {
|
||||
uri: 'testuri',
|
||||
cid: 'tes cid',
|
||||
},
|
||||
},
|
||||
},
|
||||
post: {
|
||||
uri: 'testuri',
|
||||
cid: 'testcid',
|
||||
record: {},
|
||||
author: {
|
||||
did: 'test did',
|
||||
handle: 'test.handle',
|
||||
declaration: {cid: '', actorType: ''},
|
||||
viewer: {
|
||||
muted: true,
|
||||
},
|
||||
},
|
||||
replyCount: 0,
|
||||
repostCount: 0,
|
||||
upvoteCount: 0,
|
||||
downvoteCount: 0,
|
||||
indexedAt: '',
|
||||
viewer: {
|
||||
repost: '',
|
||||
upvote: '',
|
||||
downvote: '',
|
||||
},
|
||||
},
|
||||
rootStore: {} as RootStoreModel,
|
||||
assignTreeModels: jest.fn(),
|
||||
toggleRepost: jest.fn().mockRejectedValue({}),
|
||||
toggleUpvote: jest.fn().mockRejectedValue({}),
|
||||
toggleDownvote: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
} as PostThreadViewPostModel
|
||||
|
||||
export const mockedPostThreadViewStore = {
|
||||
isLoading: false,
|
||||
isRefreshing: false,
|
||||
hasLoaded: false,
|
||||
error: '',
|
||||
notFound: false,
|
||||
resolvedUri: 'testuri',
|
||||
params: {
|
||||
uri: 'testuri',
|
||||
},
|
||||
thread: mockedPostThreadViewPostStore,
|
||||
hasContent: true,
|
||||
hasError: false,
|
||||
setup: jest.fn(),
|
||||
refresh: jest.fn().mockResolvedValue({}),
|
||||
update: jest.fn(),
|
||||
// unknown required because of the missing private methods: _xLoading, _xIdle, _resolveUri, _load, _replaceAll
|
||||
} as unknown as PostThreadViewModel
|
||||
|
||||
export const mockedNotificationsViewItemStore = {
|
||||
_reactKey: 'item-1',
|
||||
uri: 'testuri',
|
||||
cid: '',
|
||||
author: {
|
||||
did: 'test did',
|
||||
handle: 'test.handle',
|
||||
declaration: {cid: '', actorType: ''},
|
||||
},
|
||||
rootStore: {} as RootStoreModel,
|
||||
copy: jest.fn(),
|
||||
reason: 'test reason',
|
||||
isRead: true,
|
||||
indexedAt: '',
|
||||
isUpvote: true,
|
||||
isRepost: false,
|
||||
isTrend: false,
|
||||
isMention: false,
|
||||
isReply: false,
|
||||
isFollow: false,
|
||||
isAssertion: false,
|
||||
needsAdditionalData: false,
|
||||
isInvite: false,
|
||||
subjectUri: 'testuri',
|
||||
toSupportedRecord: jest.fn().mockReturnValue({
|
||||
text: 'test text',
|
||||
createdAt: '',
|
||||
}),
|
||||
fetchAdditionalData: jest.fn(),
|
||||
} as NotificationsViewItemModel
|
||||
|
||||
export const mockedNotificationsStore = {
|
||||
isLoading: false,
|
||||
isRefreshing: false,
|
||||
hasLoaded: true,
|
||||
error: '',
|
||||
params: {
|
||||
limit: 1,
|
||||
before: '',
|
||||
},
|
||||
hasMore: true,
|
||||
notifications: [mockedNotificationsViewItemStore],
|
||||
rootStore: {} as RootStoreModel,
|
||||
hasContent: true,
|
||||
hasError: false,
|
||||
isEmpty: false,
|
||||
setup: jest.fn().mockResolvedValue({aborted: false}),
|
||||
refresh: jest.fn().mockResolvedValue({}),
|
||||
loadMore: jest.fn().mockResolvedValue({}),
|
||||
update: jest.fn().mockResolvedValue(null),
|
||||
updateReadState: jest.fn(),
|
||||
// unknown required because of the missing private methods: _xLoading, _xIdle, _pendingWork, _initialLoad, _loadMore, _update, _replaceAll, _appendAll, _updateAll, loadMoreCursor, _loadPromise, _updatePromise, _loadLatestPromise, _loadMorePromise
|
||||
} as unknown as NotificationsViewModel
|
||||
|
||||
export const mockedSessionStore = {
|
||||
serialize: jest.fn(),
|
||||
hydrate: jest.fn(),
|
||||
data: {
|
||||
service: '',
|
||||
refreshJwt: '',
|
||||
accessJwt: '',
|
||||
handle: '',
|
||||
did: 'test did',
|
||||
},
|
||||
online: false,
|
||||
attemptingConnect: false,
|
||||
rootStore: {} as RootStoreModel,
|
||||
hasSession: true,
|
||||
clear: jest.fn(),
|
||||
setState: jest.fn(),
|
||||
setOnline: jest.fn(),
|
||||
updateAuthTokens: jest.fn(),
|
||||
connect: jest.fn(),
|
||||
describeService: jest.fn().mockResolvedValue({
|
||||
availableUserDomains: ['test'],
|
||||
links: {
|
||||
termsOfService: 'https://testTermsOfService',
|
||||
privacyPolicy: 'https://testPrivacyPolicy',
|
||||
},
|
||||
}),
|
||||
login: jest.fn(),
|
||||
createAccount: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
|
||||
// unknown required because of the missing private methods: _connectPromise, configureApi & _connect
|
||||
} as unknown as SessionModel
|
||||
|
||||
export const mockedNavigationTabStore = {
|
||||
serialize: jest.fn(),
|
||||
hydrate: jest.fn(),
|
||||
id: 0,
|
||||
history: [
|
||||
{
|
||||
url: '',
|
||||
ts: 0,
|
||||
title: '',
|
||||
id: 0,
|
||||
},
|
||||
],
|
||||
index: 0,
|
||||
isNewTab: false,
|
||||
current: {
|
||||
url: '',
|
||||
ts: 0,
|
||||
title: '',
|
||||
id: 0,
|
||||
},
|
||||
canGoBack: false,
|
||||
canGoForward: false,
|
||||
backTen: [
|
||||
{
|
||||
url: '',
|
||||
title: '',
|
||||
index: 0,
|
||||
id: 0,
|
||||
},
|
||||
],
|
||||
forwardTen: [
|
||||
{
|
||||
url: '',
|
||||
title: '',
|
||||
index: 0,
|
||||
id: 0,
|
||||
},
|
||||
],
|
||||
navigate: jest.fn(),
|
||||
refresh: jest.fn(),
|
||||
goBack: jest.fn(),
|
||||
fixedTabReset: jest.fn(),
|
||||
goForward: jest.fn(),
|
||||
goToIndex: jest.fn(),
|
||||
setTitle: jest.fn(),
|
||||
setIsNewTab: jest.fn(),
|
||||
fixedTabPurpose: 0,
|
||||
getBackList: () => [
|
||||
{
|
||||
url: '/',
|
||||
title: '',
|
||||
index: 1,
|
||||
id: 1,
|
||||
},
|
||||
],
|
||||
getForwardList: jest.fn(),
|
||||
} as NavigationTabModel
|
||||
|
||||
export const mockedNavigationStore = {
|
||||
serialize: jest.fn(),
|
||||
hydrate: jest.fn(),
|
||||
tabs: [mockedNavigationTabStore],
|
||||
tabIndex: 0,
|
||||
clear: jest.fn(),
|
||||
tab: mockedNavigationTabStore,
|
||||
tabCount: 1,
|
||||
isCurrentScreen: jest.fn(),
|
||||
navigate: jest.fn(),
|
||||
refresh: jest.fn(),
|
||||
setTitle: jest.fn(),
|
||||
handleLink: jest.fn(),
|
||||
switchTo: jest.fn(),
|
||||
setActiveTab: jest.fn(),
|
||||
closeTab: jest.fn(),
|
||||
newTab: jest.fn(),
|
||||
} as NavigationModel
|
||||
|
||||
export const mockedShellStore = {
|
||||
serialize: jest.fn(),
|
||||
hydrate: jest.fn(),
|
||||
minimalShellMode: false,
|
||||
isMainMenuOpen: false,
|
||||
isModalActive: false,
|
||||
activeModal: undefined,
|
||||
isLightboxActive: false,
|
||||
activeLightbox: undefined,
|
||||
isComposerActive: false,
|
||||
composerOpts: undefined,
|
||||
darkMode: false,
|
||||
setDarkMode: jest.fn(),
|
||||
setMainMenuOpen: jest.fn(),
|
||||
setMinimalShellMode: jest.fn(),
|
||||
openModal: jest.fn(),
|
||||
closeModal: jest.fn(),
|
||||
closeComposer: jest.fn(),
|
||||
closeLightbox: jest.fn(),
|
||||
openComposer: jest.fn(),
|
||||
openLightbox: jest.fn(),
|
||||
} as ShellUiModel
|
||||
|
||||
export const mockedMeStore = {
|
||||
serialize: jest.fn(),
|
||||
hydrate: jest.fn(),
|
||||
did: 'test did',
|
||||
handle: 'test',
|
||||
displayName: 'test',
|
||||
description: 'test',
|
||||
avatar: '',
|
||||
notificationCount: 0,
|
||||
rootStore: {} as RootStoreModel,
|
||||
memberships: mockedMembershipsStore,
|
||||
mainFeed: mockedFeedStore,
|
||||
notifications: mockedNotificationsStore,
|
||||
clear: jest.fn(),
|
||||
load: jest.fn(),
|
||||
clearNotificationCount: jest.fn(),
|
||||
fetchStateUpdate: jest.fn(),
|
||||
refreshMemberships: jest.fn(),
|
||||
} as MeModel
|
||||
|
||||
export const mockedOnboardStore = {
|
||||
serialize: jest.fn(),
|
||||
hydrate: jest.fn(),
|
||||
isOnboarding: false,
|
||||
stage: '',
|
||||
start: jest.fn(),
|
||||
stop: jest.fn(),
|
||||
next: jest.fn(),
|
||||
} as OnboardModel
|
||||
|
||||
export const mockedProfilesStore = {
|
||||
hydrate: jest.fn(),
|
||||
serialize: jest.fn(),
|
||||
cache: new LRUMap(100),
|
||||
rootStore: {} as RootStoreModel,
|
||||
getProfile: jest.fn().mockResolvedValue({data: {}}),
|
||||
overwrite: jest.fn(),
|
||||
} as ProfilesViewModel
|
||||
|
||||
export const mockedLinkMetasStore = {
|
||||
hydrate: jest.fn(),
|
||||
serialize: jest.fn(),
|
||||
cache: new LRUMap(100),
|
||||
rootStore: {} as RootStoreModel,
|
||||
getLinkMeta: jest.fn(),
|
||||
} as LinkMetasViewModel
|
||||
|
||||
export const mockedLogStore = {
|
||||
entries: [],
|
||||
serialize: jest.fn(),
|
||||
hydrate: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
// unknown required because of the missing private methods: add
|
||||
} as unknown as LogModel
|
||||
|
||||
export const mockedRootStore = {
|
||||
api: {
|
||||
com: {},
|
||||
app: {
|
||||
bsky: {
|
||||
graph: {
|
||||
confirmation: {
|
||||
delete: jest.fn().mockResolvedValue({}),
|
||||
},
|
||||
getFollowers: jest.fn().mockResolvedValue({}),
|
||||
getMembers: jest.fn().mockResolvedValue({}),
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as SessionServiceClient,
|
||||
resolveName: jest.fn(),
|
||||
serialize: jest.fn(),
|
||||
hydrate: jest.fn(),
|
||||
fetchStateUpdate: jest.fn(),
|
||||
clearAll: jest.fn(),
|
||||
session: mockedSessionStore,
|
||||
nav: mockedNavigationStore,
|
||||
shell: mockedShellStore,
|
||||
me: mockedMeStore,
|
||||
onboard: mockedOnboardStore,
|
||||
profiles: mockedProfilesStore,
|
||||
linkMetas: mockedLinkMetasStore,
|
||||
log: mockedLogStore,
|
||||
} as RootStoreModel
|
||||
|
||||
export const mockedProfileUiStore = {
|
||||
profile: mockedProfileStore,
|
||||
feed: mockedFeedStore,
|
||||
memberships: mockedMembershipsStore,
|
||||
members: mockedMembersStore,
|
||||
selectedViewIndex: 0,
|
||||
rootStore: mockedRootStore,
|
||||
params: {
|
||||
user: 'test user',
|
||||
},
|
||||
currentView: mockedFeedStore,
|
||||
isInitialLoading: false,
|
||||
isRefreshing: false,
|
||||
isUser: true,
|
||||
isScene: false,
|
||||
selectorItems: [Sections.Posts, Sections.PostsWithReplies, Sections.Scenes],
|
||||
selectedView: Sections.Posts,
|
||||
setSelectedViewIndex: jest.fn(),
|
||||
setup: jest.fn().mockResolvedValue({aborted: false}),
|
||||
update: jest.fn(),
|
||||
refresh: jest.fn(),
|
||||
loadMore: jest.fn(),
|
||||
} as ProfileUiModel
|
||||
|
||||
export const mockedAutocompleteViewStore = {
|
||||
isLoading: false,
|
||||
isActive: true,
|
||||
prefix: '',
|
||||
follows: [
|
||||
{
|
||||
did: 'test did',
|
||||
declaration: {
|
||||
cid: '',
|
||||
actorType: 'app.bsky.system.actorUser',
|
||||
},
|
||||
handle: '',
|
||||
displayName: '',
|
||||
createdAt: '',
|
||||
indexedAt: '',
|
||||
},
|
||||
],
|
||||
searchRes: [
|
||||
{
|
||||
did: 'test did',
|
||||
declaration: {
|
||||
cid: '',
|
||||
actorType: 'app.bsky.system.actorUser',
|
||||
},
|
||||
handle: '',
|
||||
displayName: '',
|
||||
},
|
||||
],
|
||||
knownHandles: new Set<string>(),
|
||||
suggestions: [
|
||||
{
|
||||
handle: 'handle.test',
|
||||
displayName: 'Test Display',
|
||||
},
|
||||
{
|
||||
handle: 'handle2.test',
|
||||
displayName: 'Test Display 2',
|
||||
},
|
||||
],
|
||||
rootStore: {} as RootStoreModel,
|
||||
setup: jest.fn(),
|
||||
setActive: jest.fn(),
|
||||
setPrefix: jest.fn(),
|
||||
// unknown required because of the missing private methods: _searchPromise, _getFollows , _search
|
||||
} as unknown as UserAutocompleteViewModel
|
||||
|
||||
export const mockedLocalPhotosStore = {
|
||||
photos: {
|
||||
node: {
|
||||
type: '',
|
||||
group_name: '',
|
||||
image: {
|
||||
filename: '',
|
||||
extension: '',
|
||||
uri: '',
|
||||
height: 1000,
|
||||
width: 1000,
|
||||
fileSize: null,
|
||||
playableDuration: 0,
|
||||
},
|
||||
timestamp: 1672847197,
|
||||
location: null,
|
||||
},
|
||||
},
|
||||
rootStore: {} as RootStoreModel,
|
||||
setup: jest.fn(),
|
||||
// unknown required because of the missing private methods: _getPhotos
|
||||
} as unknown as UserLocalPhotosModel
|
||||
|
||||
export const mockedSuggestedActorsStore = {
|
||||
isLoading: false,
|
||||
isRefreshing: false,
|
||||
hasLoaded: false,
|
||||
error: '',
|
||||
suggestions: [
|
||||
{
|
||||
did: '1',
|
||||
declaration: {
|
||||
cid: '',
|
||||
actorType: 'app.bsky.system.actorUser',
|
||||
},
|
||||
handle: 'handle1.test',
|
||||
displayName: 'test name 1',
|
||||
description: 'desc',
|
||||
indexedAt: '',
|
||||
_reactKey: '1',
|
||||
},
|
||||
{
|
||||
did: '2',
|
||||
declaration: {
|
||||
cid: '',
|
||||
actorType: 'app.bsky.system.actorUser',
|
||||
},
|
||||
handle: '',
|
||||
displayName: 'handle2.test',
|
||||
description: 'desc',
|
||||
indexedAt: '',
|
||||
_reactKey: '2',
|
||||
},
|
||||
],
|
||||
rootStore: {} as RootStoreModel,
|
||||
hasContent: true,
|
||||
hasError: false,
|
||||
isEmpty: false,
|
||||
setup: jest.fn().mockResolvedValue(null),
|
||||
refresh: jest.fn(),
|
||||
// unknown required because of the missing private methods: _xLoading, _xIdle, _fetch, _appendAll, _append
|
||||
} as unknown as SuggestedActorsViewModel
|
||||
|
||||
export const mockedUserFollowersStore = {
|
||||
isLoading: false,
|
||||
isRefreshing: false,
|
||||
hasLoaded: false,
|
||||
error: '',
|
||||
params: {
|
||||
user: 'test user',
|
||||
},
|
||||
subject: {
|
||||
did: 'test did',
|
||||
handle: '',
|
||||
declaration: {cid: '', actorType: ''},
|
||||
},
|
||||
followers: [
|
||||
{
|
||||
did: 'test did',
|
||||
declaration: {cid: '', actorType: ''},
|
||||
handle: 'testhandle',
|
||||
displayName: 'test name',
|
||||
indexedAt: '',
|
||||
_reactKey: '1',
|
||||
},
|
||||
{
|
||||
did: 'test did2',
|
||||
declaration: {cid: '', actorType: ''},
|
||||
handle: 'testhandle2',
|
||||
displayName: 'test name 2',
|
||||
indexedAt: '',
|
||||
_reactKey: '2',
|
||||
},
|
||||
],
|
||||
rootStore: {} as RootStoreModel,
|
||||
hasContent: true,
|
||||
hasError: false,
|
||||
isEmpty: false,
|
||||
setup: jest.fn(),
|
||||
refresh: jest.fn(),
|
||||
loadMore: jest.fn(),
|
||||
// unknown required because of the missing private methods: _xIdle, _xLoading, _fetch, _replaceAll, _append
|
||||
} as unknown as UserFollowersViewModel
|
||||
|
||||
export const mockedUserFollowsStore = {
|
||||
isLoading: false,
|
||||
isRefreshing: false,
|
||||
hasLoaded: false,
|
||||
error: '',
|
||||
params: {
|
||||
user: 'test user',
|
||||
},
|
||||
subject: {
|
||||
did: 'test did',
|
||||
handle: '',
|
||||
declaration: {cid: '', actorType: ''},
|
||||
},
|
||||
follows: [
|
||||
{
|
||||
did: 'test did',
|
||||
declaration: {cid: '', actorType: ''},
|
||||
handle: 'testhandle',
|
||||
displayName: 'test name',
|
||||
indexedAt: '',
|
||||
_reactKey: '1',
|
||||
},
|
||||
{
|
||||
did: 'test did2',
|
||||
declaration: {cid: '', actorType: ''},
|
||||
handle: 'testhandle2',
|
||||
displayName: 'test name 2',
|
||||
indexedAt: '',
|
||||
_reactKey: '2',
|
||||
},
|
||||
],
|
||||
rootStore: {} as RootStoreModel,
|
||||
hasContent: true,
|
||||
hasError: false,
|
||||
isEmpty: false,
|
||||
setup: jest.fn(),
|
||||
refresh: jest.fn(),
|
||||
loadMore: jest.fn(),
|
||||
// unknown required because of the missing private methods: _xIdle, _xLoading, _fetch, _replaceAll, _append
|
||||
} as unknown as UserFollowsViewModel
|
||||
|
||||
export const mockedRepostedByViewStore = {
|
||||
isLoading: false,
|
||||
isRefreshing: false,
|
||||
hasLoaded: false,
|
||||
error: '',
|
||||
resolvedUri: '',
|
||||
params: {
|
||||
uri: 'testuri',
|
||||
},
|
||||
uri: '',
|
||||
repostedBy: [
|
||||
{
|
||||
_reactKey: '',
|
||||
did: '',
|
||||
handle: '',
|
||||
displayName: '',
|
||||
declaration: {cid: '', actorType: ''},
|
||||
indexedAt: '',
|
||||
},
|
||||
],
|
||||
hasContent: false,
|
||||
hasError: false,
|
||||
isEmpty: false,
|
||||
setup: jest.fn().mockResolvedValue({}),
|
||||
refresh: jest.fn().mockResolvedValue({}),
|
||||
loadMore: jest.fn().mockResolvedValue({}),
|
||||
// unknown required because of the missing private methods: _xIdle, _xLoading, _resolveUri, _fetch, _refresh, _replaceAll, _append
|
||||
} as unknown as RepostedByViewModel
|
||||
|
||||
export const mockedVotesViewStore = {
|
||||
isLoading: false,
|
||||
isRefreshing: false,
|
||||
hasLoaded: false,
|
||||
error: '',
|
||||
resolvedUri: '',
|
||||
params: {
|
||||
uri: 'testuri',
|
||||
},
|
||||
uri: '',
|
||||
votes: [
|
||||
{
|
||||
_reactKey: '',
|
||||
direction: 'up',
|
||||
indexedAt: '',
|
||||
createdAt: '',
|
||||
actor: {
|
||||
did: '',
|
||||
handle: '',
|
||||
declaration: {cid: '', actorType: ''},
|
||||
},
|
||||
},
|
||||
],
|
||||
hasContent: false,
|
||||
hasError: false,
|
||||
isEmpty: false,
|
||||
setup: jest.fn(),
|
||||
refresh: jest.fn().mockResolvedValue({}),
|
||||
loadMore: jest.fn().mockResolvedValue({}),
|
||||
// unknown required because of the missing private methods: _xIdle, _xLoading, _resolveUri, _fetch, _replaceAll, _append
|
||||
} as unknown as VotesViewModel
|
||||
@@ -1,16 +0,0 @@
|
||||
// /**
|
||||
// * @format
|
||||
// */
|
||||
|
||||
// import 'react-native'
|
||||
// import React from 'react'
|
||||
// import App from '../src/App'
|
||||
|
||||
// // Note: test renderer must be required after react-native.
|
||||
// import renderer from 'react-test-renderer'
|
||||
|
||||
// it('renders correctly', () => {
|
||||
// renderer.act(() => {
|
||||
// renderer.create(<App />)
|
||||
// })
|
||||
// })
|
||||
@@ -0,0 +1,19 @@
|
||||
import {isNetworkError} from '../../src/lib/errors'
|
||||
|
||||
describe('isNetworkError', () => {
|
||||
const inputs = [
|
||||
'TypeError: Network request failed',
|
||||
'Uncaught TypeError: Cannot read property x of undefined',
|
||||
'Uncaught RangeError',
|
||||
'Error: Aborted',
|
||||
]
|
||||
const outputs = [true, false, false, true]
|
||||
|
||||
it('correctly distinguishes network errors', () => {
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
const input = inputs[i]
|
||||
const result = isNetworkError(input)
|
||||
expect(result).toEqual(outputs[i])
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,93 @@
|
||||
import {downloadAndResize, DownloadAndResizeOpts} from '../../src/lib/images'
|
||||
import ImageResizer from '@bam.tech/react-native-image-resizer'
|
||||
import RNFetchBlob from 'rn-fetch-blob'
|
||||
|
||||
describe('downloadAndResize', () => {
|
||||
const errorSpy = jest.spyOn(global.console, 'error')
|
||||
|
||||
const mockResizedImage = {
|
||||
path: jest.fn().mockReturnValue('file://resized-image.jpg'),
|
||||
size: 100,
|
||||
width: 50,
|
||||
height: 50,
|
||||
mime: 'image/jpeg',
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
const mockedCreateResizedImage =
|
||||
ImageResizer.createResizedImage as jest.Mock
|
||||
mockedCreateResizedImage.mockResolvedValue(mockResizedImage)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should return resized image for valid URI and options', async () => {
|
||||
const mockedFetch = RNFetchBlob.fetch as jest.Mock
|
||||
mockedFetch.mockResolvedValueOnce({
|
||||
path: jest.fn().mockReturnValue('file://downloaded-image.jpg'),
|
||||
flush: jest.fn(),
|
||||
})
|
||||
|
||||
const opts: DownloadAndResizeOpts = {
|
||||
uri: 'https://example.com/image.jpg',
|
||||
width: 100,
|
||||
height: 100,
|
||||
maxSize: 500000,
|
||||
mode: 'cover',
|
||||
timeout: 10000,
|
||||
}
|
||||
|
||||
const result = await downloadAndResize(opts)
|
||||
expect(result).toEqual(mockResizedImage)
|
||||
expect(RNFetchBlob.config).toHaveBeenCalledWith({
|
||||
fileCache: true,
|
||||
appendExt: 'jpeg',
|
||||
})
|
||||
expect(RNFetchBlob.fetch).toHaveBeenCalledWith(
|
||||
'GET',
|
||||
'https://example.com/image.jpg',
|
||||
)
|
||||
expect(ImageResizer.createResizedImage).toHaveBeenCalledWith(
|
||||
'file://downloaded-image.jpg',
|
||||
100,
|
||||
100,
|
||||
'JPEG',
|
||||
1,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{mode: 'cover'},
|
||||
)
|
||||
})
|
||||
|
||||
it('should return undefined for invalid URI', async () => {
|
||||
const opts: DownloadAndResizeOpts = {
|
||||
uri: 'invalid-uri',
|
||||
width: 100,
|
||||
height: 100,
|
||||
maxSize: 500000,
|
||||
mode: 'cover',
|
||||
timeout: 10000,
|
||||
}
|
||||
|
||||
const result = await downloadAndResize(opts)
|
||||
expect(errorSpy).toHaveBeenCalled()
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return undefined for unsupported file type', async () => {
|
||||
const opts: DownloadAndResizeOpts = {
|
||||
uri: 'https://example.com/image.bmp',
|
||||
width: 100,
|
||||
height: 100,
|
||||
maxSize: 500000,
|
||||
mode: 'cover',
|
||||
timeout: 10000,
|
||||
}
|
||||
|
||||
const result = await downloadAndResize(opts)
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import {LikelyType, getLinkMeta} from '../src/lib/link-meta'
|
||||
import {LikelyType, getLinkMeta, getLikelyType} from '../../src/lib/link-meta'
|
||||
|
||||
const exampleComHtml = `<!doctype html>
|
||||
<html>
|
||||
@@ -57,7 +57,9 @@ describe('getLinkMeta', () => {
|
||||
'https://example.com/image.png',
|
||||
'https://example.com/video.avi',
|
||||
'https://example.com/audio.ogg',
|
||||
'https://example.com/text.txt',
|
||||
'https://example.com/javascript.js',
|
||||
'https://bsky.app/index.html',
|
||||
]
|
||||
const outputs = [
|
||||
{
|
||||
@@ -94,15 +96,29 @@ describe('getLinkMeta', () => {
|
||||
likelyType: LikelyType.Audio,
|
||||
url: 'https://example.com/audio.ogg',
|
||||
},
|
||||
{
|
||||
likelyType: LikelyType.Text,
|
||||
url: 'https://example.com/text.txt',
|
||||
},
|
||||
{
|
||||
likelyType: LikelyType.Other,
|
||||
url: 'https://example.com/javascript.js',
|
||||
},
|
||||
{
|
||||
likelyType: LikelyType.AtpData,
|
||||
url: '/index.html',
|
||||
title: 'Not found',
|
||||
},
|
||||
{
|
||||
likelyType: LikelyType.Other,
|
||||
url: '',
|
||||
title: '',
|
||||
},
|
||||
]
|
||||
it('correctly handles a set of text inputs', async () => {
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
global.fetch = jest.fn().mockImplementationOnce(() => {
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise((resolve, _reject) => {
|
||||
resolve({
|
||||
ok: true,
|
||||
status: 200,
|
||||
@@ -116,3 +132,15 @@ describe('getLinkMeta', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('getLikelyType', () => {
|
||||
it('correctly handles non-parsed url', async () => {
|
||||
const output = await getLikelyType('https://example.com')
|
||||
expect(output).toEqual(LikelyType.HTML)
|
||||
})
|
||||
|
||||
it('handles non-string urls without crashing', async () => {
|
||||
const output = await getLikelyType('123')
|
||||
expect(output).toEqual(LikelyType.Other)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,24 @@
|
||||
import {clamp} from '../../src/lib/numbers'
|
||||
|
||||
describe('clamp', () => {
|
||||
const inputs: [number, number, number][] = [
|
||||
[100, 0, 200],
|
||||
[100, 0, 100],
|
||||
[0, 0, 100],
|
||||
[100, 0, -1],
|
||||
[4, 1, 1],
|
||||
[100, -100, 0],
|
||||
[400, 100, -100],
|
||||
[70, -1, 1],
|
||||
[Infinity, Infinity, Infinity],
|
||||
]
|
||||
const outputs = [100, 100, 0, -1, 1, 0, -100, 1, Infinity]
|
||||
|
||||
it('correctly clamps any given number and range', () => {
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
const input = inputs[i]
|
||||
const result = clamp(...input)
|
||||
expect(result).toEqual(outputs[i])
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -2,7 +2,17 @@ import {
|
||||
extractEntities,
|
||||
detectLinkables,
|
||||
extractHtmlMeta,
|
||||
} from '../src/lib/strings'
|
||||
pluralize,
|
||||
makeRecordUri,
|
||||
ago,
|
||||
makeValidHandle,
|
||||
createFullHandle,
|
||||
enforceLen,
|
||||
cleanError,
|
||||
toNiceDomain,
|
||||
toShortUrl,
|
||||
toShareUrl,
|
||||
} from '../../src/lib/strings'
|
||||
|
||||
describe('extractEntities', () => {
|
||||
const knownHandles = new Set(['handle.com', 'full123.test-of-chars'])
|
||||
@@ -317,3 +327,206 @@ describe('extractHtmlMeta', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('pluralize', () => {
|
||||
const inputs: [number, string, string?][] = [
|
||||
[1, 'follower'],
|
||||
[1, 'member'],
|
||||
[100, 'post'],
|
||||
[1000, 'repost'],
|
||||
[10000, 'upvote'],
|
||||
[100000, 'other'],
|
||||
[2, 'man', 'men'],
|
||||
]
|
||||
const outputs = [
|
||||
'follower',
|
||||
'member',
|
||||
'posts',
|
||||
'reposts',
|
||||
'upvotes',
|
||||
'others',
|
||||
'men',
|
||||
]
|
||||
|
||||
it('correctly pluralizes a set of words', () => {
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
const input = inputs[i]
|
||||
const output = pluralize(...input)
|
||||
expect(output).toEqual(outputs[i])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('makeRecordUri', () => {
|
||||
const inputs: [string, string, string][] = [
|
||||
['alice.test', 'app.bsky.feed.post', '3jk7x4irgv52r'],
|
||||
]
|
||||
const outputs = ['at://alice.test/app.bsky.feed.post/3jk7x4irgv52r']
|
||||
|
||||
it('correctly builds a record URI', () => {
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
const input = inputs[i]
|
||||
const result = makeRecordUri(...input)
|
||||
expect(result).toEqual(outputs[i])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('ago', () => {
|
||||
const inputs = [
|
||||
1671461038,
|
||||
'04 Dec 1995 00:12:00 GMT',
|
||||
new Date(),
|
||||
new Date().setMinutes(new Date().getMinutes() - 10),
|
||||
new Date().setHours(new Date().getHours() - 1),
|
||||
new Date().setDate(new Date().getDate() - 1),
|
||||
new Date().setMonth(new Date().getMonth() - 1),
|
||||
]
|
||||
const outputs = [
|
||||
new Date(1671461038).toLocaleDateString(),
|
||||
new Date('04 Dec 1995 00:12:00 GMT').toLocaleDateString(),
|
||||
'0s',
|
||||
'10m',
|
||||
'1h',
|
||||
'1d',
|
||||
'1mo',
|
||||
]
|
||||
|
||||
it('correctly calculates how much time passed, in a string', () => {
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
const result = ago(inputs[i])
|
||||
expect(result).toEqual(outputs[i])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('makeValidHandle', () => {
|
||||
const inputs = [
|
||||
'test-handle-123',
|
||||
'test!"#$%&/()=?_',
|
||||
'this-handle-should-be-too-big',
|
||||
]
|
||||
const outputs = ['test-handle-123', 'test', 'this-handle-should-b']
|
||||
|
||||
it('correctly parses and corrects handles', () => {
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
const result = makeValidHandle(inputs[i])
|
||||
expect(result).toEqual(outputs[i])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('createFullHandle', () => {
|
||||
const inputs: [string, string][] = [
|
||||
['test-handle-123', 'test'],
|
||||
['.test.handle', 'test.test.'],
|
||||
['test.handle.', '.test.test'],
|
||||
]
|
||||
const outputs = [
|
||||
'test-handle-123.test',
|
||||
'.test.handle.test.test.',
|
||||
'test.handle.test.test',
|
||||
]
|
||||
|
||||
it('correctly parses and corrects handles', () => {
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
const input = inputs[i]
|
||||
const result = createFullHandle(...input)
|
||||
expect(result).toEqual(outputs[i])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('enforceLen', () => {
|
||||
const inputs: [string, number][] = [
|
||||
['Hello World!', 5],
|
||||
['Hello World!', 20],
|
||||
['', 5],
|
||||
]
|
||||
const outputs = ['Hello', 'Hello World!', '']
|
||||
|
||||
it('correctly enforces defined length on a given string', () => {
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
const input = inputs[i]
|
||||
const result = enforceLen(...input)
|
||||
expect(result).toEqual(outputs[i])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('cleanError', () => {
|
||||
const inputs = [
|
||||
'TypeError: Network request failed',
|
||||
'Error: Aborted',
|
||||
'Error: TypeError "x" is not a function',
|
||||
'Error: SyntaxError unexpected token "export"',
|
||||
'Some other error',
|
||||
]
|
||||
const outputs = [
|
||||
'Unable to connect. Please check your internet connection and try again.',
|
||||
'Unable to connect. Please check your internet connection and try again.',
|
||||
'TypeError "x" is not a function',
|
||||
'SyntaxError unexpected token "export"',
|
||||
'Some other error',
|
||||
]
|
||||
|
||||
it('removes extra content from error message', () => {
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
const result = cleanError(inputs[i])
|
||||
expect(result).toEqual(outputs[i])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('toNiceDomain', () => {
|
||||
const inputs = [
|
||||
'https://example.com/index.html',
|
||||
'https://bsky.app',
|
||||
'https://bsky.social',
|
||||
'#123123123',
|
||||
]
|
||||
const outputs = ['example.com', 'bsky.app', 'Bluesky Social', '#123123123']
|
||||
|
||||
it("displays the url's host in a easily readable manner", () => {
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
const result = toNiceDomain(inputs[i])
|
||||
expect(result).toEqual(outputs[i])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('toShortUrl', () => {
|
||||
const inputs = [
|
||||
'https://bsky.app',
|
||||
'https://bsky.app/3jk7x4irgv52r',
|
||||
'https://bsky.app/3jk7x4irgv52r2313y182h9',
|
||||
]
|
||||
const outputs = [
|
||||
'bsky.app',
|
||||
'bsky.app/3jk7x4irgv52r',
|
||||
'bsky.app/3jk7x4irgv52r2313y...',
|
||||
]
|
||||
|
||||
it('shortens the url', () => {
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
const result = toShortUrl(inputs[i])
|
||||
expect(result).toEqual(outputs[i])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('toShareUrl', () => {
|
||||
const inputs = ['https://bsky.app', '/3jk7x4irgv52r', 'item/test/123']
|
||||
const outputs = [
|
||||
'https://bsky.app',
|
||||
'https://bsky.app/3jk7x4irgv52r',
|
||||
'https://bsky.app/item/test/123',
|
||||
]
|
||||
|
||||
it('appends https, when not present', () => {
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
const result = toShareUrl(inputs[i])
|
||||
expect(result).toEqual(outputs[i])
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,72 @@
|
||||
import {RootStoreModel} from '../../../src/state/models/root-store'
|
||||
import {LinkMetasViewModel} from '../../../src/state/models/link-metas-view'
|
||||
import * as LinkMetaLib from '../../../src/lib/link-meta'
|
||||
import {LikelyType} from './../../../src/lib/link-meta'
|
||||
import {sessionClient, SessionServiceClient} from '@atproto/api'
|
||||
import {DEFAULT_SERVICE} from '../../../src/state'
|
||||
|
||||
describe('LinkMetasViewModel', () => {
|
||||
let viewModel: LinkMetasViewModel
|
||||
let rootStore: RootStoreModel
|
||||
|
||||
const getLinkMetaMockSpy = jest.spyOn(LinkMetaLib, 'getLinkMeta')
|
||||
const mockedMeta = {
|
||||
title: 'Test Title',
|
||||
url: 'testurl',
|
||||
likelyType: LikelyType.Other,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
const api = sessionClient.service(DEFAULT_SERVICE) as SessionServiceClient
|
||||
rootStore = new RootStoreModel(api)
|
||||
viewModel = new LinkMetasViewModel(rootStore)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('getLinkMeta', () => {
|
||||
it('should return link meta if it is cached', async () => {
|
||||
const url = 'http://example.com'
|
||||
|
||||
viewModel.cache.set(url, mockedMeta)
|
||||
|
||||
const result = await viewModel.getLinkMeta(url)
|
||||
|
||||
expect(getLinkMetaMockSpy).not.toHaveBeenCalled()
|
||||
expect(result).toEqual(mockedMeta)
|
||||
})
|
||||
|
||||
it('should return link meta if it is not cached', async () => {
|
||||
getLinkMetaMockSpy.mockResolvedValueOnce(mockedMeta)
|
||||
|
||||
const result = await viewModel.getLinkMeta(mockedMeta.url)
|
||||
|
||||
expect(getLinkMetaMockSpy).toHaveBeenCalledWith(mockedMeta.url)
|
||||
expect(result).toEqual(mockedMeta)
|
||||
})
|
||||
|
||||
it('should cache the link meta if it is successfully returned', async () => {
|
||||
getLinkMetaMockSpy.mockResolvedValueOnce(mockedMeta)
|
||||
|
||||
await viewModel.getLinkMeta(mockedMeta.url)
|
||||
|
||||
expect(viewModel.cache.get(mockedMeta.url)).toEqual(mockedMeta)
|
||||
})
|
||||
|
||||
it('should not cache the link meta if it fails to return', async () => {
|
||||
const url = 'http://example.com'
|
||||
const error = new Error('Failed to fetch link meta')
|
||||
getLinkMetaMockSpy.mockRejectedValueOnce(error)
|
||||
|
||||
try {
|
||||
await viewModel.getLinkMeta(url)
|
||||
fail('Error was not thrown')
|
||||
} catch (e) {
|
||||
expect(e).toEqual(error)
|
||||
expect(viewModel.cache.get(url)).toBeUndefined()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,153 @@
|
||||
import {LogModel} from '../../../src/state/models/log'
|
||||
|
||||
describe('LogModel', () => {
|
||||
let logModel: LogModel
|
||||
|
||||
beforeEach(() => {
|
||||
logModel = new LogModel()
|
||||
jest.spyOn(console, 'debug')
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should call a log method and add a log entry to the entries array', () => {
|
||||
logModel.debug('Test log')
|
||||
expect(logModel.entries.length).toEqual(1)
|
||||
expect(logModel.entries[0]).toEqual({
|
||||
id: logModel.entries[0].id,
|
||||
type: 'debug',
|
||||
summary: 'Test log',
|
||||
details: undefined,
|
||||
ts: logModel.entries[0].ts,
|
||||
})
|
||||
|
||||
logModel.warn('Test log')
|
||||
expect(logModel.entries.length).toEqual(2)
|
||||
expect(logModel.entries[1]).toEqual({
|
||||
id: logModel.entries[1].id,
|
||||
type: 'warn',
|
||||
summary: 'Test log',
|
||||
details: undefined,
|
||||
ts: logModel.entries[1].ts,
|
||||
})
|
||||
|
||||
logModel.error('Test log')
|
||||
expect(logModel.entries.length).toEqual(3)
|
||||
expect(logModel.entries[2]).toEqual({
|
||||
id: logModel.entries[2].id,
|
||||
type: 'error',
|
||||
summary: 'Test log',
|
||||
details: undefined,
|
||||
ts: logModel.entries[2].ts,
|
||||
})
|
||||
})
|
||||
|
||||
it('should call the console.debug after calling the debug method', () => {
|
||||
logModel.debug('Test log')
|
||||
expect(console.debug).toHaveBeenCalledWith('Test log', '')
|
||||
})
|
||||
|
||||
it('should call the serialize method', () => {
|
||||
logModel.debug('Test log')
|
||||
expect(logModel.serialize()).toEqual({
|
||||
entries: [
|
||||
{
|
||||
id: logModel.entries[0].id,
|
||||
type: 'debug',
|
||||
summary: 'Test log',
|
||||
details: undefined,
|
||||
ts: logModel.entries[0].ts,
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('should call the hydrate method with valid properties', () => {
|
||||
logModel.hydrate({
|
||||
entries: [
|
||||
{
|
||||
id: '123',
|
||||
type: 'debug',
|
||||
summary: 'Test log',
|
||||
details: undefined,
|
||||
ts: 123,
|
||||
},
|
||||
],
|
||||
})
|
||||
expect(logModel.entries).toEqual([
|
||||
{
|
||||
id: '123',
|
||||
type: 'debug',
|
||||
summary: 'Test log',
|
||||
details: undefined,
|
||||
ts: 123,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should call the hydrate method with invalid properties', () => {
|
||||
logModel.hydrate({
|
||||
entries: [
|
||||
{
|
||||
id: '123',
|
||||
type: 'debug',
|
||||
summary: 'Test log',
|
||||
details: undefined,
|
||||
ts: 123,
|
||||
},
|
||||
{
|
||||
summary: 'Invalid entry',
|
||||
},
|
||||
],
|
||||
})
|
||||
expect(logModel.entries).toEqual([
|
||||
{
|
||||
id: '123',
|
||||
type: 'debug',
|
||||
summary: 'Test log',
|
||||
details: undefined,
|
||||
ts: 123,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should stringify the details if it is not a string', () => {
|
||||
logModel.debug('Test log', {details: 'test'})
|
||||
expect(logModel.entries[0].details).toEqual('{\n "details": "test"\n}')
|
||||
})
|
||||
|
||||
it('should stringify the details object if it is of a specific error', () => {
|
||||
class TestError extends Error {
|
||||
constructor() {
|
||||
super()
|
||||
this.name = 'TestError'
|
||||
}
|
||||
}
|
||||
const error = new TestError()
|
||||
logModel.error('Test error log', error)
|
||||
expect(logModel.entries[0].details).toEqual('TestError')
|
||||
|
||||
class XRPCInvalidResponseErrorMock {
|
||||
validationError = {toString: () => 'validationError'}
|
||||
lexiconNsid = 'test'
|
||||
}
|
||||
const xrpcInvalidResponseError = new XRPCInvalidResponseErrorMock()
|
||||
logModel.error('Test error log', xrpcInvalidResponseError)
|
||||
expect(logModel.entries[1].details).toEqual(
|
||||
'{\n "validationError": {},\n "lexiconNsid": "test"\n}',
|
||||
)
|
||||
|
||||
class XRPCErrorMock {
|
||||
status = 'status'
|
||||
error = 'error'
|
||||
message = 'message'
|
||||
}
|
||||
const xrpcError = new XRPCErrorMock()
|
||||
logModel.error('Test error log', xrpcError)
|
||||
expect(logModel.entries[2].details).toEqual(
|
||||
'{\n "status": "status",\n "error": "error",\n "message": "message"\n}',
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,183 @@
|
||||
import {RootStoreModel} from '../../../src/state/models/root-store'
|
||||
import {MeModel} from '../../../src/state/models/me'
|
||||
import {MembershipsViewModel} from './../../../src/state/models/memberships-view'
|
||||
import {NotificationsViewModel} from './../../../src/state/models/notifications-view'
|
||||
import {sessionClient, SessionServiceClient} from '@atproto/api'
|
||||
import {DEFAULT_SERVICE} from './../../../src/state/index'
|
||||
|
||||
describe('MeModel', () => {
|
||||
let rootStore: RootStoreModel
|
||||
let meModel: MeModel
|
||||
|
||||
beforeEach(() => {
|
||||
const api = sessionClient.service(DEFAULT_SERVICE) as SessionServiceClient
|
||||
rootStore = new RootStoreModel(api)
|
||||
meModel = new MeModel(rootStore)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should clear() correctly', () => {
|
||||
meModel.did = '123'
|
||||
meModel.handle = 'handle'
|
||||
meModel.displayName = 'John Doe'
|
||||
meModel.description = 'description'
|
||||
meModel.avatar = 'avatar'
|
||||
meModel.notificationCount = 1
|
||||
meModel.clear()
|
||||
expect(meModel.did).toEqual('')
|
||||
expect(meModel.handle).toEqual('')
|
||||
expect(meModel.displayName).toEqual('')
|
||||
expect(meModel.description).toEqual('')
|
||||
expect(meModel.avatar).toEqual('')
|
||||
expect(meModel.notificationCount).toEqual(0)
|
||||
expect(meModel.memberships).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should hydrate() successfully with valid properties', () => {
|
||||
meModel.hydrate({
|
||||
did: '123',
|
||||
handle: 'handle',
|
||||
displayName: 'John Doe',
|
||||
description: 'description',
|
||||
avatar: 'avatar',
|
||||
})
|
||||
expect(meModel.did).toEqual('123')
|
||||
expect(meModel.handle).toEqual('handle')
|
||||
expect(meModel.displayName).toEqual('John Doe')
|
||||
expect(meModel.description).toEqual('description')
|
||||
expect(meModel.avatar).toEqual('avatar')
|
||||
})
|
||||
|
||||
it('should not hydrate() with invalid properties', () => {
|
||||
meModel.hydrate({
|
||||
did: '',
|
||||
handle: 'handle',
|
||||
displayName: 'John Doe',
|
||||
description: 'description',
|
||||
avatar: 'avatar',
|
||||
})
|
||||
expect(meModel.did).toEqual('')
|
||||
expect(meModel.handle).toEqual('')
|
||||
expect(meModel.displayName).toEqual('')
|
||||
expect(meModel.description).toEqual('')
|
||||
expect(meModel.avatar).toEqual('')
|
||||
|
||||
meModel.hydrate({
|
||||
did: '123',
|
||||
displayName: 'John Doe',
|
||||
description: 'description',
|
||||
avatar: 'avatar',
|
||||
})
|
||||
expect(meModel.did).toEqual('')
|
||||
expect(meModel.handle).toEqual('')
|
||||
expect(meModel.displayName).toEqual('')
|
||||
expect(meModel.description).toEqual('')
|
||||
expect(meModel.avatar).toEqual('')
|
||||
})
|
||||
|
||||
it('should load() successfully', async () => {
|
||||
jest
|
||||
.spyOn(rootStore.api.app.bsky.actor, 'getProfile')
|
||||
.mockImplementationOnce((): Promise<any> => {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
displayName: 'John Doe',
|
||||
description: 'description',
|
||||
avatar: 'avatar',
|
||||
},
|
||||
})
|
||||
})
|
||||
rootStore.session.data = {
|
||||
did: '123',
|
||||
handle: 'handle',
|
||||
service: 'test service',
|
||||
accessJwt: 'test token',
|
||||
refreshJwt: 'test token',
|
||||
}
|
||||
await meModel.load()
|
||||
expect(meModel.did).toEqual('123')
|
||||
expect(meModel.handle).toEqual('handle')
|
||||
expect(meModel.displayName).toEqual('John Doe')
|
||||
expect(meModel.description).toEqual('description')
|
||||
expect(meModel.avatar).toEqual('avatar')
|
||||
})
|
||||
|
||||
it('should load() successfully without profile data', async () => {
|
||||
jest
|
||||
.spyOn(rootStore.api.app.bsky.actor, 'getProfile')
|
||||
.mockImplementationOnce((): Promise<any> => {
|
||||
return Promise.resolve({
|
||||
data: null,
|
||||
})
|
||||
})
|
||||
rootStore.session.data = {
|
||||
did: '123',
|
||||
handle: 'handle',
|
||||
service: 'test service',
|
||||
accessJwt: 'test token',
|
||||
refreshJwt: 'test token',
|
||||
}
|
||||
await meModel.load()
|
||||
expect(meModel.did).toEqual('123')
|
||||
expect(meModel.handle).toEqual('handle')
|
||||
expect(meModel.displayName).toEqual('')
|
||||
expect(meModel.description).toEqual('')
|
||||
expect(meModel.avatar).toEqual('')
|
||||
})
|
||||
|
||||
it('should load() to nothing when no session', async () => {
|
||||
rootStore.session.data = null
|
||||
await meModel.load()
|
||||
expect(meModel.did).toEqual('')
|
||||
expect(meModel.handle).toEqual('')
|
||||
expect(meModel.displayName).toEqual('')
|
||||
expect(meModel.description).toEqual('')
|
||||
expect(meModel.avatar).toEqual('')
|
||||
expect(meModel.notificationCount).toEqual(0)
|
||||
expect(meModel.memberships).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should serialize() key information', () => {
|
||||
meModel.did = '123'
|
||||
meModel.handle = 'handle'
|
||||
meModel.displayName = 'John Doe'
|
||||
meModel.description = 'description'
|
||||
meModel.avatar = 'avatar'
|
||||
|
||||
expect(meModel.serialize()).toEqual({
|
||||
did: '123',
|
||||
handle: 'handle',
|
||||
displayName: 'John Doe',
|
||||
description: 'description',
|
||||
avatar: 'avatar',
|
||||
})
|
||||
})
|
||||
|
||||
it('should clearNotificationCount() successfully', () => {
|
||||
meModel.clearNotificationCount()
|
||||
expect(meModel.notificationCount).toBe(0)
|
||||
})
|
||||
|
||||
it('should update notifs count with fetchStateUpdate()', async () => {
|
||||
meModel.notifications = {
|
||||
refresh: jest.fn(),
|
||||
} as unknown as NotificationsViewModel
|
||||
|
||||
jest
|
||||
.spyOn(rootStore.api.app.bsky.notification, 'getCount')
|
||||
.mockImplementationOnce((): Promise<any> => {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
count: 1,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
await meModel.fetchStateUpdate()
|
||||
expect(meModel.notificationCount).toBe(1)
|
||||
expect(meModel.notifications.refresh).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,154 @@
|
||||
import {
|
||||
NavigationModel,
|
||||
NavigationTabModel,
|
||||
} from './../../../src/state/models/navigation'
|
||||
import * as flags from '../../../src/build-flags'
|
||||
|
||||
describe('NavigationModel', () => {
|
||||
let model: NavigationModel
|
||||
|
||||
beforeEach(() => {
|
||||
model = new NavigationModel()
|
||||
model.setTitle([0, 0], 'title')
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should clear() to the correct base state', async () => {
|
||||
await model.clear()
|
||||
expect(model.tabCount).toBe(2)
|
||||
expect(model.tab).toEqual({
|
||||
fixedTabPurpose: 0,
|
||||
history: [
|
||||
{
|
||||
id: expect.anything(),
|
||||
ts: expect.anything(),
|
||||
url: '/',
|
||||
},
|
||||
],
|
||||
id: expect.anything(),
|
||||
index: 0,
|
||||
isNewTab: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('should call the navigate method', async () => {
|
||||
const navigateSpy = jest.spyOn(model.tab, 'navigate')
|
||||
await model.navigate('testurl', 'teststring')
|
||||
expect(navigateSpy).toHaveBeenCalledWith('testurl', 'teststring')
|
||||
})
|
||||
|
||||
it('should call the refresh method', async () => {
|
||||
const refreshSpy = jest.spyOn(model.tab, 'refresh')
|
||||
await model.refresh()
|
||||
expect(refreshSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call the isCurrentScreen method', () => {
|
||||
expect(model.isCurrentScreen(11, 0)).toEqual(false)
|
||||
})
|
||||
|
||||
it('should call the tab getter', () => {
|
||||
expect(model.tab).toEqual({
|
||||
fixedTabPurpose: 0,
|
||||
history: [
|
||||
{
|
||||
id: expect.anything(),
|
||||
ts: expect.anything(),
|
||||
url: '/',
|
||||
},
|
||||
],
|
||||
id: expect.anything(),
|
||||
index: 0,
|
||||
isNewTab: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('should call the tabCount getter', () => {
|
||||
expect(model.tabCount).toBe(2)
|
||||
})
|
||||
|
||||
describe('tabs not enabled', () => {
|
||||
jest.mock('../../../src/build-flags', () => ({
|
||||
TABS_ENABLED: false,
|
||||
}))
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should not create new tabs', () => {
|
||||
// @ts-expect-error
|
||||
flags.TABS_ENABLED = false
|
||||
model.newTab('testurl')
|
||||
expect(model.tab.isNewTab).toBe(false)
|
||||
expect(model.tabIndex).toBe(0)
|
||||
})
|
||||
|
||||
it('should not change the active tab', () => {
|
||||
// @ts-expect-error
|
||||
flags.TABS_ENABLED = false
|
||||
model.setActiveTab(2)
|
||||
expect(model.tabIndex).toBe(0)
|
||||
})
|
||||
|
||||
it('should note close tabs', () => {
|
||||
// @ts-expect-error
|
||||
flags.TABS_ENABLED = false
|
||||
model.closeTab(0)
|
||||
expect(model.tabCount).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('tabs enabled', () => {
|
||||
jest.mock('../../../src/build-flags', () => ({
|
||||
TABS_ENABLED: true,
|
||||
}))
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should create new tabs', () => {
|
||||
// @ts-expect-error
|
||||
flags.TABS_ENABLED = true
|
||||
|
||||
model.newTab('testurl', 'title')
|
||||
expect(model.tab.isNewTab).toBe(true)
|
||||
expect(model.tabIndex).toBe(2)
|
||||
})
|
||||
|
||||
it('should change the current tab', () => {
|
||||
// @ts-expect-error
|
||||
flags.TABS_ENABLED = true
|
||||
|
||||
model.setActiveTab(0)
|
||||
expect(model.tabIndex).toBe(0)
|
||||
})
|
||||
|
||||
it('should close tabs', () => {
|
||||
// @ts-expect-error
|
||||
flags.TABS_ENABLED = true
|
||||
|
||||
model.closeTab(0)
|
||||
expect(model.tabs).toEqual([
|
||||
{
|
||||
fixedTabPurpose: 1,
|
||||
history: [
|
||||
{
|
||||
id: expect.anything(),
|
||||
ts: expect.anything(),
|
||||
url: '/notifications',
|
||||
},
|
||||
],
|
||||
id: expect.anything(),
|
||||
index: 0,
|
||||
isNewTab: false,
|
||||
},
|
||||
])
|
||||
expect(model.tabIndex).toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
OnboardModel,
|
||||
OnboardStageOrder,
|
||||
} from '../../../src/state/models/onboard'
|
||||
|
||||
describe('OnboardModel', () => {
|
||||
let onboardModel: OnboardModel
|
||||
|
||||
beforeEach(() => {
|
||||
onboardModel = new OnboardModel()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should start/stop correctly', () => {
|
||||
onboardModel.start()
|
||||
expect(onboardModel.isOnboarding).toBe(true)
|
||||
onboardModel.stop()
|
||||
expect(onboardModel.isOnboarding).toBe(false)
|
||||
})
|
||||
|
||||
it('should call the next method until it has no more stages', () => {
|
||||
onboardModel.start()
|
||||
onboardModel.next()
|
||||
expect(onboardModel.stage).toBe(OnboardStageOrder[1])
|
||||
|
||||
onboardModel.next()
|
||||
expect(onboardModel.isOnboarding).toBe(false)
|
||||
expect(onboardModel.stage).toBe(OnboardStageOrder[0])
|
||||
})
|
||||
|
||||
it('serialize and hydrate', () => {
|
||||
const serialized = onboardModel.serialize()
|
||||
const newModel = new OnboardModel()
|
||||
newModel.hydrate(serialized)
|
||||
expect(newModel).toEqual(onboardModel)
|
||||
|
||||
onboardModel.start()
|
||||
onboardModel.next()
|
||||
const serialized2 = onboardModel.serialize()
|
||||
newModel.hydrate(serialized2)
|
||||
expect(newModel).toEqual(onboardModel)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,73 @@
|
||||
import {RootStoreModel} from '../../../src/state/models/root-store'
|
||||
import {setupState} from '../../../src/state'
|
||||
|
||||
describe('rootStore', () => {
|
||||
let rootStore: RootStoreModel
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
rootStore = await setupState()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('resolveName() handles inputs correctly', () => {
|
||||
const spyMethod = jest
|
||||
.spyOn(rootStore.api.com.atproto.handle, 'resolve')
|
||||
.mockResolvedValue({success: true, headers: {}, data: {did: 'testdid'}})
|
||||
|
||||
rootStore.resolveName('teststring')
|
||||
expect(spyMethod).toHaveBeenCalledWith({handle: 'teststring'})
|
||||
|
||||
expect(rootStore.resolveName('')).rejects.toThrow('Invalid handle: ""')
|
||||
|
||||
expect(rootStore.resolveName('did:123')).resolves.toReturnWith('did:123')
|
||||
})
|
||||
|
||||
it('should call the clearAll() resets state correctly', () => {
|
||||
rootStore.clearAll()
|
||||
|
||||
expect(rootStore.session.data).toEqual(null)
|
||||
expect(rootStore.nav.tabs).toEqual([
|
||||
{
|
||||
fixedTabPurpose: 0,
|
||||
history: [
|
||||
{
|
||||
id: expect.anything(),
|
||||
ts: expect.anything(),
|
||||
url: '/',
|
||||
},
|
||||
],
|
||||
id: expect.anything(),
|
||||
index: 0,
|
||||
isNewTab: false,
|
||||
},
|
||||
{
|
||||
fixedTabPurpose: 1,
|
||||
history: [
|
||||
{
|
||||
id: expect.anything(),
|
||||
ts: expect.anything(),
|
||||
url: '/notifications',
|
||||
},
|
||||
],
|
||||
id: expect.anything(),
|
||||
index: 0,
|
||||
isNewTab: false,
|
||||
},
|
||||
])
|
||||
expect(rootStore.nav.tabIndex).toEqual(0)
|
||||
expect(rootStore.me.did).toEqual('')
|
||||
expect(rootStore.me.handle).toEqual('')
|
||||
expect(rootStore.me.displayName).toEqual('')
|
||||
expect(rootStore.me.description).toEqual('')
|
||||
expect(rootStore.me.avatar).toEqual('')
|
||||
expect(rootStore.me.notificationCount).toEqual(0)
|
||||
expect(rootStore.me.memberships).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
ConfirmModal,
|
||||
ImageLightbox,
|
||||
ShellUiModel,
|
||||
} from './../../../src/state/models/shell-ui'
|
||||
|
||||
describe('ShellUiModel', () => {
|
||||
let model: ShellUiModel
|
||||
|
||||
beforeEach(() => {
|
||||
model = new ShellUiModel()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should call the openModal & closeModal method', () => {
|
||||
model.openModal(ConfirmModal)
|
||||
expect(model.isModalActive).toEqual(true)
|
||||
expect(model.activeModal).toEqual(ConfirmModal)
|
||||
|
||||
model.closeModal()
|
||||
expect(model.isModalActive).toEqual(false)
|
||||
expect(model.activeModal).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should call the openLightbox & closeLightbox method', () => {
|
||||
model.openLightbox(new ImageLightbox('uri'))
|
||||
expect(model.isLightboxActive).toEqual(true)
|
||||
expect(model.activeLightbox).toEqual(new ImageLightbox('uri'))
|
||||
|
||||
model.closeLightbox()
|
||||
expect(model.isLightboxActive).toEqual(false)
|
||||
expect(model.activeLightbox).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should call the openComposer & closeComposer method', () => {
|
||||
const composer = {
|
||||
replyTo: {
|
||||
uri: 'uri',
|
||||
cid: 'cid',
|
||||
text: 'text',
|
||||
author: {
|
||||
handle: 'handle',
|
||||
displayName: 'name',
|
||||
},
|
||||
},
|
||||
onPost: jest.fn(),
|
||||
}
|
||||
model.openComposer(composer)
|
||||
expect(model.isComposerActive).toEqual(true)
|
||||
expect(model.composerOpts).toEqual(composer)
|
||||
|
||||
model.closeComposer()
|
||||
expect(model.isComposerActive).toEqual(false)
|
||||
expect(model.composerOpts).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from 'react'
|
||||
import {Autocomplete} from '../../../../src/view/com/composer/Autocomplete'
|
||||
import {cleanup, fireEvent, render} from '../../../../jest/test-utils'
|
||||
|
||||
describe('Autocomplete', () => {
|
||||
const onSelectMock = jest.fn()
|
||||
const mockedProps = {
|
||||
active: true,
|
||||
items: [
|
||||
{
|
||||
handle: 'handle.test',
|
||||
displayName: 'Test Display',
|
||||
},
|
||||
{
|
||||
handle: 'handle2.test',
|
||||
displayName: 'Test Display 2',
|
||||
},
|
||||
],
|
||||
onSelect: onSelectMock,
|
||||
}
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('renders a button for each user', async () => {
|
||||
const {findAllByTestId} = render(<Autocomplete {...mockedProps} />)
|
||||
const autocompleteButton = await findAllByTestId('autocompleteButton')
|
||||
expect(autocompleteButton.length).toBe(2)
|
||||
})
|
||||
|
||||
it('triggers onSelect by pressing the button', async () => {
|
||||
const {findAllByTestId} = render(<Autocomplete {...mockedProps} />)
|
||||
const autocompleteButton = await findAllByTestId('autocompleteButton')
|
||||
|
||||
fireEvent.press(autocompleteButton[0])
|
||||
expect(onSelectMock).toHaveBeenCalledWith('handle.test')
|
||||
|
||||
fireEvent.press(autocompleteButton[1])
|
||||
expect(onSelectMock).toHaveBeenCalledWith('handle2.test')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,117 @@
|
||||
import React from 'react'
|
||||
import {ComposePost} from '../../../../src/view/com/composer/ComposePost'
|
||||
import {cleanup, fireEvent, render, waitFor} from '../../../../jest/test-utils'
|
||||
import * as apilib from '../../../../src/state/lib/api'
|
||||
import {
|
||||
mockedAutocompleteViewStore,
|
||||
mockedRootStore,
|
||||
} from '../../../../__mocks__/state-mock'
|
||||
import Toast from 'react-native-root-toast'
|
||||
|
||||
describe('ComposePost', () => {
|
||||
const mockedProps = {
|
||||
replyTo: {
|
||||
uri: 'testUri',
|
||||
cid: 'testCid',
|
||||
text: 'testText',
|
||||
author: {
|
||||
handle: 'test.handle',
|
||||
displayName: 'test name',
|
||||
avatar: '',
|
||||
},
|
||||
},
|
||||
onPost: jest.fn(),
|
||||
onClose: jest.fn(),
|
||||
}
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('renders post composer', async () => {
|
||||
const {findByTestId} = render(<ComposePost {...mockedProps} />)
|
||||
const composePostView = await findByTestId('composePostView')
|
||||
expect(composePostView).toBeTruthy()
|
||||
})
|
||||
|
||||
it('closes composer', async () => {
|
||||
const {findByTestId} = render(<ComposePost {...mockedProps} />)
|
||||
const composerCancelButton = await findByTestId('composerCancelButton')
|
||||
fireEvent.press(composerCancelButton)
|
||||
expect(mockedProps.onClose).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('changes text and publishes post', async () => {
|
||||
const postSpy = jest.spyOn(apilib, 'post').mockResolvedValue({
|
||||
uri: '',
|
||||
cid: '',
|
||||
})
|
||||
const toastSpy = jest.spyOn(Toast, 'show')
|
||||
|
||||
const wrapper = render(<ComposePost {...mockedProps} />)
|
||||
|
||||
const composerTextInput = await wrapper.findByTestId('composerTextInput')
|
||||
fireEvent.changeText(composerTextInput, 'testing publish')
|
||||
|
||||
const composerPublishButton = await wrapper.findByTestId(
|
||||
'composerPublishButton',
|
||||
)
|
||||
fireEvent.press(composerPublishButton)
|
||||
|
||||
expect(postSpy).toHaveBeenCalledWith(
|
||||
mockedRootStore,
|
||||
'testing publish',
|
||||
'testUri',
|
||||
[],
|
||||
new Set<string>(),
|
||||
expect.anything(),
|
||||
)
|
||||
|
||||
// Waits for request to be resolved
|
||||
await waitFor(() => {
|
||||
expect(mockedProps.onPost).toHaveBeenCalled()
|
||||
expect(mockedProps.onClose).toHaveBeenCalled()
|
||||
expect(toastSpy).toHaveBeenCalledWith('Your reply has been published', {
|
||||
animation: true,
|
||||
duration: 3500,
|
||||
hideOnPress: true,
|
||||
position: 50,
|
||||
shadow: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('selects autocomplete item', async () => {
|
||||
jest
|
||||
.spyOn(React, 'useMemo')
|
||||
.mockReturnValueOnce(mockedAutocompleteViewStore)
|
||||
|
||||
const {findAllByTestId} = render(<ComposePost {...mockedProps} />)
|
||||
const autocompleteButton = await findAllByTestId('autocompleteButton')
|
||||
|
||||
fireEvent.press(autocompleteButton[0])
|
||||
expect(mockedAutocompleteViewStore.setActive).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it('selects photos', async () => {
|
||||
const {findByTestId, queryByTestId} = render(
|
||||
<ComposePost {...mockedProps} />,
|
||||
)
|
||||
let photoCarouselPickerView = queryByTestId('photoCarouselPickerView')
|
||||
expect(photoCarouselPickerView).toBeFalsy()
|
||||
|
||||
const composerSelectPhotosButton = await findByTestId(
|
||||
'composerSelectPhotosButton',
|
||||
)
|
||||
fireEvent.press(composerSelectPhotosButton)
|
||||
|
||||
photoCarouselPickerView = await findByTestId('photoCarouselPickerView')
|
||||
expect(photoCarouselPickerView).toBeTruthy()
|
||||
|
||||
fireEvent.press(composerSelectPhotosButton)
|
||||
|
||||
photoCarouselPickerView = queryByTestId('photoCarouselPickerView')
|
||||
expect(photoCarouselPickerView).toBeFalsy()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,92 @@
|
||||
import React from 'react'
|
||||
import {PhotoCarouselPicker} from '../../../../src/view/com/composer/PhotoCarouselPicker'
|
||||
import {cleanup, fireEvent, render} from '../../../../jest/test-utils'
|
||||
import {
|
||||
openCamera,
|
||||
openCropper,
|
||||
openPicker,
|
||||
} from 'react-native-image-crop-picker'
|
||||
|
||||
describe('PhotoCarouselPicker', () => {
|
||||
const mockedProps = {
|
||||
selectedPhotos: ['mock-uri', 'mock-uri-2'],
|
||||
onSelectPhotos: jest.fn(),
|
||||
localPhotos: {
|
||||
photos: [
|
||||
{
|
||||
node: {
|
||||
image: {
|
||||
uri: 'mock-uri',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('renders carousel picker', async () => {
|
||||
const {findByTestId} = render(<PhotoCarouselPicker {...mockedProps} />)
|
||||
const photoCarouselPickerView = await findByTestId(
|
||||
'photoCarouselPickerView',
|
||||
)
|
||||
expect(photoCarouselPickerView).toBeTruthy()
|
||||
})
|
||||
|
||||
it('triggers openCamera', async () => {
|
||||
const {findByTestId} = render(<PhotoCarouselPicker {...mockedProps} />)
|
||||
const openCameraButton = await findByTestId('openCameraButton')
|
||||
fireEvent.press(openCameraButton)
|
||||
|
||||
expect(openCamera).toHaveBeenCalledWith({
|
||||
compressImageQuality: 1,
|
||||
cropping: true,
|
||||
forceJpg: true,
|
||||
freeStyleCropEnabled: true,
|
||||
height: 1000,
|
||||
mediaType: 'photo',
|
||||
width: 1000,
|
||||
})
|
||||
})
|
||||
|
||||
it('triggers openCropper', async () => {
|
||||
const {findByTestId} = render(<PhotoCarouselPicker {...mockedProps} />)
|
||||
const openSelectPhotoButton = await findByTestId('openSelectPhotoButton')
|
||||
fireEvent.press(openSelectPhotoButton)
|
||||
|
||||
expect(openCropper).toHaveBeenCalledWith({
|
||||
compressImageQuality: 1,
|
||||
forceJpg: true,
|
||||
freeStyleCropEnabled: true,
|
||||
height: 1000,
|
||||
mediaType: 'photo',
|
||||
path: 'mock-uri',
|
||||
width: 1000,
|
||||
})
|
||||
})
|
||||
|
||||
it('triggers openPicker', async () => {
|
||||
const {findByTestId} = render(<PhotoCarouselPicker {...mockedProps} />)
|
||||
const openGalleryButton = await findByTestId('openGalleryButton')
|
||||
fireEvent.press(openGalleryButton)
|
||||
|
||||
expect(openPicker).toHaveBeenCalledWith({
|
||||
maxFiles: 2,
|
||||
mediaType: 'photo',
|
||||
multiple: true,
|
||||
})
|
||||
expect(openCropper).toHaveBeenCalledWith({
|
||||
compressImageQuality: 1,
|
||||
forceJpg: true,
|
||||
freeStyleCropEnabled: true,
|
||||
height: 1000,
|
||||
mediaType: 'photo',
|
||||
path: 'mock-uri',
|
||||
width: 1000,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,70 @@
|
||||
import React from 'react'
|
||||
import {SelectedPhoto} from '../../../../src/view/com/composer/SelectedPhoto'
|
||||
import {cleanup, fireEvent, render} from '../../../../jest/test-utils'
|
||||
|
||||
describe('SelectedPhoto', () => {
|
||||
const mockedProps = {
|
||||
selectedPhotos: ['mock-uri', 'mock-uri-2'],
|
||||
onSelectPhotos: jest.fn(),
|
||||
}
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('has no photos to render', () => {
|
||||
const {queryByTestId} = render(
|
||||
<SelectedPhoto selectedPhotos={[]} onSelectPhotos={jest.fn()} />,
|
||||
)
|
||||
const selectedPhotosView = queryByTestId('selectedPhotosView')
|
||||
expect(selectedPhotosView).toBeNull()
|
||||
|
||||
const selectedPhotoImage = queryByTestId('selectedPhotoImage')
|
||||
expect(selectedPhotoImage).toBeNull()
|
||||
})
|
||||
|
||||
it('has 1 photos to render', async () => {
|
||||
const {findByTestId} = render(
|
||||
<SelectedPhoto
|
||||
selectedPhotos={['mock-uri']}
|
||||
onSelectPhotos={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
const selectedPhotosView = await findByTestId('selectedPhotosView')
|
||||
expect(selectedPhotosView).toBeTruthy()
|
||||
|
||||
const selectedPhotoImage = await findByTestId('selectedPhotoImage')
|
||||
expect(selectedPhotoImage).toBeTruthy()
|
||||
// @ts-expect-error
|
||||
expect(selectedPhotoImage).toHaveStyle({width: 250})
|
||||
})
|
||||
|
||||
it('has 2 photos to render', async () => {
|
||||
const {findAllByTestId} = render(<SelectedPhoto {...mockedProps} />)
|
||||
const selectedPhotoImage = await findAllByTestId('selectedPhotoImage')
|
||||
expect(selectedPhotoImage[0]).toBeTruthy()
|
||||
// @ts-expect-error
|
||||
expect(selectedPhotoImage[0]).toHaveStyle({width: 175})
|
||||
})
|
||||
|
||||
it('has 3 photos to render', async () => {
|
||||
const {findAllByTestId} = render(
|
||||
<SelectedPhoto
|
||||
selectedPhotos={['mock-uri', 'mock-uri-2', 'mock-uri-3']}
|
||||
onSelectPhotos={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
const selectedPhotoImage = await findAllByTestId('selectedPhotoImage')
|
||||
expect(selectedPhotoImage[0]).toBeTruthy()
|
||||
// @ts-expect-error
|
||||
expect(selectedPhotoImage[0]).toHaveStyle({width: 85})
|
||||
})
|
||||
|
||||
it('removes a photo', async () => {
|
||||
const {findAllByTestId} = render(<SelectedPhoto {...mockedProps} />)
|
||||
const removePhotoButton = await findAllByTestId('removePhotoButton')
|
||||
fireEvent.press(removePhotoButton[0])
|
||||
expect(mockedProps.onSelectPhotos).toHaveBeenCalledWith(['mock-uri-2'])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,60 @@
|
||||
import React from 'react'
|
||||
import {Keyboard} from 'react-native'
|
||||
import {CreateAccount} from '../../../../src/view/com/login/CreateAccount'
|
||||
import {cleanup, fireEvent, render} from '../../../../jest/test-utils'
|
||||
import {
|
||||
mockedLogStore,
|
||||
mockedRootStore,
|
||||
mockedSessionStore,
|
||||
mockedShellStore,
|
||||
} from '../../../../__mocks__/state-mock'
|
||||
|
||||
describe('CreateAccount', () => {
|
||||
const mockedProps = {
|
||||
onPressBack: jest.fn(),
|
||||
}
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('renders form and creates new account', async () => {
|
||||
const {findByTestId} = render(<CreateAccount {...mockedProps} />)
|
||||
|
||||
const registerEmailInput = await findByTestId('registerEmailInput')
|
||||
expect(registerEmailInput).toBeTruthy()
|
||||
fireEvent.changeText(registerEmailInput, 'test@email.com')
|
||||
|
||||
const registerHandleInput = await findByTestId('registerHandleInput')
|
||||
expect(registerHandleInput).toBeTruthy()
|
||||
fireEvent.changeText(registerHandleInput, 'test.handle')
|
||||
|
||||
const registerPasswordInput = await findByTestId('registerPasswordInput')
|
||||
expect(registerPasswordInput).toBeTruthy()
|
||||
fireEvent.changeText(registerPasswordInput, 'testpass')
|
||||
|
||||
const registerIs13Input = await findByTestId('registerIs13Input')
|
||||
expect(registerIs13Input).toBeTruthy()
|
||||
fireEvent.press(registerIs13Input)
|
||||
|
||||
const createAccountButton = await findByTestId('createAccountButton')
|
||||
expect(createAccountButton).toBeTruthy()
|
||||
fireEvent.press(createAccountButton)
|
||||
|
||||
expect(mockedSessionStore.createAccount).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('renders and selects service', async () => {
|
||||
const keyboardSpy = jest.spyOn(Keyboard, 'dismiss')
|
||||
const {findByTestId} = render(<CreateAccount {...mockedProps} />)
|
||||
|
||||
const registerSelectServiceButton = await findByTestId(
|
||||
'registerSelectServiceButton',
|
||||
)
|
||||
expect(registerSelectServiceButton).toBeTruthy()
|
||||
fireEvent.press(registerSelectServiceButton)
|
||||
|
||||
expect(mockedShellStore.openModal).toHaveBeenCalled()
|
||||
expect(keyboardSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,128 @@
|
||||
import React from 'react'
|
||||
import {Signin} from '../../../../src/view/com/login/Signin'
|
||||
import {cleanup, fireEvent, render} from '../../../../jest/test-utils'
|
||||
import {SessionServiceClient, sessionClient as AtpApi} from '@atproto/api'
|
||||
import {
|
||||
mockedLogStore,
|
||||
mockedRootStore,
|
||||
mockedSessionStore,
|
||||
mockedShellStore,
|
||||
} from '../../../../__mocks__/state-mock'
|
||||
import {Keyboard} from 'react-native'
|
||||
|
||||
describe('Signin', () => {
|
||||
const requestPasswordResetMock = jest.fn()
|
||||
const resetPasswordMock = jest.fn()
|
||||
jest.spyOn(AtpApi, 'service').mockReturnValue({
|
||||
com: {
|
||||
atproto: {
|
||||
account: {
|
||||
requestPasswordReset: requestPasswordResetMock,
|
||||
resetPassword: resetPasswordMock,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as SessionServiceClient)
|
||||
const mockedProps = {
|
||||
onPressBack: jest.fn(),
|
||||
}
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('renders logs in form', async () => {
|
||||
const {findByTestId} = render(<Signin {...mockedProps} />)
|
||||
|
||||
const loginFormView = await findByTestId('loginFormView')
|
||||
expect(loginFormView).toBeTruthy()
|
||||
|
||||
const loginUsernameInput = await findByTestId('loginUsernameInput')
|
||||
expect(loginUsernameInput).toBeTruthy()
|
||||
|
||||
fireEvent.changeText(loginUsernameInput, 'testusername')
|
||||
|
||||
const loginPasswordInput = await findByTestId('loginPasswordInput')
|
||||
expect(loginPasswordInput).toBeTruthy()
|
||||
|
||||
fireEvent.changeText(loginPasswordInput, 'test pass')
|
||||
|
||||
const loginNextButton = await findByTestId('loginNextButton')
|
||||
expect(loginNextButton).toBeTruthy()
|
||||
|
||||
fireEvent.press(loginNextButton)
|
||||
|
||||
expect(mockedSessionStore.login).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('renders selects service from login form', async () => {
|
||||
const keyboardSpy = jest.spyOn(Keyboard, 'dismiss')
|
||||
const {findByTestId} = render(<Signin {...mockedProps} />)
|
||||
|
||||
const loginSelectServiceButton = await findByTestId(
|
||||
'loginSelectServiceButton',
|
||||
)
|
||||
expect(loginSelectServiceButton).toBeTruthy()
|
||||
|
||||
fireEvent.press(loginSelectServiceButton)
|
||||
|
||||
expect(mockedShellStore.openModal).toHaveBeenCalled()
|
||||
expect(keyboardSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('renders new password form', async () => {
|
||||
const {findByTestId} = render(<Signin {...mockedProps} />)
|
||||
|
||||
const forgotPasswordButton = await findByTestId('forgotPasswordButton')
|
||||
expect(forgotPasswordButton).toBeTruthy()
|
||||
|
||||
fireEvent.press(forgotPasswordButton)
|
||||
const forgotPasswordView = await findByTestId('forgotPasswordView')
|
||||
expect(forgotPasswordView).toBeTruthy()
|
||||
|
||||
const forgotPasswordEmail = await findByTestId('forgotPasswordEmail')
|
||||
expect(forgotPasswordEmail).toBeTruthy()
|
||||
fireEvent.changeText(forgotPasswordEmail, 'test@email.com')
|
||||
|
||||
const newPasswordButton = await findByTestId('newPasswordButton')
|
||||
expect(newPasswordButton).toBeTruthy()
|
||||
fireEvent.press(newPasswordButton)
|
||||
|
||||
expect(requestPasswordResetMock).toHaveBeenCalled()
|
||||
|
||||
const newPasswordView = await findByTestId('newPasswordView')
|
||||
expect(newPasswordView).toBeTruthy()
|
||||
|
||||
const newPasswordInput = await findByTestId('newPasswordInput')
|
||||
expect(newPasswordInput).toBeTruthy()
|
||||
const resetCodeInput = await findByTestId('resetCodeInput')
|
||||
expect(resetCodeInput).toBeTruthy()
|
||||
|
||||
fireEvent.changeText(newPasswordInput, 'test pass')
|
||||
fireEvent.changeText(resetCodeInput, 'test reset code')
|
||||
|
||||
const setNewPasswordButton = await findByTestId('setNewPasswordButton')
|
||||
expect(setNewPasswordButton).toBeTruthy()
|
||||
|
||||
fireEvent.press(setNewPasswordButton)
|
||||
|
||||
expect(resetPasswordMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('renders forgot password form', async () => {
|
||||
const {findByTestId} = render(<Signin {...mockedProps} />)
|
||||
|
||||
const forgotPasswordButton = await findByTestId('forgotPasswordButton')
|
||||
expect(forgotPasswordButton).toBeTruthy()
|
||||
|
||||
fireEvent.press(forgotPasswordButton)
|
||||
const forgotPasswordSelectServiceButton = await findByTestId(
|
||||
'forgotPasswordSelectServiceButton',
|
||||
)
|
||||
expect(forgotPasswordSelectServiceButton).toBeTruthy()
|
||||
|
||||
fireEvent.press(forgotPasswordSelectServiceButton)
|
||||
|
||||
expect(mockedShellStore.openModal).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,109 @@
|
||||
import React from 'react'
|
||||
import {cleanup, fireEvent, render} from '../../../../jest/test-utils'
|
||||
import {ProfileViewModel} from '../../../../src/state/models/profile-view'
|
||||
import {ProfileHeader} from '../../../../src/view/com/profile/ProfileHeader'
|
||||
import {
|
||||
mockedNavigationStore,
|
||||
mockedProfileStore,
|
||||
mockedShellStore,
|
||||
} from '../../../../__mocks__/state-mock'
|
||||
|
||||
describe('ProfileHeader', () => {
|
||||
const mockedProps = {
|
||||
view: mockedProfileStore,
|
||||
onRefreshAll: jest.fn(),
|
||||
}
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('renders ErrorMessage on error', async () => {
|
||||
const {findByTestId} = render(
|
||||
<ProfileHeader
|
||||
{...{
|
||||
view: {
|
||||
...mockedProfileStore,
|
||||
hasError: true,
|
||||
} as ProfileViewModel,
|
||||
onRefreshAll: jest.fn(),
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
|
||||
const profileHeaderHasError = await findByTestId('profileHeaderHasError')
|
||||
expect(profileHeaderHasError).toBeTruthy()
|
||||
})
|
||||
|
||||
it('presses and opens edit profile', async () => {
|
||||
const {findByTestId} = render(<ProfileHeader {...mockedProps} />)
|
||||
|
||||
const profileHeaderEditProfileButton = await findByTestId(
|
||||
'profileHeaderEditProfileButton',
|
||||
)
|
||||
expect(profileHeaderEditProfileButton).toBeTruthy()
|
||||
fireEvent.press(profileHeaderEditProfileButton)
|
||||
|
||||
expect(mockedShellStore.openModal).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('presses and opens followers page', async () => {
|
||||
const {findByTestId} = render(<ProfileHeader {...mockedProps} />)
|
||||
|
||||
const profileHeaderFollowersButton = await findByTestId(
|
||||
'profileHeaderFollowersButton',
|
||||
)
|
||||
expect(profileHeaderFollowersButton).toBeTruthy()
|
||||
fireEvent.press(profileHeaderFollowersButton)
|
||||
|
||||
expect(mockedNavigationStore.navigate).toHaveBeenCalledWith(
|
||||
'/profile/testhandle/followers',
|
||||
)
|
||||
})
|
||||
|
||||
it('presses and opens avatar modal', async () => {
|
||||
const {findByTestId} = render(<ProfileHeader {...mockedProps} />)
|
||||
|
||||
const profileHeaderAviButton = await findByTestId('profileHeaderAviButton')
|
||||
expect(profileHeaderAviButton).toBeTruthy()
|
||||
fireEvent.press(profileHeaderAviButton)
|
||||
|
||||
expect(mockedShellStore.openLightbox).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('presses and opens follows page', async () => {
|
||||
const {findByTestId} = render(<ProfileHeader {...mockedProps} />)
|
||||
|
||||
const profileHeaderFollowsButton = await findByTestId(
|
||||
'profileHeaderFollowsButton',
|
||||
)
|
||||
expect(profileHeaderFollowsButton).toBeTruthy()
|
||||
fireEvent.press(profileHeaderFollowsButton)
|
||||
|
||||
expect(mockedNavigationStore.navigate).toHaveBeenCalledWith(
|
||||
'/profile/testhandle/follows',
|
||||
)
|
||||
})
|
||||
|
||||
it('toggles following', async () => {
|
||||
const {findByTestId} = render(
|
||||
<ProfileHeader
|
||||
{...{
|
||||
view: {
|
||||
...mockedProfileStore,
|
||||
did: 'test did 2',
|
||||
} as ProfileViewModel,
|
||||
onRefreshAll: jest.fn(),
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
|
||||
const profileHeaderToggleFollowButton = await findByTestId(
|
||||
'profileHeaderToggleFollowButton',
|
||||
)
|
||||
expect(profileHeaderToggleFollowButton).toBeTruthy()
|
||||
fireEvent.press(profileHeaderToggleFollowButton)
|
||||
|
||||
expect(mockedProps.view.toggleFollowing).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,17 @@
|
||||
import {renderHook} from '../../../jest/test-utils'
|
||||
import {useAnimatedValue} from '../../../src/view/lib/hooks/useAnimatedValue'
|
||||
|
||||
describe('useAnimatedValue', () => {
|
||||
it('creates an Animated.Value with the initial value passed to the hook', () => {
|
||||
const {result} = renderHook(() => useAnimatedValue(10))
|
||||
// @ts-expect-error
|
||||
expect(result.current.__getValue()).toEqual(10)
|
||||
})
|
||||
|
||||
it('returns the same Animated.Value instance on subsequent renders', () => {
|
||||
const {result, rerender} = renderHook(() => useAnimatedValue(10))
|
||||
const firstValue = result.current
|
||||
rerender({})
|
||||
expect(result.current).toBe(firstValue)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from 'react'
|
||||
import {fireEvent, render} from '../../../jest/test-utils'
|
||||
import {Home} from '../../../src/view/screens/Home'
|
||||
import {mockedRootStore, mockedShellStore} from '../../../__mocks__/state-mock'
|
||||
|
||||
describe('useOnMainScroll', () => {
|
||||
const mockedProps = {
|
||||
navIdx: [0, 0] as [number, number],
|
||||
params: {},
|
||||
visible: true,
|
||||
}
|
||||
|
||||
it('toggles minimalShellMode to true', () => {
|
||||
jest.useFakeTimers()
|
||||
const {getByTestId} = render(<Home {...mockedProps} />)
|
||||
|
||||
fireEvent.scroll(getByTestId('homeFeed'), {
|
||||
nativeEvent: {
|
||||
contentOffset: {y: 20},
|
||||
contentSize: {height: 100},
|
||||
layoutMeasurement: {height: 50},
|
||||
},
|
||||
})
|
||||
|
||||
expect(mockedRootStore.shell.setMinimalShellMode).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it('toggles minimalShellMode to false', () => {
|
||||
jest.useFakeTimers()
|
||||
const {getByTestId} = render(<Home {...mockedProps} />, {
|
||||
...mockedRootStore,
|
||||
shell: {
|
||||
...mockedShellStore,
|
||||
minimalShellMode: true,
|
||||
},
|
||||
})
|
||||
|
||||
fireEvent.scroll(getByTestId('homeFeed'), {
|
||||
nativeEvent: {
|
||||
contentOffset: {y: 0},
|
||||
contentSize: {height: 100},
|
||||
layoutMeasurement: {height: 50},
|
||||
},
|
||||
})
|
||||
expect(mockedRootStore.shell.setMinimalShellMode).toHaveBeenCalledWith(
|
||||
false,
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from 'react'
|
||||
import {Login} from '../../../src/view/screens/Login'
|
||||
import {cleanup, fireEvent, render} from '../../../jest/test-utils'
|
||||
|
||||
describe('Login', () => {
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('renders initial screen', () => {
|
||||
const {getByTestId} = render(<Login />)
|
||||
const signUpScreen = getByTestId('signinOrCreateAccount')
|
||||
|
||||
expect(signUpScreen).toBeTruthy()
|
||||
})
|
||||
|
||||
it('renders Signin screen', () => {
|
||||
const {getByTestId} = render(<Login />)
|
||||
const signInButton = getByTestId('signInButton')
|
||||
|
||||
fireEvent.press(signInButton)
|
||||
|
||||
const signInScreen = getByTestId('signIn')
|
||||
expect(signInScreen).toBeTruthy()
|
||||
})
|
||||
|
||||
it('renders CreateAccount screen', () => {
|
||||
const {getByTestId} = render(<Login />)
|
||||
const createAccountButton = getByTestId('createAccountButton')
|
||||
|
||||
fireEvent.press(createAccountButton)
|
||||
|
||||
const createAccountScreen = getByTestId('createAccount')
|
||||
expect(createAccountScreen).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react'
|
||||
import {NotFound} from '../../../src/view/screens/NotFound'
|
||||
import {cleanup, fireEvent, render} from '../../../jest/test-utils'
|
||||
import {mockedNavigationStore} from '../../../__mocks__/state-mock'
|
||||
|
||||
describe('NotFound', () => {
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('navigates home', async () => {
|
||||
const navigationSpy = jest.spyOn(mockedNavigationStore, 'navigate')
|
||||
const {getByTestId} = render(<NotFound />)
|
||||
const navigateHomeButton = getByTestId('navigateHomeButton')
|
||||
|
||||
fireEvent.press(navigateHomeButton)
|
||||
|
||||
expect(navigationSpy).toHaveBeenCalledWith('/')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from 'react'
|
||||
import {Search} from '../../../src/view/screens/Search'
|
||||
import {cleanup, fireEvent, render} from '../../../jest/test-utils'
|
||||
|
||||
describe('Search', () => {
|
||||
jest.useFakeTimers()
|
||||
const mockedProps = {
|
||||
navIdx: [0, 0] as [number, number],
|
||||
params: {
|
||||
name: 'test name',
|
||||
},
|
||||
visible: true,
|
||||
}
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('renders with query', async () => {
|
||||
const {findByTestId} = render(<Search {...mockedProps} />)
|
||||
const searchTextInput = await findByTestId('searchTextInput')
|
||||
|
||||
expect(searchTextInput).toBeTruthy()
|
||||
fireEvent.changeText(searchTextInput, 'test')
|
||||
|
||||
const searchScrollView = await findByTestId('searchScrollView')
|
||||
expect(searchScrollView).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,60 @@
|
||||
import React from 'react'
|
||||
import {Menu} from '../../../../src/view/shell/mobile/Menu'
|
||||
import {cleanup, fireEvent, render} from '../../../../jest/test-utils'
|
||||
import {
|
||||
mockedNavigationStore,
|
||||
mockedShellStore,
|
||||
} from '../../../../__mocks__/state-mock'
|
||||
|
||||
describe('Menu', () => {
|
||||
const onCloseMock = jest.fn()
|
||||
|
||||
const mockedProps = {
|
||||
visible: true,
|
||||
onClose: onCloseMock,
|
||||
}
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('renders menu', () => {
|
||||
const {getByTestId} = render(<Menu {...mockedProps} />)
|
||||
|
||||
const menuView = getByTestId('menuView')
|
||||
|
||||
expect(menuView).toBeTruthy()
|
||||
})
|
||||
|
||||
it('presses profile card button', () => {
|
||||
const {getByTestId} = render(<Menu {...mockedProps} />)
|
||||
|
||||
const profileCardButton = getByTestId('profileCardButton')
|
||||
fireEvent.press(profileCardButton)
|
||||
|
||||
expect(onCloseMock).toHaveBeenCalled()
|
||||
expect(mockedNavigationStore.switchTo).toHaveBeenCalledWith(0, true)
|
||||
})
|
||||
|
||||
it('presses search button', () => {
|
||||
const {getByTestId} = render(<Menu {...mockedProps} />)
|
||||
|
||||
const searchBtn = getByTestId('searchBtn')
|
||||
fireEvent.press(searchBtn)
|
||||
|
||||
expect(onCloseMock).toHaveBeenCalled()
|
||||
expect(mockedNavigationStore.switchTo).toHaveBeenCalledWith(0, true)
|
||||
expect(mockedNavigationStore.navigate).toHaveBeenCalledWith('/search')
|
||||
})
|
||||
|
||||
it("presses notifications menu item' button", () => {
|
||||
const {getAllByTestId} = render(<Menu {...mockedProps} />)
|
||||
|
||||
const menuItemButton = getAllByTestId('menuItemButton')
|
||||
fireEvent.press(menuItemButton[1])
|
||||
|
||||
expect(onCloseMock).toHaveBeenCalled()
|
||||
expect(mockedNavigationStore.switchTo).toHaveBeenCalledWith(1, true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,99 @@
|
||||
import React from 'react'
|
||||
import {Animated, Share} from 'react-native'
|
||||
import {TabsSelector} from '../../../../src/view/shell/mobile/TabsSelector'
|
||||
import {cleanup, fireEvent, render} from '../../../../jest/test-utils'
|
||||
import {mockedNavigationStore} from '../../../../__mocks__/state-mock'
|
||||
|
||||
describe('TabsSelector', () => {
|
||||
const onCloseMock = jest.fn()
|
||||
|
||||
const mockedProps = {
|
||||
active: true,
|
||||
tabMenuInterp: new Animated.Value(0),
|
||||
onClose: onCloseMock,
|
||||
}
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('renders tabs selector', () => {
|
||||
const {getByTestId} = render(<TabsSelector {...mockedProps} />)
|
||||
|
||||
const tabsSelectorView = getByTestId('tabsSelectorView')
|
||||
|
||||
expect(tabsSelectorView).toBeTruthy()
|
||||
})
|
||||
|
||||
it('renders nothing if inactive', () => {
|
||||
const {getByTestId} = render(
|
||||
<TabsSelector {...{...mockedProps, active: false}} />,
|
||||
)
|
||||
|
||||
const emptyView = getByTestId('emptyView')
|
||||
|
||||
expect(emptyView).toBeTruthy()
|
||||
})
|
||||
|
||||
it('presses share button', () => {
|
||||
const shareSpy = jest.spyOn(Share, 'share')
|
||||
const {getByTestId} = render(<TabsSelector {...mockedProps} />)
|
||||
|
||||
const shareButton = getByTestId('shareButton')
|
||||
fireEvent.press(shareButton)
|
||||
|
||||
expect(onCloseMock).toHaveBeenCalled()
|
||||
expect(shareSpy).toHaveBeenCalledWith({url: 'https://bsky.app/'})
|
||||
})
|
||||
|
||||
it('presses clone button', () => {
|
||||
const {getByTestId} = render(<TabsSelector {...mockedProps} />)
|
||||
|
||||
const cloneButton = getByTestId('cloneButton')
|
||||
fireEvent.press(cloneButton)
|
||||
|
||||
expect(onCloseMock).toHaveBeenCalled()
|
||||
expect(mockedNavigationStore.newTab).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('presses new tab button', () => {
|
||||
const {getByTestId} = render(<TabsSelector {...mockedProps} />)
|
||||
|
||||
const newTabButton = getByTestId('newTabButton')
|
||||
fireEvent.press(newTabButton)
|
||||
|
||||
expect(onCloseMock).toHaveBeenCalled()
|
||||
expect(mockedNavigationStore.newTab).toHaveBeenCalledWith('/')
|
||||
})
|
||||
|
||||
it('presses change tab button', () => {
|
||||
const {getAllByTestId} = render(<TabsSelector {...mockedProps} />)
|
||||
|
||||
const changeTabButton = getAllByTestId('changeTabButton')
|
||||
fireEvent.press(changeTabButton[0])
|
||||
|
||||
expect(onCloseMock).toHaveBeenCalled()
|
||||
expect(mockedNavigationStore.newTab).toHaveBeenCalledWith('/')
|
||||
})
|
||||
|
||||
it('presses close tab button', () => {
|
||||
const {getAllByTestId} = render(<TabsSelector {...mockedProps} />)
|
||||
|
||||
const closeTabButton = getAllByTestId('closeTabButton')
|
||||
fireEvent.press(closeTabButton[0])
|
||||
|
||||
expect(onCloseMock).toHaveBeenCalled()
|
||||
expect(mockedNavigationStore.setActiveTab).toHaveBeenCalledWith(0)
|
||||
})
|
||||
|
||||
it('presses swipes to close the tab', () => {
|
||||
const {getByTestId} = render(<TabsSelector {...mockedProps} />)
|
||||
|
||||
const tabsSwipable = getByTestId('tabsSwipable')
|
||||
fireEvent(tabsSwipable, 'swipeableRightOpen')
|
||||
|
||||
expect(onCloseMock).toHaveBeenCalled()
|
||||
expect(mockedNavigationStore.setActiveTab).toHaveBeenCalledWith(0)
|
||||
})
|
||||
})
|
||||
@@ -267,6 +267,8 @@ dependencies {
|
||||
implementation "com.facebook.react:react-native:+" // From node_modules
|
||||
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
|
||||
|
||||
implementation project(':rn-fetch-blob')
|
||||
|
||||
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
|
||||
exclude group:'com.facebook.fbjni'
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
package="xyz.blueskyweb.app">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
@@ -15,7 +17,7 @@
|
||||
android:label="@string/app_name"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:windowSoftInputMode="adjustPan"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
@@ -4,10 +4,10 @@ import org.apache.tools.ant.taskdefs.condition.Os
|
||||
|
||||
buildscript {
|
||||
ext {
|
||||
buildToolsVersion = "31.0.0"
|
||||
buildToolsVersion = "33.0.0"
|
||||
minSdkVersion = 21
|
||||
compileSdkVersion = 31
|
||||
targetSdkVersion = 31
|
||||
compileSdkVersion = 33
|
||||
targetSdkVersion = 33
|
||||
|
||||
if (System.properties['os.arch'] == "aarch64") {
|
||||
// For M1 Users we need to use the NDK 24 which added support for aarch64
|
||||
@@ -49,5 +49,6 @@ allprojects {
|
||||
}
|
||||
google()
|
||||
maven { url 'https://www.jitpack.io' }
|
||||
maven { url 'https://maven.google.com' }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@ rootProject.name = 'app'
|
||||
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
|
||||
include ':app'
|
||||
includeBuild('../node_modules/react-native-gradle-plugin')
|
||||
|
||||
include ':rn-fetch-blob'
|
||||
project(':rn-fetch-blob').projectDir = new File(rootProject.projectDir, '../node_modules/rn-fetch-blob/android')
|
||||
if (settings.hasProperty("newArchEnabled") && settings.newArchEnabled == "true") {
|
||||
include(":ReactAndroid")
|
||||
project(":ReactAndroid").projectDir = file('../node_modules/react-native/ReactAndroid')
|
||||
|
||||
+32
-14
@@ -220,9 +220,11 @@ PODS:
|
||||
- React-jsinspector (0.68.2)
|
||||
- React-logger (0.68.2):
|
||||
- glog
|
||||
- react-native-cameraroll (5.1.0):
|
||||
- react-native-cameraroll (5.2.0):
|
||||
- React-Core
|
||||
- react-native-pager-view (6.0.2):
|
||||
- react-native-image-resizer (3.0.4):
|
||||
- React-Core
|
||||
- react-native-pager-view (6.1.2):
|
||||
- React-Core
|
||||
- react-native-safe-area-context (4.4.1):
|
||||
- RCT-Folly
|
||||
@@ -299,11 +301,13 @@ PODS:
|
||||
- React-jsi (= 0.68.2)
|
||||
- React-logger (= 0.68.2)
|
||||
- React-perflogger (= 0.68.2)
|
||||
- RNCAsyncStorage (1.17.10):
|
||||
- rn-fetch-blob (0.12.0):
|
||||
- React-Core
|
||||
- RNCAsyncStorage (1.17.11):
|
||||
- React-Core
|
||||
- RNCClipboard (1.11.1):
|
||||
- React-Core
|
||||
- RNGestureHandler (2.6.1):
|
||||
- RNGestureHandler (2.8.0):
|
||||
- React-Core
|
||||
- RNImageCropPicker (0.38.1):
|
||||
- React-Core
|
||||
@@ -316,7 +320,9 @@ PODS:
|
||||
- TOCropViewController
|
||||
- RNInAppBrowser (3.7.0):
|
||||
- React-Core
|
||||
- RNReanimated (2.10.0):
|
||||
- RNReactNativeHapticFeedback (1.14.0):
|
||||
- React-Core
|
||||
- RNReanimated (2.13.0):
|
||||
- DoubleConversion
|
||||
- FBLazyVector
|
||||
- FBReactNativeSpec
|
||||
@@ -343,10 +349,10 @@ PODS:
|
||||
- React-RCTText
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- RNScreens (3.17.0):
|
||||
- RNScreens (3.18.2):
|
||||
- React-Core
|
||||
- React-RCTImage
|
||||
- RNSVG (12.4.4):
|
||||
- RNSVG (12.5.0):
|
||||
- React-Core
|
||||
- TOCropViewController (2.6.1)
|
||||
- Yoga (1.14.0)
|
||||
@@ -374,6 +380,7 @@ DEPENDENCIES:
|
||||
- React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`)
|
||||
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
|
||||
- "react-native-cameraroll (from `../node_modules/@react-native-camera-roll/camera-roll`)"
|
||||
- "react-native-image-resizer (from `../node_modules/@bam.tech/react-native-image-resizer`)"
|
||||
- react-native-pager-view (from `../node_modules/react-native-pager-view`)
|
||||
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
|
||||
- react-native-splash-screen (from `../node_modules/react-native-splash-screen`)
|
||||
@@ -390,11 +397,13 @@ DEPENDENCIES:
|
||||
- React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`)
|
||||
- React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`)
|
||||
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
|
||||
- rn-fetch-blob (from `../node_modules/rn-fetch-blob`)
|
||||
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
|
||||
- "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)"
|
||||
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
|
||||
- RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`)
|
||||
- RNInAppBrowser (from `../node_modules/react-native-inappbrowser-reborn`)
|
||||
- RNReactNativeHapticFeedback (from `../node_modules/react-native-haptic-feedback`)
|
||||
- RNReanimated (from `../node_modules/react-native-reanimated`)
|
||||
- RNScreens (from `../node_modules/react-native-screens`)
|
||||
- RNSVG (from `../node_modules/react-native-svg`)
|
||||
@@ -446,6 +455,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native/ReactCommon/logger"
|
||||
react-native-cameraroll:
|
||||
:path: "../node_modules/@react-native-camera-roll/camera-roll"
|
||||
react-native-image-resizer:
|
||||
:path: "../node_modules/@bam.tech/react-native-image-resizer"
|
||||
react-native-pager-view:
|
||||
:path: "../node_modules/react-native-pager-view"
|
||||
react-native-safe-area-context:
|
||||
@@ -478,6 +489,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native/ReactCommon/runtimeexecutor"
|
||||
ReactCommon:
|
||||
:path: "../node_modules/react-native/ReactCommon"
|
||||
rn-fetch-blob:
|
||||
:path: "../node_modules/rn-fetch-blob"
|
||||
RNCAsyncStorage:
|
||||
:path: "../node_modules/@react-native-async-storage/async-storage"
|
||||
RNCClipboard:
|
||||
@@ -488,6 +501,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native-image-crop-picker"
|
||||
RNInAppBrowser:
|
||||
:path: "../node_modules/react-native-inappbrowser-reborn"
|
||||
RNReactNativeHapticFeedback:
|
||||
:path: "../node_modules/react-native-haptic-feedback"
|
||||
RNReanimated:
|
||||
:path: "../node_modules/react-native-reanimated"
|
||||
RNScreens:
|
||||
@@ -518,8 +533,9 @@ SPEC CHECKSUMS:
|
||||
React-jsiexecutor: b7b553412f2ec768fe6c8f27cd6bafdb9d8719e6
|
||||
React-jsinspector: c5989c77cb89ae6a69561095a61cce56a44ae8e8
|
||||
React-logger: a0833912d93b36b791b7a521672d8ee89107aff1
|
||||
react-native-cameraroll: a40b082318eb1ecd0336a2f29d9f74b7f2c8cae8
|
||||
react-native-pager-view: 592421df0259bf7a7a4fe85b74c24f3f39905605
|
||||
react-native-cameraroll: 0ff04cc4e0ff5f19a94ff4313e5c8bc4503cd86d
|
||||
react-native-image-resizer: 794abf75ec13ed1f0dbb1f134e27504ea65e9e66
|
||||
react-native-pager-view: 54bed894cecebe28cede54c01038d9d1e122de43
|
||||
react-native-safe-area-context: 99b24a0c5acd0d5dcac2b1a7f18c49ea317be99a
|
||||
react-native-splash-screen: 4312f786b13a81b5169ef346d76d33bc0c6dc457
|
||||
react-native-version-number: b415bbec6a13f2df62bf978e85bc0d699462f37f
|
||||
@@ -535,14 +551,16 @@ SPEC CHECKSUMS:
|
||||
React-RCTVibration: 79040b92bfa9c3c2d2cb4f57e981164ec7ab9374
|
||||
React-runtimeexecutor: b960b687d2dfef0d3761fbb187e01812ebab8b23
|
||||
ReactCommon: 095366164a276d91ea704ce53cb03825c487a3f2
|
||||
RNCAsyncStorage: 0c357f3156fcb16c8589ede67cc036330b6698ca
|
||||
rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba
|
||||
RNCAsyncStorage: 8616bd5a58af409453ea4e1b246521bb76578d60
|
||||
RNCClipboard: 2834e1c4af68697089cdd455ee4a4cdd198fa7dd
|
||||
RNGestureHandler: 28ad20bf02257791f7f137b31beef34b9549f54b
|
||||
RNGestureHandler: 62232ba8f562f7dea5ba1b3383494eb5bf97a4d3
|
||||
RNImageCropPicker: 648356d68fbf9911a1016b3e3723885d28373eda
|
||||
RNInAppBrowser: e36d6935517101ccba0e875bac8ad7b0cb655364
|
||||
RNReanimated: 5bdcbcc3a72aedeee7bb099604262403aa75a1e5
|
||||
RNScreens: 0df01424e9e0ed7827200d6ed1087ddd06c493f9
|
||||
RNSVG: ecd661f380a07ba690c9c5929c475a44f432d674
|
||||
RNReactNativeHapticFeedback: 1e3efeca9628ff9876ee7cdd9edec1b336913f8c
|
||||
RNReanimated: b21b362b4b8ca921932e8b1718e88cf3a36f157e
|
||||
RNScreens: 34cc502acf1b916c582c60003dc3089fa01dc66d
|
||||
RNSVG: 6adc5c52d2488a476248413064b7f2832e639057
|
||||
TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863
|
||||
Yoga: 99652481fcd320aefa4a7ef90095b95acd181952
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; };
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
|
||||
56F0250941FE75DA8CF8094A /* libPods-app-appTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F5D2424078D205E2F98F0707 /* libPods-app-appTests.a */; };
|
||||
5698CA584FD738B2091BD18F /* libPods-app-appTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C0C7BDE7769B84011D3747DC /* libPods-app-appTests.a */; };
|
||||
67BF1AE6AABFC881715B2D6A /* libPods-app.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8789F612EDA2C48C6064ADD6 /* libPods-app.a */; };
|
||||
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
|
||||
BFADB63545C91424F7C8293A /* libPods-app.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9C668306DD1C0A63684EDCB0 /* libPods-app.a */; };
|
||||
E4BBD590292C1F5200296224 /* app.entitlements in Resources */ = {isa = PBXBuildFile; fileRef = E4437C9E28581FA7006DA9E7 /* app.entitlements */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
@@ -37,15 +37,15 @@
|
||||
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = app/Images.xcassets; sourceTree = "<group>"; };
|
||||
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = app/Info.plist; sourceTree = "<group>"; };
|
||||
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = app/main.m; sourceTree = "<group>"; };
|
||||
3D3D4D80CD734363E3EADE03 /* Pods-app.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-app.release.xcconfig"; path = "Target Support Files/Pods-app/Pods-app.release.xcconfig"; sourceTree = "<group>"; };
|
||||
65D2EDDD9D88DA37994C7743 /* Pods-app-appTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-app-appTests.debug.xcconfig"; path = "Target Support Files/Pods-app-appTests/Pods-app-appTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
53DBA218C184B95B107AC33E /* Pods-app.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-app.release.xcconfig"; path = "Target Support Files/Pods-app/Pods-app.release.xcconfig"; sourceTree = "<group>"; };
|
||||
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = app/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
9C668306DD1C0A63684EDCB0 /* libPods-app.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-app.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
A79BD0E1A3F5B868AD4F5D68 /* Pods-app.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-app.debug.xcconfig"; path = "Target Support Files/Pods-app/Pods-app.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
AA0AD1B60CF7C90FFDB948B9 /* Pods-app-appTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-app-appTests.release.xcconfig"; path = "Target Support Files/Pods-app-appTests/Pods-app-appTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
8789F612EDA2C48C6064ADD6 /* libPods-app.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-app.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
8BB4EDB104E125B8A1913E74 /* Pods-app.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-app.debug.xcconfig"; path = "Target Support Files/Pods-app/Pods-app.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
A8E093A0B5DA947150924A68 /* Pods-app-appTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-app-appTests.release.xcconfig"; path = "Target Support Files/Pods-app-appTests/Pods-app-appTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
C01FB6762BC17DADC0319338 /* Pods-app-appTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-app-appTests.debug.xcconfig"; path = "Target Support Files/Pods-app-appTests/Pods-app-appTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
C0C7BDE7769B84011D3747DC /* libPods-app-appTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-app-appTests.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
E4437C9E28581FA7006DA9E7 /* app.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = app.entitlements; path = app/app.entitlements; sourceTree = "<group>"; };
|
||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
|
||||
F5D2424078D205E2F98F0707 /* libPods-app-appTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-app-appTests.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -53,7 +53,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
56F0250941FE75DA8CF8094A /* libPods-app-appTests.a in Frameworks */,
|
||||
5698CA584FD738B2091BD18F /* libPods-app-appTests.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -61,7 +61,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
BFADB63545C91424F7C8293A /* libPods-app.a in Frameworks */,
|
||||
67BF1AE6AABFC881715B2D6A /* libPods-app.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -103,8 +103,8 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
|
||||
9C668306DD1C0A63684EDCB0 /* libPods-app.a */,
|
||||
F5D2424078D205E2F98F0707 /* libPods-app-appTests.a */,
|
||||
8789F612EDA2C48C6064ADD6 /* libPods-app.a */,
|
||||
C0C7BDE7769B84011D3747DC /* libPods-app-appTests.a */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
@@ -143,10 +143,10 @@
|
||||
BBD78D7AC51CEA395F1C20DB /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A79BD0E1A3F5B868AD4F5D68 /* Pods-app.debug.xcconfig */,
|
||||
3D3D4D80CD734363E3EADE03 /* Pods-app.release.xcconfig */,
|
||||
65D2EDDD9D88DA37994C7743 /* Pods-app-appTests.debug.xcconfig */,
|
||||
AA0AD1B60CF7C90FFDB948B9 /* Pods-app-appTests.release.xcconfig */,
|
||||
8BB4EDB104E125B8A1913E74 /* Pods-app.debug.xcconfig */,
|
||||
53DBA218C184B95B107AC33E /* Pods-app.release.xcconfig */,
|
||||
C01FB6762BC17DADC0319338 /* Pods-app-appTests.debug.xcconfig */,
|
||||
A8E093A0B5DA947150924A68 /* Pods-app-appTests.release.xcconfig */,
|
||||
);
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
@@ -158,11 +158,11 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "appTests" */;
|
||||
buildPhases = (
|
||||
A8646FFAA087C80271BB2CC6 /* [CP] Check Pods Manifest.lock */,
|
||||
A6849EFFDCB214D4F4EAE972 /* [CP] Check Pods Manifest.lock */,
|
||||
00E356EA1AD99517003FC87E /* Sources */,
|
||||
00E356EB1AD99517003FC87E /* Frameworks */,
|
||||
00E356EC1AD99517003FC87E /* Resources */,
|
||||
8DC349844744C81F79F94A1E /* [CP] Copy Pods Resources */,
|
||||
DFA0C2B14E2F369C4B2337CE /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -178,13 +178,13 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "app" */;
|
||||
buildPhases = (
|
||||
AFA1F7779F8B1AF36C3A7914 /* [CP] Check Pods Manifest.lock */,
|
||||
DDF15D430A078CE70E577FDA /* [CP] Check Pods Manifest.lock */,
|
||||
FD10A7F022414F080027D42C /* Start Packager */,
|
||||
13B07F871A680F5B00A75B9A /* Sources */,
|
||||
13B07F8C1A680F5B00A75B9A /* Frameworks */,
|
||||
13B07F8E1A680F5B00A75B9A /* Resources */,
|
||||
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
|
||||
EEF3465D9B31EDDCEB2438EA /* [CP] Copy Pods Resources */,
|
||||
45E56D79C207C80AF89FBAA2 /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -266,24 +266,24 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "set -e\n\nexport NODE_BINARY=node\n../node_modules/react-native/scripts/react-native-xcode.sh\n";
|
||||
};
|
||||
8DC349844744C81F79F94A1E /* [CP] Copy Pods Resources */ = {
|
||||
45E56D79C207C80AF89FBAA2 /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-app-appTests/Pods-app-appTests-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
"${PODS_ROOT}/Target Support Files/Pods-app/Pods-app-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-app-appTests/Pods-app-appTests-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
"${PODS_ROOT}/Target Support Files/Pods-app/Pods-app-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-app-appTests/Pods-app-appTests-resources.sh\"\n";
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-app/Pods-app-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
A8646FFAA087C80271BB2CC6 /* [CP] Check Pods Manifest.lock */ = {
|
||||
A6849EFFDCB214D4F4EAE972 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
@@ -305,7 +305,7 @@
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
AFA1F7779F8B1AF36C3A7914 /* [CP] Check Pods Manifest.lock */ = {
|
||||
DDF15D430A078CE70E577FDA /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
@@ -327,21 +327,21 @@
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
EEF3465D9B31EDDCEB2438EA /* [CP] Copy Pods Resources */ = {
|
||||
DFA0C2B14E2F369C4B2337CE /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-app/Pods-app-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
"${PODS_ROOT}/Target Support Files/Pods-app-appTests/Pods-app-appTests-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-app/Pods-app-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
"${PODS_ROOT}/Target Support Files/Pods-app-appTests/Pods-app-appTests-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-app/Pods-app-resources.sh\"\n";
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-app-appTests/Pods-app-appTests-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
FD10A7F022414F080027D42C /* Start Packager */ = {
|
||||
@@ -396,7 +396,7 @@
|
||||
/* Begin XCBuildConfiguration section */
|
||||
00E356F61AD99517003FC87E /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 65D2EDDD9D88DA37994C7743 /* Pods-app-appTests.debug.xcconfig */;
|
||||
baseConfigurationReference = C01FB6762BC17DADC0319338 /* Pods-app-appTests.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
@@ -424,7 +424,7 @@
|
||||
};
|
||||
00E356F71AD99517003FC87E /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = AA0AD1B60CF7C90FFDB948B9 /* Pods-app-appTests.release.xcconfig */;
|
||||
baseConfigurationReference = A8E093A0B5DA947150924A68 /* Pods-app-appTests.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
@@ -449,7 +449,7 @@
|
||||
};
|
||||
13B07F941A680F5B00A75B9A /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = A79BD0E1A3F5B868AD4F5D68 /* Pods-app.debug.xcconfig */;
|
||||
baseConfigurationReference = 8BB4EDB104E125B8A1913E74 /* Pods-app.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
@@ -479,7 +479,7 @@
|
||||
};
|
||||
13B07F951A680F5B00A75B9A /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 3D3D4D80CD734363E3EADE03 /* Pods-app.release.xcconfig */;
|
||||
baseConfigurationReference = 53DBA218C184B95B107AC33E /* Pods-app.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
jest.mock('@react-native-async-storage/async-storage', () =>
|
||||
require('@react-native-async-storage/async-storage/jest/async-storage-mock'),
|
||||
)
|
||||
@@ -0,0 +1,53 @@
|
||||
import 'react-native-gesture-handler/jestSetup'
|
||||
jest.mock('@react-native-async-storage/async-storage', () =>
|
||||
require('@react-native-async-storage/async-storage/jest/async-storage-mock'),
|
||||
)
|
||||
jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter')
|
||||
|
||||
// Silence the warning: Animated: `useNativeDriver` is not supported
|
||||
jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper')
|
||||
require('react-native-reanimated/lib/reanimated2/jestUtils').setUpTests()
|
||||
|
||||
jest.mock('@fortawesome/react-native-fontawesome', () => ({
|
||||
FontAwesomeIcon: '',
|
||||
}))
|
||||
|
||||
jest.mock('react-native-safe-area-context', () => {
|
||||
const inset = {top: 0, right: 0, bottom: 0, left: 0}
|
||||
return {
|
||||
SafeAreaProvider: jest.fn().mockImplementation(({children}) => children),
|
||||
SafeAreaConsumer: jest
|
||||
.fn()
|
||||
.mockImplementation(({children}) => children(inset)),
|
||||
useSafeAreaInsets: jest.fn().mockImplementation(() => inset),
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('@gorhom/bottom-sheet', () => {
|
||||
const react = require('react-native')
|
||||
return {
|
||||
__esModule: true,
|
||||
default: react.View,
|
||||
namedExport: {
|
||||
...require('react-native-reanimated/mock'),
|
||||
...jest.requireActual('@gorhom/bottom-sheet'),
|
||||
BottomSheetFlatList: react.FlatList,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('rn-fetch-blob', () => ({
|
||||
config: jest.fn().mockReturnThis(),
|
||||
cancel: jest.fn(),
|
||||
fetch: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock('@bam.tech/react-native-image-resizer', () => ({
|
||||
createResizedImage: jest.fn(),
|
||||
}))
|
||||
|
||||
import {View as mockedView} from 'react-native'
|
||||
jest.mock('react-native-tab-view', () => ({
|
||||
...jest.requireActual('react-native-tab-view'),
|
||||
TabView: mockedView,
|
||||
}))
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from 'react'
|
||||
import {render} from '@testing-library/react-native'
|
||||
import {GestureHandlerRootView} from 'react-native-gesture-handler'
|
||||
import {RootSiblingParent} from 'react-native-root-siblings'
|
||||
import {SafeAreaProvider} from 'react-native-safe-area-context'
|
||||
import {RootStoreProvider} from '../src/state'
|
||||
import {mockedRootStore} from '../__mocks__/state-mock'
|
||||
|
||||
const customRender = (ui: any, storeMock?: any) =>
|
||||
render(
|
||||
// eslint-disable-next-line react-native/no-inline-styles
|
||||
<GestureHandlerRootView style={{flex: 1}}>
|
||||
<RootSiblingParent>
|
||||
<RootStoreProvider
|
||||
value={
|
||||
storeMock != null
|
||||
? {...mockedRootStore, ...storeMock}
|
||||
: mockedRootStore
|
||||
}>
|
||||
<SafeAreaProvider>{ui}</SafeAreaProvider>
|
||||
</RootStoreProvider>
|
||||
</RootSiblingParent>
|
||||
</GestureHandlerRootView>,
|
||||
)
|
||||
|
||||
// re-export everything
|
||||
export * from '@testing-library/react-native'
|
||||
|
||||
// override render method
|
||||
export {customRender as render}
|
||||
+22
-2
@@ -8,10 +8,14 @@
|
||||
"web": "react-scripts start",
|
||||
"start": "react-native start",
|
||||
"clean-cache": "rm -rf node_modules/.cache/babel-loader/*",
|
||||
"test": "jest",
|
||||
"test": "jest --coverage",
|
||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.0.6",
|
||||
"@atproto/lexicon": "^0.0.4",
|
||||
"@atproto/xrpc": "^0.0.3",
|
||||
"@bam.tech/react-native-image-resizer": "^3.0.4",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
||||
@@ -35,6 +39,7 @@
|
||||
"react-native": "0.68.2",
|
||||
"react-native-appstate-hook": "^1.0.6",
|
||||
"react-native-gesture-handler": "^2.5.0",
|
||||
"react-native-haptic-feedback": "^1.14.0",
|
||||
"react-native-image-crop-picker": "^0.38.1",
|
||||
"react-native-inappbrowser-reborn": "^3.6.3",
|
||||
"react-native-linear-gradient": "^2.6.2",
|
||||
@@ -51,12 +56,15 @@
|
||||
"react-native-url-polyfill": "^1.3.0",
|
||||
"react-native-version-number": "^0.3.6",
|
||||
"react-native-web": "^0.17.7",
|
||||
"rn-fetch-blob": "^0.12.0",
|
||||
"tlds": "^1.234.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.9",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@react-native-community/eslint-config": "^2.0.0",
|
||||
"@testing-library/jest-native": "^5.3.3",
|
||||
"@testing-library/react-native": "^11.5.0",
|
||||
"@types/he": "^1.1.2",
|
||||
"@types/jest": "^26.0.23",
|
||||
"@types/lodash.chunk": "^4.2.7",
|
||||
@@ -81,7 +89,11 @@
|
||||
"jest": {
|
||||
"preset": "react-native",
|
||||
"setupFiles": [
|
||||
"./jest.js"
|
||||
"./jest/jestSetup.js",
|
||||
"./node_modules/react-native-gesture-handler/jestSetup.js"
|
||||
],
|
||||
"setupFilesAfterEnv": [
|
||||
"@testing-library/jest-native/extend-expect"
|
||||
],
|
||||
"moduleFileExtensions": [
|
||||
"ts",
|
||||
@@ -93,6 +105,14 @@
|
||||
],
|
||||
"transformIgnorePatterns": [
|
||||
"node_modules/(?!(jest-)?react-native|react-clone-referenced-element|@react-native-community|rollbar-react-native|@fortawesome|@react-native|@react-navigation)"
|
||||
],
|
||||
"coveragePathIgnorePatterns": [
|
||||
"<rootDir>/node_modules/",
|
||||
"<rootDir>/src/platform",
|
||||
"<rootDir>/src/third-party",
|
||||
"<rootDir>/src/view/com/util",
|
||||
"<rootDir>/src/state/lib",
|
||||
"<rootDir>/__tests__/test-utils.js"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 122 KiB |
+9
-5
@@ -5,11 +5,13 @@ import {RootSiblingParent} from 'react-native-root-siblings'
|
||||
import {GestureHandlerRootView} from 'react-native-gesture-handler'
|
||||
import SplashScreen from 'react-native-splash-screen'
|
||||
import {SafeAreaProvider} from 'react-native-safe-area-context'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {ThemeProvider} from './view/lib/ThemeContext'
|
||||
import * as view from './view/index'
|
||||
import {RootStoreModel, setupState, RootStoreProvider} from './state'
|
||||
import {MobileShell} from './view/shell/mobile'
|
||||
|
||||
function App() {
|
||||
const App = observer(() => {
|
||||
const [rootStore, setRootStore] = useState<RootStoreModel | undefined>(
|
||||
undefined,
|
||||
)
|
||||
@@ -40,13 +42,15 @@ function App() {
|
||||
<GestureHandlerRootView style={{flex: 1}}>
|
||||
<RootSiblingParent>
|
||||
<RootStoreProvider value={rootStore}>
|
||||
<SafeAreaProvider>
|
||||
<MobileShell />
|
||||
</SafeAreaProvider>
|
||||
<ThemeProvider theme={rootStore.shell.darkMode ? 'dark' : 'light'}>
|
||||
<SafeAreaProvider>
|
||||
<MobileShell />
|
||||
</SafeAreaProvider>
|
||||
</ThemeProvider>
|
||||
</RootStoreProvider>
|
||||
</RootSiblingParent>
|
||||
</GestureHandlerRootView>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export default App
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export const LOGIN_INCLUDE_DEV_SERVERS = true
|
||||
export const TABS_ENABLED = false
|
||||
export const IMAGES_ENABLED = false
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export function choose<U, T extends Record<string, U>>(
|
||||
value: keyof T,
|
||||
choices: T,
|
||||
): U {
|
||||
return choices[value]
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import RNFetchBlob from 'rn-fetch-blob'
|
||||
import ImageResizer from '@bam.tech/react-native-image-resizer'
|
||||
|
||||
export interface DownloadAndResizeOpts {
|
||||
uri: string
|
||||
width: number
|
||||
height: number
|
||||
mode: 'contain' | 'cover' | 'stretch'
|
||||
maxSize: number
|
||||
timeout: number
|
||||
}
|
||||
|
||||
export interface Image {
|
||||
path: string
|
||||
mime: string
|
||||
size: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export async function downloadAndResize(opts: DownloadAndResizeOpts) {
|
||||
let appendExt
|
||||
try {
|
||||
const urip = new URL(opts.uri)
|
||||
const ext = urip.pathname.split('.').pop()
|
||||
if (ext === 'jpg' || ext === 'jpeg') {
|
||||
appendExt = 'jpeg'
|
||||
} else if (ext === 'png') {
|
||||
appendExt = 'png'
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('Invalid URI', opts.uri, e)
|
||||
return
|
||||
}
|
||||
|
||||
let downloadRes
|
||||
try {
|
||||
const downloadResPromise = RNFetchBlob.config({
|
||||
fileCache: true,
|
||||
appendExt,
|
||||
}).fetch('GET', opts.uri)
|
||||
const to1 = setTimeout(() => downloadResPromise.cancel(), opts.timeout)
|
||||
downloadRes = await downloadResPromise
|
||||
clearTimeout(to1)
|
||||
|
||||
let localUri = downloadRes.path()
|
||||
if (!localUri.startsWith('file://')) {
|
||||
localUri = `file://${localUri}`
|
||||
}
|
||||
|
||||
return await resize(localUri, opts)
|
||||
} finally {
|
||||
if (downloadRes) {
|
||||
downloadRes.flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface ResizeOpts {
|
||||
width: number
|
||||
height: number
|
||||
mode: 'contain' | 'cover' | 'stretch'
|
||||
maxSize: number
|
||||
}
|
||||
|
||||
export async function resize(
|
||||
localUri: string,
|
||||
opts: ResizeOpts,
|
||||
): Promise<Image> {
|
||||
for (let i = 0; i < 9; i++) {
|
||||
const quality = 1.0 - i / 10
|
||||
const resizeRes = await ImageResizer.createResizedImage(
|
||||
localUri,
|
||||
opts.width,
|
||||
opts.height,
|
||||
'JPEG',
|
||||
quality,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{mode: opts.mode},
|
||||
)
|
||||
if (resizeRes.size < opts.maxSize) {
|
||||
return {
|
||||
path: resizeRes.path,
|
||||
mime: 'image/jpeg',
|
||||
size: resizeRes.size,
|
||||
width: resizeRes.width,
|
||||
height: resizeRes.height,
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`This image is too big! We couldn't compress it down to ${opts.maxSize} bytes`,
|
||||
)
|
||||
}
|
||||
|
||||
export async function compressIfNeeded(
|
||||
img: Image,
|
||||
maxSize: number,
|
||||
): Promise<Image> {
|
||||
const origUri = `file://${img.path}`
|
||||
if (img.size < maxSize) {
|
||||
return img
|
||||
}
|
||||
return await resize(origUri, {
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
mode: 'stretch',
|
||||
maxSize,
|
||||
})
|
||||
}
|
||||
+14
-4
@@ -22,9 +22,13 @@ export interface LinkMeta {
|
||||
url: string
|
||||
title?: string
|
||||
description?: string
|
||||
image?: string
|
||||
}
|
||||
|
||||
export async function getLinkMeta(url: string): Promise<LinkMeta> {
|
||||
export async function getLinkMeta(
|
||||
url: string,
|
||||
timeout = 5e3,
|
||||
): Promise<LinkMeta> {
|
||||
if (isBskyAppUrl(url)) {
|
||||
// TODO this could be better
|
||||
url = convertBskyAppUrlIfNeeded(url)
|
||||
@@ -57,17 +61,23 @@ export async function getLinkMeta(url: string): Promise<LinkMeta> {
|
||||
}
|
||||
|
||||
try {
|
||||
const httpRes = await fetch(url)
|
||||
const controller = new AbortController()
|
||||
const to = setTimeout(() => controller.abort(), timeout || 5e3)
|
||||
const httpRes = await fetch(url, {
|
||||
headers: {accept: 'text/html'},
|
||||
signal: controller.signal,
|
||||
})
|
||||
const httpResBody = await httpRes.text()
|
||||
clearTimeout(to)
|
||||
const httpResMeta = extractHtmlMeta(httpResBody)
|
||||
meta.title = httpResMeta.title ? he.decode(httpResMeta.title) : undefined
|
||||
meta.description = httpResMeta.description
|
||||
? he.decode(httpResMeta.description)
|
||||
: undefined
|
||||
// TODO meta.image = httpResMeta.image
|
||||
meta.image = httpResMeta.image
|
||||
} catch (e) {
|
||||
// failed
|
||||
console.log(e)
|
||||
console.error(e)
|
||||
meta.error = 'Failed to fetch link'
|
||||
}
|
||||
|
||||
|
||||
+27
-26
@@ -1,5 +1,6 @@
|
||||
import {AtUri} from '../third-party/uri'
|
||||
import {Entity} from '../third-party/api/src/client/types/app/bsky/feed/post'
|
||||
import {AppBskyFeedPost} from '@atproto/api'
|
||||
type Entity = AppBskyFeedPost.Entity
|
||||
import {PROD_SERVICE} from '../state'
|
||||
import {isNetworkError} from './errors'
|
||||
import TLDs from 'tlds'
|
||||
@@ -22,7 +23,7 @@ export function makeRecordUri(
|
||||
collection: string,
|
||||
rkey: string,
|
||||
) {
|
||||
const urip = new AtUri(`at://host/`)
|
||||
const urip = new AtUri('at://host/')
|
||||
urip.host = didOrName
|
||||
urip.collection = collection
|
||||
urip.rkey = rkey
|
||||
@@ -62,7 +63,9 @@ export function ago(date: number | string | Date): string {
|
||||
export function isValidDomain(str: string): boolean {
|
||||
return !!TLDs.find(tld => {
|
||||
let i = str.lastIndexOf(tld)
|
||||
if (i === -1) return false
|
||||
if (i === -1) {
|
||||
return false
|
||||
}
|
||||
return str.charAt(i - 1) === '.' && i === str.length - tld.length
|
||||
})
|
||||
}
|
||||
@@ -75,27 +78,25 @@ export function extractEntities(
|
||||
let ents: Entity[] = []
|
||||
{
|
||||
// mentions
|
||||
const re = /(^|\s|\()(@)([a-zA-Z0-9\.-]+)(\b)/dg
|
||||
const re = /(^|\s|\()(@)([a-zA-Z0-9\.-]+)(\b)/g
|
||||
while ((match = re.exec(text))) {
|
||||
if (knownHandles && !knownHandles.has(match[3])) {
|
||||
continue // not a known handle
|
||||
} else if (!match[3].includes('.')) {
|
||||
continue // probably not a handle
|
||||
}
|
||||
const start = text.indexOf(match[3], match.index) - 1
|
||||
ents.push({
|
||||
type: 'mention',
|
||||
value: match[3],
|
||||
index: {
|
||||
start: match.indices[2][0], // skip the (^|\s) but include the '@'
|
||||
end: match.indices[3][1],
|
||||
},
|
||||
index: {start, end: start + match[3].length + 1},
|
||||
})
|
||||
}
|
||||
}
|
||||
{
|
||||
// links
|
||||
const re =
|
||||
/(^|\s|\()((https?:\/\/[\S]+)|((?<domain>[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*))/dgm
|
||||
/(^|\s|\()((https?:\/\/[\S]+)|((?<domain>[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*))/gm
|
||||
while ((match = re.exec(text))) {
|
||||
let value = match[2]
|
||||
if (!value.startsWith('http')) {
|
||||
@@ -105,20 +106,16 @@ export function extractEntities(
|
||||
}
|
||||
value = `https://${value}`
|
||||
}
|
||||
const index = {
|
||||
start: match.indices[2][0], // skip the (^|\s)
|
||||
end: match.indices[2][1],
|
||||
const start = text.indexOf(match[2], match.index)
|
||||
const index = {start, end: start + match[2].length}
|
||||
// strip ending puncuation
|
||||
if (/[.,;!?]$/.test(value)) {
|
||||
value = value.slice(0, -1)
|
||||
index.end--
|
||||
}
|
||||
{
|
||||
// strip ending puncuation
|
||||
if (/[.,;!?]$/.test(value)) {
|
||||
value = value.slice(0, -1)
|
||||
index.end--
|
||||
}
|
||||
if (/[)]$/.test(value) && !value.includes('(')) {
|
||||
value = value.slice(0, -1)
|
||||
index.end--
|
||||
}
|
||||
if (/[)]$/.test(value) && !value.includes('(')) {
|
||||
value = value.slice(0, -1)
|
||||
index.end--
|
||||
}
|
||||
ents.push({
|
||||
type: 'link',
|
||||
@@ -188,8 +185,8 @@ export function makeValidHandle(str: string): string {
|
||||
}
|
||||
|
||||
export function createFullHandle(name: string, domain: string): string {
|
||||
name = name.replace(/[\.]+$/, '')
|
||||
domain = domain.replace(/^[\.]+/, '')
|
||||
name = (name || '').replace(/[\.]+$/, '')
|
||||
domain = (domain || '').replace(/^[\.]+/, '')
|
||||
return `${name}.${domain}`
|
||||
}
|
||||
|
||||
@@ -201,7 +198,11 @@ export function enforceLen(str: string, len: number): string {
|
||||
return str
|
||||
}
|
||||
|
||||
export function cleanError(str: string): string {
|
||||
export function cleanError(str: any): string {
|
||||
if (!str) return str
|
||||
if (typeof str !== 'string') {
|
||||
str = str.toString()
|
||||
}
|
||||
if (isNetworkError(str)) {
|
||||
return 'Unable to connect. Please check your internet connection and try again.'
|
||||
}
|
||||
@@ -259,7 +260,7 @@ export function convertBskyAppUrlIfNeeded(url: string): string {
|
||||
const urlp = new URL(url)
|
||||
return urlp.pathname
|
||||
} catch (e) {
|
||||
console.log('Unexpected error in convertBskyAppUrlIfNeeded()', e)
|
||||
console.error('Unexpected error in convertBskyAppUrlIfNeeded()', e)
|
||||
}
|
||||
}
|
||||
return url
|
||||
|
||||
+10
-9
@@ -1,11 +1,12 @@
|
||||
import {autorun} from 'mobx'
|
||||
import {sessionClient as AtpApi} from '../third-party/api'
|
||||
import type {SessionServiceClient} from '../third-party/api/src/index'
|
||||
import {Platform} from 'react-native'
|
||||
import {sessionClient as AtpApi, SessionServiceClient} from '@atproto/api'
|
||||
import {RootStoreModel} from './models/root-store'
|
||||
import * as libapi from './lib/api'
|
||||
import * as storage from './lib/storage'
|
||||
|
||||
export const LOCAL_DEV_SERVICE = 'http://localhost:2583'
|
||||
export const LOCAL_DEV_SERVICE =
|
||||
Platform.OS === 'ios' ? 'http://localhost:2583' : 'http://10.0.2.2:2583'
|
||||
export const STAGING_SERVICE = 'https://pds.staging.bsky.dev'
|
||||
export const PROD_SERVICE = 'https://bsky.social'
|
||||
export const DEFAULT_SERVICE = PROD_SERVICE
|
||||
@@ -22,20 +23,20 @@ export async function setupState() {
|
||||
rootStore = new RootStoreModel(api)
|
||||
try {
|
||||
data = (await storage.load(ROOT_STATE_STORAGE_KEY)) || {}
|
||||
rootStore.log.debug('Initial hydrate', {hasSession: !!data.session})
|
||||
rootStore.hydrate(data)
|
||||
} catch (e) {
|
||||
console.error('Failed to load state from storage', e)
|
||||
} catch (e: any) {
|
||||
rootStore.log.error('Failed to load state from storage', e)
|
||||
}
|
||||
|
||||
console.log('Initial hydrate', rootStore.me)
|
||||
rootStore.session
|
||||
.connect()
|
||||
.then(() => {
|
||||
console.log('Session connected', rootStore.me)
|
||||
rootStore.log.debug('Session connected')
|
||||
return rootStore.fetchStateUpdate()
|
||||
})
|
||||
.catch(e => {
|
||||
console.log('Failed initial connect', e)
|
||||
.catch((e: any) => {
|
||||
rootStore.log.warn('Failed initial connect', e)
|
||||
})
|
||||
// @ts-ignore .on() is correct -prf
|
||||
api.sessionManager.on('session', () => {
|
||||
|
||||
+133
-70
@@ -4,13 +4,18 @@
|
||||
*/
|
||||
|
||||
// import {ReactNativeStore} from './auth'
|
||||
import {sessionClient as AtpApi} from '../../third-party/api'
|
||||
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 {
|
||||
sessionClient as AtpApi,
|
||||
APP_BSKY_GRAPH,
|
||||
AppBskyEmbedImages,
|
||||
AppBskyEmbedExternal,
|
||||
} from '@atproto/api'
|
||||
import {AtUri} from '../../third-party/uri'
|
||||
import {APP_BSKY_GRAPH} from '../../third-party/api'
|
||||
import {RootStoreModel} from '../models/root-store'
|
||||
import {extractEntities} from '../../lib/strings'
|
||||
import {isNetworkError} from '../../lib/errors'
|
||||
import {downloadAndResize} from '../../lib/images'
|
||||
import {getLikelyType, LikelyType, getLinkMeta} from '../../lib/link-meta'
|
||||
|
||||
const TIMEOUT = 10e3 // 10s
|
||||
|
||||
@@ -21,12 +26,113 @@ export function doPolyfill() {
|
||||
export async function post(
|
||||
store: RootStoreModel,
|
||||
text: string,
|
||||
replyTo?: Post.ReplyRef,
|
||||
replyTo?: string,
|
||||
images?: string[],
|
||||
knownHandles?: Set<string>,
|
||||
onStateChange?: (state: string) => void,
|
||||
) {
|
||||
let embed: AppBskyEmbedImages.Main | AppBskyEmbedExternal.Main | undefined
|
||||
let reply
|
||||
|
||||
onStateChange?.('Processing...')
|
||||
const entities = extractEntities(text, knownHandles)
|
||||
if (entities) {
|
||||
for (const ent of entities) {
|
||||
if (ent.type === 'mention') {
|
||||
const prof = await store.profiles.getProfile(ent.value)
|
||||
ent.value = prof.data.did
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (images?.length) {
|
||||
embed = {
|
||||
$type: 'app.bsky.embed.images',
|
||||
images: [],
|
||||
} as AppBskyEmbedImages.Main
|
||||
let i = 1
|
||||
for (const image of images) {
|
||||
onStateChange?.(`Uploading image #${i++}...`)
|
||||
const res = await store.api.com.atproto.blob.upload(
|
||||
image, // this will be special-cased by the fetch monkeypatch in /src/state/lib/api.ts
|
||||
{encoding: 'image/jpeg'},
|
||||
)
|
||||
embed.images.push({
|
||||
image: {
|
||||
cid: res.data.cid,
|
||||
mimeType: 'image/jpeg',
|
||||
},
|
||||
alt: '', // TODO supply alt text
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (!embed && entities) {
|
||||
const link = entities.find(
|
||||
ent =>
|
||||
ent.type === 'link' &&
|
||||
getLikelyType(ent.value || '') === LikelyType.HTML,
|
||||
)
|
||||
if (link) {
|
||||
try {
|
||||
onStateChange?.(`Fetching link metadata...`)
|
||||
let thumb
|
||||
const linkMeta = await getLinkMeta(link.value)
|
||||
if (linkMeta.image) {
|
||||
onStateChange?.(`Downloading link thumbnail...`)
|
||||
const thumbLocal = await downloadAndResize({
|
||||
uri: linkMeta.image,
|
||||
width: 250,
|
||||
height: 250,
|
||||
mode: 'contain',
|
||||
maxSize: 100000,
|
||||
timeout: 15e3,
|
||||
}).catch(() => undefined)
|
||||
if (thumbLocal) {
|
||||
onStateChange?.(`Uploading link thumbnail...`)
|
||||
let encoding
|
||||
if (thumbLocal.uri.endsWith('.png')) {
|
||||
encoding = 'image/png'
|
||||
} else if (
|
||||
thumbLocal.uri.endsWith('.jpeg') ||
|
||||
thumbLocal.uri.endsWith('.jpg')
|
||||
) {
|
||||
encoding = 'image/jpeg'
|
||||
} else {
|
||||
store.log.warn(
|
||||
'Unexpected image format for thumbnail, skipping',
|
||||
thumbLocal.uri,
|
||||
)
|
||||
}
|
||||
if (encoding) {
|
||||
const thumbUploadRes = await store.api.com.atproto.blob.upload(
|
||||
thumbLocal.uri, // this will be special-cased by the fetch monkeypatch in /src/state/lib/api.ts
|
||||
{encoding},
|
||||
)
|
||||
thumb = {
|
||||
cid: thumbUploadRes.data.cid,
|
||||
mimeType: encoding,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
embed = {
|
||||
$type: 'app.bsky.embed.external',
|
||||
external: {
|
||||
uri: link.value,
|
||||
title: linkMeta.title || linkMeta.url,
|
||||
description: linkMeta.description || '',
|
||||
thumb,
|
||||
},
|
||||
} as AppBskyEmbedExternal.Main
|
||||
} catch (e: any) {
|
||||
store.log.warn(`Failed to fetch link meta for ${link.value}`, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (replyTo) {
|
||||
const replyToUrip = new AtUri(replyTo.uri)
|
||||
const replyToUrip = new AtUri(replyTo)
|
||||
const parentPost = await store.api.app.bsky.feed.post.get({
|
||||
user: replyToUrip.host,
|
||||
rkey: replyToUrip.rkey,
|
||||
@@ -42,24 +148,29 @@ export async function post(
|
||||
}
|
||||
}
|
||||
}
|
||||
const entities = extractEntities(text, knownHandles)
|
||||
if (entities) {
|
||||
for (const ent of entities) {
|
||||
if (ent.type === 'mention') {
|
||||
const prof = await store.profiles.getProfile(ent.value)
|
||||
ent.value = prof.data.did
|
||||
}
|
||||
|
||||
try {
|
||||
onStateChange?.(`Posting...`)
|
||||
return await store.api.app.bsky.feed.post.create(
|
||||
{did: store.me.did || ''},
|
||||
{
|
||||
text,
|
||||
reply,
|
||||
embed,
|
||||
entities,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
)
|
||||
} catch (e: any) {
|
||||
console.error(`Failed to create post: ${e.toString()}`)
|
||||
if (isNetworkError(e)) {
|
||||
throw new Error(
|
||||
'Post failed to upload. Please check your Internet connection and try again.',
|
||||
)
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
return await store.api.app.bsky.feed.post.create(
|
||||
{did: store.me.did || ''},
|
||||
{
|
||||
text,
|
||||
reply,
|
||||
entities,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export async function repost(store: RootStoreModel, uri: string, cid: string) {
|
||||
@@ -105,54 +216,6 @@ export async function unfollow(store: RootStoreModel, followUri: string) {
|
||||
})
|
||||
}
|
||||
|
||||
export async function inviteToScene(
|
||||
store: RootStoreModel,
|
||||
sceneDid: string,
|
||||
subjectDid: string,
|
||||
subjectDeclarationCid: string,
|
||||
): Promise<string> {
|
||||
const res = await store.api.app.bsky.graph.assertion.create(
|
||||
{
|
||||
did: sceneDid,
|
||||
},
|
||||
{
|
||||
subject: {
|
||||
did: subjectDid,
|
||||
declarationCid: subjectDeclarationCid,
|
||||
},
|
||||
assertion: APP_BSKY_GRAPH.AssertMember,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
)
|
||||
return res.uri
|
||||
}
|
||||
|
||||
interface Confirmation {
|
||||
originator: {
|
||||
did: string
|
||||
declarationCid: string
|
||||
}
|
||||
assertion: {
|
||||
uri: string
|
||||
cid: string
|
||||
}
|
||||
}
|
||||
export async function acceptSceneInvite(
|
||||
store: RootStoreModel,
|
||||
details: Confirmation,
|
||||
): Promise<string> {
|
||||
const res = await store.api.app.bsky.graph.confirmation.create(
|
||||
{
|
||||
did: store.me.did || '',
|
||||
},
|
||||
{
|
||||
...details,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
)
|
||||
return res.uri
|
||||
}
|
||||
|
||||
interface FetchHandlerResponse {
|
||||
status: number
|
||||
headers: Record<string, string>
|
||||
|
||||
+231
-193
@@ -1,184 +1,170 @@
|
||||
import {makeAutoObservable, runInAction} from 'mobx'
|
||||
import {Record as PostRecord} from '../../third-party/api/src/client/types/app/bsky/feed/post'
|
||||
import * as GetTimeline from '../../third-party/api/src/client/types/app/bsky/feed/getTimeline'
|
||||
import * as ActorRef from '../../third-party/api/src/client/types/app/bsky/actor/ref'
|
||||
import * as GetAuthorFeed from '../../third-party/api/src/client/types/app/bsky/feed/getAuthorFeed'
|
||||
import {PostThreadViewModel} from './post-thread-view'
|
||||
import {
|
||||
AppBskyFeedGetTimeline as GetTimeline,
|
||||
AppBskyFeedFeedViewPost,
|
||||
AppBskyFeedPost,
|
||||
AppBskyFeedGetAuthorFeed as GetAuthorFeed,
|
||||
} from '@atproto/api'
|
||||
type FeedViewPost = AppBskyFeedFeedViewPost.Main
|
||||
type ReasonRepost = AppBskyFeedFeedViewPost.ReasonRepost
|
||||
type PostView = AppBskyFeedPost.View
|
||||
import {AtUri} from '../../third-party/uri'
|
||||
import {RootStoreModel} from './root-store'
|
||||
import * as apilib from '../lib/api'
|
||||
import {cleanError} from '../../lib/strings'
|
||||
import {isObj, hasProp} from '../lib/type-guards'
|
||||
|
||||
const PAGE_SIZE = 30
|
||||
|
||||
type FeedItem = GetTimeline.FeedItem | GetAuthorFeed.FeedItem
|
||||
type FeedItemWithThreadMeta = FeedItem & {
|
||||
let _idCounter = 0
|
||||
|
||||
type FeedViewPostWithThreadMeta = FeedViewPost & {
|
||||
_isThreadParent?: boolean
|
||||
_isThreadChildElided?: boolean
|
||||
_isThreadChild?: boolean
|
||||
}
|
||||
|
||||
export class FeedItemMyStateModel {
|
||||
repost?: string
|
||||
upvote?: string
|
||||
downvote?: string
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this)
|
||||
}
|
||||
}
|
||||
|
||||
export class FeedItemModel implements GetTimeline.FeedItem {
|
||||
export class FeedItemModel {
|
||||
// ui state
|
||||
_reactKey: string = ''
|
||||
_isThreadParent: boolean = false
|
||||
_isThreadChildElided: boolean = false
|
||||
_isThreadChild: boolean = false
|
||||
_hideParent: boolean = true // used to avoid dup post rendering while showing some parents
|
||||
|
||||
// data
|
||||
uri: string = ''
|
||||
cid: string = ''
|
||||
author: ActorRef.WithInfo = {
|
||||
did: '',
|
||||
handle: '',
|
||||
displayName: '',
|
||||
declaration: {cid: '', actorType: ''},
|
||||
avatar: undefined,
|
||||
}
|
||||
repostedBy?: ActorRef.WithInfo
|
||||
trendedBy?: ActorRef.WithInfo
|
||||
record: Record<string, unknown> = {}
|
||||
replyCount: number = 0
|
||||
repostCount: number = 0
|
||||
upvoteCount: number = 0
|
||||
downvoteCount: number = 0
|
||||
indexedAt: string = ''
|
||||
myState = new FeedItemMyStateModel()
|
||||
|
||||
// additional data
|
||||
additionalParentPost?: PostThreadViewModel
|
||||
post: PostView
|
||||
postRecord?: AppBskyFeedPost.Record
|
||||
reply?: FeedViewPost['reply']
|
||||
replyParent?: FeedItemModel
|
||||
reason?: FeedViewPost['reason']
|
||||
|
||||
constructor(
|
||||
public rootStore: RootStoreModel,
|
||||
reactKey: string,
|
||||
v: FeedItemWithThreadMeta,
|
||||
v: FeedViewPostWithThreadMeta,
|
||||
) {
|
||||
makeAutoObservable(this, {rootStore: false})
|
||||
this._reactKey = reactKey
|
||||
this.copy(v)
|
||||
this.post = v.post
|
||||
if (AppBskyFeedPost.isRecord(this.post.record)) {
|
||||
const valid = AppBskyFeedPost.validateRecord(this.post.record)
|
||||
if (valid.success) {
|
||||
this.postRecord = this.post.record
|
||||
} else {
|
||||
rootStore.log.warn(
|
||||
'Received an invalid app.bsky.feed.post record',
|
||||
valid.error,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
rootStore.log.warn(
|
||||
'app.bsky.feed.getTimeline or app.bsky.feed.getAuthorFeed served an unexpected record type',
|
||||
this.post.record,
|
||||
)
|
||||
}
|
||||
this.reply = v.reply
|
||||
if (v.reply?.parent) {
|
||||
this.replyParent = new FeedItemModel(rootStore, '', {
|
||||
post: v.reply.parent,
|
||||
})
|
||||
}
|
||||
this.reason = v.reason
|
||||
this._isThreadParent = v._isThreadParent || false
|
||||
this._isThreadChild = v._isThreadChild || false
|
||||
this._isThreadChildElided = v._isThreadChildElided || false
|
||||
makeAutoObservable(this, {rootStore: false})
|
||||
}
|
||||
|
||||
copy(v: GetTimeline.FeedItem | GetAuthorFeed.FeedItem) {
|
||||
this.uri = v.uri
|
||||
this.cid = v.cid
|
||||
this.author = v.author
|
||||
this.repostedBy = v.repostedBy
|
||||
this.trendedBy = v.trendedBy
|
||||
this.record = v.record
|
||||
this.replyCount = v.replyCount
|
||||
this.repostCount = v.repostCount
|
||||
this.upvoteCount = v.upvoteCount
|
||||
this.downvoteCount = v.downvoteCount
|
||||
this.indexedAt = v.indexedAt
|
||||
if (v.myState) {
|
||||
this.myState.upvote = v.myState.upvote
|
||||
this.myState.downvote = v.myState.downvote
|
||||
this.myState.repost = v.myState.repost
|
||||
copy(v: FeedViewPost) {
|
||||
this.post = v.post
|
||||
this.reply = v.reply
|
||||
if (v.reply?.parent) {
|
||||
this.replyParent = new FeedItemModel(this.rootStore, '', {
|
||||
post: v.reply.parent,
|
||||
})
|
||||
} else {
|
||||
this.replyParent = undefined
|
||||
}
|
||||
this.reason = v.reason
|
||||
}
|
||||
|
||||
get reasonRepost(): ReasonRepost | undefined {
|
||||
if (this.reason?.$type === 'app.bsky.feed.feedViewPost#reasonRepost') {
|
||||
return this.reason as ReasonRepost
|
||||
}
|
||||
}
|
||||
|
||||
async toggleUpvote() {
|
||||
const wasUpvoted = !!this.myState.upvote
|
||||
const wasDownvoted = !!this.myState.downvote
|
||||
const wasUpvoted = !!this.post.viewer.upvote
|
||||
const wasDownvoted = !!this.post.viewer.downvote
|
||||
const res = await this.rootStore.api.app.bsky.feed.setVote({
|
||||
subject: {
|
||||
uri: this.uri,
|
||||
cid: this.cid,
|
||||
uri: this.post.uri,
|
||||
cid: this.post.cid,
|
||||
},
|
||||
direction: wasUpvoted ? 'none' : 'up',
|
||||
})
|
||||
runInAction(() => {
|
||||
if (wasDownvoted) {
|
||||
this.downvoteCount--
|
||||
this.post.downvoteCount--
|
||||
}
|
||||
if (wasUpvoted) {
|
||||
this.upvoteCount--
|
||||
this.post.upvoteCount--
|
||||
} else {
|
||||
this.upvoteCount++
|
||||
this.post.upvoteCount++
|
||||
}
|
||||
this.myState.upvote = res.data.upvote
|
||||
this.myState.downvote = res.data.downvote
|
||||
this.post.viewer.upvote = res.data.upvote
|
||||
this.post.viewer.downvote = res.data.downvote
|
||||
})
|
||||
}
|
||||
|
||||
async toggleDownvote() {
|
||||
const wasUpvoted = !!this.myState.upvote
|
||||
const wasDownvoted = !!this.myState.downvote
|
||||
const wasUpvoted = !!this.post.viewer.upvote
|
||||
const wasDownvoted = !!this.post.viewer.downvote
|
||||
const res = await this.rootStore.api.app.bsky.feed.setVote({
|
||||
subject: {
|
||||
uri: this.uri,
|
||||
cid: this.cid,
|
||||
uri: this.post.uri,
|
||||
cid: this.post.cid,
|
||||
},
|
||||
direction: wasDownvoted ? 'none' : 'down',
|
||||
})
|
||||
runInAction(() => {
|
||||
if (wasUpvoted) {
|
||||
this.upvoteCount--
|
||||
this.post.upvoteCount--
|
||||
}
|
||||
if (wasDownvoted) {
|
||||
this.downvoteCount--
|
||||
this.post.downvoteCount--
|
||||
} else {
|
||||
this.downvoteCount++
|
||||
this.post.downvoteCount++
|
||||
}
|
||||
this.myState.upvote = res.data.upvote
|
||||
this.myState.downvote = res.data.downvote
|
||||
this.post.viewer.upvote = res.data.upvote
|
||||
this.post.viewer.downvote = res.data.downvote
|
||||
})
|
||||
}
|
||||
|
||||
async toggleRepost() {
|
||||
if (this.myState.repost) {
|
||||
await apilib.unrepost(this.rootStore, this.myState.repost)
|
||||
if (this.post.viewer.repost) {
|
||||
await apilib.unrepost(this.rootStore, this.post.viewer.repost)
|
||||
runInAction(() => {
|
||||
this.repostCount--
|
||||
this.myState.repost = undefined
|
||||
this.post.repostCount--
|
||||
this.post.viewer.repost = undefined
|
||||
})
|
||||
} else {
|
||||
const res = await apilib.repost(this.rootStore, this.uri, this.cid)
|
||||
const res = await apilib.repost(
|
||||
this.rootStore,
|
||||
this.post.uri,
|
||||
this.post.cid,
|
||||
)
|
||||
runInAction(() => {
|
||||
this.repostCount++
|
||||
this.myState.repost = res.uri
|
||||
this.post.repostCount++
|
||||
this.post.viewer.repost = res.uri
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async delete() {
|
||||
await this.rootStore.api.app.bsky.feed.post.delete({
|
||||
did: this.author.did,
|
||||
rkey: new AtUri(this.uri).rkey,
|
||||
})
|
||||
}
|
||||
|
||||
get needsAdditionalData() {
|
||||
if (
|
||||
(this.record as PostRecord).reply?.parent?.uri &&
|
||||
!this._isThreadChild
|
||||
) {
|
||||
return !this.additionalParentPost
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async fetchAdditionalData() {
|
||||
if (!this.needsAdditionalData) {
|
||||
return
|
||||
}
|
||||
this.additionalParentPost = new PostThreadViewModel(this.rootStore, {
|
||||
uri: (this.record as PostRecord).reply?.parent.uri,
|
||||
depth: 0,
|
||||
})
|
||||
await this.additionalParentPost.setup().catch(e => {
|
||||
console.error('Failed to load post needed by notification', e)
|
||||
did: this.post.author.did,
|
||||
rkey: new AtUri(this.post.uri).rkey,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -237,12 +223,11 @@ export class FeedModel {
|
||||
|
||||
get nonReplyFeed() {
|
||||
return this.feed.filter(
|
||||
post =>
|
||||
!post.record.reply || // not a reply
|
||||
!!post.repostedBy || // or a repost
|
||||
!!post.trendedBy || // or a trend
|
||||
post._isThreadParent || // but allow if it's a thread by the user
|
||||
post._isThreadChild,
|
||||
item =>
|
||||
!item.reply || // not a reply
|
||||
((item._isThreadParent || // but allow if it's a thread by the user
|
||||
item._isThreadChild) &&
|
||||
item.reply?.root.author.did === item.post.author.did),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -274,7 +259,7 @@ export class FeedModel {
|
||||
* Reset and load
|
||||
*/
|
||||
async refresh() {
|
||||
return this.setup(true)
|
||||
await this.setup(true)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -326,10 +311,15 @@ export class FeedModel {
|
||||
}
|
||||
await this._pendingWork()
|
||||
const res = await this._getFeed({limit: 1})
|
||||
this.setHasNewLatest(
|
||||
res.data.feed[0] &&
|
||||
(this.feed.length === 0 || res.data.feed[0].uri !== this.pollCursor),
|
||||
const currentLatestUri = this.pollCursor
|
||||
const receivedLatestUri = res.data.feed[0]
|
||||
? res.data.feed[0].post.uri
|
||||
: undefined
|
||||
const hasNewLatest = Boolean(
|
||||
receivedLatestUri &&
|
||||
(this.feed.length === 0 || receivedLatestUri !== currentLatestUri),
|
||||
)
|
||||
this.setHasNewLatest(hasNewLatest)
|
||||
}
|
||||
|
||||
// state transitions
|
||||
@@ -341,11 +331,14 @@ export class FeedModel {
|
||||
this.error = ''
|
||||
}
|
||||
|
||||
private _xIdle(err: string = '') {
|
||||
private _xIdle(err?: any) {
|
||||
this.isLoading = false
|
||||
this.isRefreshing = false
|
||||
this.hasLoaded = true
|
||||
this.error = cleanError(err)
|
||||
this.error = err ? cleanError(err.toString()) : ''
|
||||
if (err) {
|
||||
this.rootStore.log.error('Posts feed request failed', err)
|
||||
}
|
||||
}
|
||||
|
||||
// loader functions
|
||||
@@ -373,7 +366,7 @@ export class FeedModel {
|
||||
await this._replaceAll(res)
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
this._xIdle(e.toString())
|
||||
this._xIdle(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,12 +377,12 @@ export class FeedModel {
|
||||
await this._prependAll(res)
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
this._xIdle(e.toString())
|
||||
this._xIdle(e)
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadMore() {
|
||||
if (!this.hasMore) {
|
||||
if (!this.hasMore || this.hasError) {
|
||||
return
|
||||
}
|
||||
this._xLoading()
|
||||
@@ -401,7 +394,7 @@ export class FeedModel {
|
||||
await this._appendAll(res)
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
this._xIdle(`Failed to load feed: ${e.toString()}`)
|
||||
this._xIdle(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,19 +416,20 @@ export class FeedModel {
|
||||
}
|
||||
this._updateAll(res)
|
||||
numToFetch -= res.data.feed.length
|
||||
cursor = this.feed[res.data.feed.length - 1].indexedAt
|
||||
console.log(numToFetch, cursor, res.data.feed.length)
|
||||
cursor = this.feed[res.data.feed.length - 1]
|
||||
? ts(this.feed[res.data.feed.length - 1])
|
||||
: undefined
|
||||
} while (numToFetch > 0)
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
this._xIdle(`Failed to update feed: ${e.toString()}`)
|
||||
this._xIdle(e)
|
||||
}
|
||||
}
|
||||
|
||||
private async _replaceAll(
|
||||
res: GetTimeline.Response | GetAuthorFeed.Response,
|
||||
) {
|
||||
this.pollCursor = res.data.feed[0]?.uri
|
||||
this.pollCursor = res.data.feed[0]?.post.uri
|
||||
return this._appendAll(res, true)
|
||||
}
|
||||
|
||||
@@ -445,73 +439,46 @@ export class FeedModel {
|
||||
) {
|
||||
this.loadMoreCursor = res.data.cursor
|
||||
this.hasMore = !!this.loadMoreCursor
|
||||
let counter = this.feed.length
|
||||
|
||||
// HACK 1
|
||||
// rearrange the posts to represent threads
|
||||
// (should be done on the server)
|
||||
// -prf
|
||||
// HACK 2
|
||||
// deduplicate posts on the home feed
|
||||
// (should be done on the server)
|
||||
// -prf
|
||||
const reorgedFeed = preprocessFeed(res.data.feed, this.feedType === 'home')
|
||||
const reorgedFeed = preprocessFeed(res.data.feed)
|
||||
|
||||
const promises = []
|
||||
const toAppend: FeedItemModel[] = []
|
||||
for (const item of reorgedFeed) {
|
||||
const itemModel = new FeedItemModel(
|
||||
this.rootStore,
|
||||
`item-${counter++}`,
|
||||
`item-${_idCounter++}`,
|
||||
item,
|
||||
)
|
||||
if (itemModel.needsAdditionalData) {
|
||||
promises.push(
|
||||
itemModel.fetchAdditionalData().catch(e => {
|
||||
console.error('Failure during feed-view _appendAll()', e)
|
||||
}),
|
||||
)
|
||||
}
|
||||
toAppend.push(itemModel)
|
||||
}
|
||||
await Promise.all(promises)
|
||||
runInAction(() => {
|
||||
if (replace) {
|
||||
this.feed = toAppend
|
||||
} else {
|
||||
this.feed = this.feed.concat(toAppend)
|
||||
}
|
||||
dedupParents(this.feed)
|
||||
})
|
||||
}
|
||||
|
||||
private async _prependAll(
|
||||
res: GetTimeline.Response | GetAuthorFeed.Response,
|
||||
) {
|
||||
this.pollCursor = res.data.feed[0]?.uri
|
||||
let counter = this.feed.length
|
||||
this.pollCursor = res.data.feed[0]?.post.uri
|
||||
|
||||
const promises = []
|
||||
const toPrepend: FeedItemModel[] = []
|
||||
for (const item of res.data.feed) {
|
||||
if (this.feed.find(item2 => item2.uri === item.uri)) {
|
||||
if (this.feed.find(item2 => item2.post.uri === item.post.uri)) {
|
||||
break // stop here - we've hit a post we already have
|
||||
}
|
||||
|
||||
const itemModel = new FeedItemModel(
|
||||
this.rootStore,
|
||||
`item-${counter++}`,
|
||||
`item-${_idCounter++}`,
|
||||
item,
|
||||
)
|
||||
if (itemModel.needsAdditionalData) {
|
||||
promises.push(
|
||||
itemModel.fetchAdditionalData().catch(e => {
|
||||
console.error('Failure during feed-view _prependAll()', e)
|
||||
}),
|
||||
)
|
||||
}
|
||||
toPrepend.push(itemModel)
|
||||
}
|
||||
await Promise.all(promises)
|
||||
runInAction(() => {
|
||||
this.feed = toPrepend.concat(this.feed)
|
||||
})
|
||||
@@ -520,10 +487,11 @@ export class FeedModel {
|
||||
private _updateAll(res: GetTimeline.Response | GetAuthorFeed.Response) {
|
||||
for (const item of res.data.feed) {
|
||||
const existingItem = this.feed.find(
|
||||
// this find function has a key subtley- the indexedAt comparison
|
||||
// the reason for this is reposts: they set the URI of the original post, not of the repost record
|
||||
// the indexedAt time will be for the repost however, so we use that to help us
|
||||
item2 => item.uri === item2.uri && item.indexedAt === item2.indexedAt,
|
||||
// HACK: need to find the reposts' item, so we have to check for that -prf
|
||||
item2 =>
|
||||
item.post.uri === item2.post.uri &&
|
||||
// @ts-ignore todo
|
||||
item.reason?.by?.did === item2.reason?.by?.did,
|
||||
)
|
||||
if (existingItem) {
|
||||
existingItem.copy(item)
|
||||
@@ -547,23 +515,23 @@ export class FeedModel {
|
||||
}
|
||||
}
|
||||
|
||||
function preprocessFeed(
|
||||
feed: FeedItem[],
|
||||
dedup: boolean,
|
||||
): FeedItemWithThreadMeta[] {
|
||||
const reorg: FeedItemWithThreadMeta[] = []
|
||||
for (let i = feed.length - 1; i >= 0; i--) {
|
||||
const item = feed[i] as FeedItemWithThreadMeta
|
||||
interface Slice {
|
||||
index: number
|
||||
length: number
|
||||
}
|
||||
function preprocessFeed(feed: FeedViewPost[]): FeedViewPostWithThreadMeta[] {
|
||||
const reorg: FeedViewPostWithThreadMeta[] = []
|
||||
|
||||
if (dedup) {
|
||||
if (reorg.find(item2 => item2.uri === item.uri)) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
// phase one: identify threads and reorganize them into the feed so
|
||||
// that they are in order and marked as part of a thread
|
||||
for (let i = feed.length - 1; i >= 0; i--) {
|
||||
const item = feed[i] as FeedViewPostWithThreadMeta
|
||||
|
||||
const selfReplyUri = getSelfReplyUri(item)
|
||||
if (selfReplyUri) {
|
||||
const parentIndex = reorg.findIndex(item2 => item2.uri === selfReplyUri)
|
||||
const parentIndex = reorg.findIndex(
|
||||
item2 => item2.post.uri === selfReplyUri,
|
||||
)
|
||||
if (parentIndex !== -1 && !reorg[parentIndex]._isThreadParent) {
|
||||
reorg[parentIndex]._isThreadParent = true
|
||||
item._isThreadChild = true
|
||||
@@ -573,23 +541,93 @@ function preprocessFeed(
|
||||
}
|
||||
reorg.unshift(item)
|
||||
}
|
||||
|
||||
// phase two: identify the positions of the threads
|
||||
let activeSlice = -1
|
||||
let threadSlices: Slice[] = []
|
||||
for (let i = 0; i < reorg.length; i++) {
|
||||
const item = reorg[i] as FeedViewPostWithThreadMeta
|
||||
if (activeSlice === -1) {
|
||||
if (item._isThreadParent) {
|
||||
activeSlice = i
|
||||
}
|
||||
} else {
|
||||
if (!item._isThreadChild) {
|
||||
threadSlices.push({index: activeSlice, length: i - activeSlice})
|
||||
if (item._isThreadParent) {
|
||||
activeSlice = i
|
||||
} else {
|
||||
activeSlice = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (activeSlice !== -1) {
|
||||
threadSlices.push({index: activeSlice, length: reorg.length - activeSlice})
|
||||
}
|
||||
|
||||
// phase three: reorder the feed so that the timestamp of the
|
||||
// last post in a thread establishes its ordering
|
||||
for (const slice of threadSlices) {
|
||||
const removed: FeedViewPostWithThreadMeta[] = reorg.splice(
|
||||
slice.index,
|
||||
slice.length,
|
||||
)
|
||||
const targetDate = new Date(ts(removed[removed.length - 1]))
|
||||
let newIndex = reorg.findIndex(item => new Date(ts(item)) < targetDate)
|
||||
if (newIndex === -1) {
|
||||
newIndex = reorg.length
|
||||
}
|
||||
reorg.splice(newIndex, 0, ...removed)
|
||||
slice.index = newIndex
|
||||
}
|
||||
|
||||
// phase four: compress any threads that are longer than 3 posts
|
||||
let removedCount = 0
|
||||
for (const slice of threadSlices) {
|
||||
if (slice.length > 3) {
|
||||
reorg.splice(slice.index - removedCount + 1, slice.length - 3)
|
||||
reorg[slice.index - removedCount]._isThreadChildElided = true
|
||||
removedCount += slice.length - 3
|
||||
}
|
||||
}
|
||||
|
||||
return reorg
|
||||
}
|
||||
|
||||
function getSelfReplyUri(
|
||||
item: GetTimeline.FeedItem | GetAuthorFeed.FeedItem,
|
||||
): string | undefined {
|
||||
if (
|
||||
isObj(item.record) &&
|
||||
hasProp(item.record, 'reply') &&
|
||||
isObj(item.record.reply) &&
|
||||
hasProp(item.record.reply, 'parent') &&
|
||||
isObj(item.record.reply.parent) &&
|
||||
hasProp(item.record.reply.parent, 'uri') &&
|
||||
typeof item.record.reply.parent.uri === 'string'
|
||||
) {
|
||||
if (new AtUri(item.record.reply.parent.uri).host === item.author.did) {
|
||||
return item.record.reply.parent.uri
|
||||
function dedupParents(feed: FeedItemModel[]) {
|
||||
// only show parents that aren't already in the feed
|
||||
// NOTE if a parent post arrives in a followup loadmore, it'll show when we otherwise wish it wouldnt -prf
|
||||
for (let i = 0; i < feed.length; i++) {
|
||||
const item1 = feed[i]
|
||||
if (!item1.replyParent || item1._isThreadChild) {
|
||||
continue
|
||||
}
|
||||
let hideParent = false
|
||||
for (let j = 0; j < feed.length; j++) {
|
||||
const item2 = feed[j]
|
||||
if (
|
||||
item1.replyParent.post.uri === item2.post.uri || // the post itself is there
|
||||
(j < i && item1.replyParent.post.uri === item2.replyParent?.post.uri) // another reply already showed it
|
||||
) {
|
||||
hideParent = true
|
||||
break
|
||||
}
|
||||
}
|
||||
item1._hideParent = hideParent
|
||||
}
|
||||
}
|
||||
|
||||
function getSelfReplyUri(item: FeedViewPost): string | undefined {
|
||||
return item.reply?.parent.author.did === item.post.author.did
|
||||
? item.reply?.parent.uri
|
||||
: undefined
|
||||
}
|
||||
|
||||
function ts(item: FeedViewPost | FeedItemModel): string {
|
||||
if (item.reason?.indexedAt) {
|
||||
// @ts-ignore need better type checks
|
||||
return item.reason.indexedAt
|
||||
}
|
||||
return item.post.indexedAt
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import {makeAutoObservable} from 'mobx'
|
||||
import * as GetAssertions from '../../third-party/api/src/client/types/app/bsky/graph/getAssertions'
|
||||
import {AppBskyGraphGetAssertions as GetAssertions} from '@atproto/api'
|
||||
import {RootStoreModel} from './root-store'
|
||||
|
||||
type ResponseAssertion = GetAssertions.OutputSchema['assertions'][number]
|
||||
export type Assertion = ResponseAssertion & {
|
||||
export type Assertion = GetAssertions.Assertion & {
|
||||
_reactKey: string
|
||||
}
|
||||
|
||||
@@ -81,11 +80,14 @@ export class GetAssertionsView {
|
||||
this.error = ''
|
||||
}
|
||||
|
||||
private _xIdle(err: string = '') {
|
||||
private _xIdle(err?: any) {
|
||||
this.isLoading = false
|
||||
this.isRefreshing = false
|
||||
this.hasLoaded = true
|
||||
this.error = err
|
||||
this.error = err ? err.toString() : ''
|
||||
if (err) {
|
||||
this.rootStore.log.error('Failed to fetch assertions', err)
|
||||
}
|
||||
}
|
||||
|
||||
// loader functions
|
||||
@@ -100,7 +102,7 @@ export class GetAssertionsView {
|
||||
this._replaceAll(res)
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
this._xIdle(e.toString())
|
||||
this._xIdle(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import {makeAutoObservable} from 'mobx'
|
||||
import {XRPCError, XRPCInvalidResponseError} from '@atproto/xrpc'
|
||||
import {isObj, hasProp} from '../lib/type-guards'
|
||||
|
||||
interface LogEntry {
|
||||
id: string
|
||||
type?: string
|
||||
summary?: string
|
||||
details?: string
|
||||
ts?: number
|
||||
}
|
||||
|
||||
let _lastTs: string
|
||||
let _lastId: string
|
||||
function genId(): string {
|
||||
let candidate = String(Date.now())
|
||||
if (_lastTs === candidate) {
|
||||
const id = _lastId + 'x'
|
||||
_lastId = id
|
||||
return id
|
||||
}
|
||||
_lastTs = candidate
|
||||
_lastId = candidate
|
||||
return candidate
|
||||
}
|
||||
|
||||
export class LogModel {
|
||||
entries: LogEntry[] = []
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this, {serialize: false, hydrate: false})
|
||||
}
|
||||
|
||||
serialize(): unknown {
|
||||
return {
|
||||
entries: this.entries.slice(-100),
|
||||
}
|
||||
}
|
||||
|
||||
hydrate(v: unknown) {
|
||||
if (isObj(v)) {
|
||||
if (hasProp(v, 'entries') && Array.isArray(v.entries)) {
|
||||
this.entries = v.entries.filter(
|
||||
e => isObj(e) && hasProp(e, 'id') && typeof e.id === 'string',
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private add(entry: LogEntry) {
|
||||
this.entries.push(entry)
|
||||
}
|
||||
|
||||
debug(summary: string, details?: any) {
|
||||
details = detailsToStr(details)
|
||||
console.debug(summary, details || '')
|
||||
this.add({
|
||||
id: genId(),
|
||||
type: 'debug',
|
||||
summary,
|
||||
details,
|
||||
ts: Date.now(),
|
||||
})
|
||||
}
|
||||
|
||||
warn(summary: string, details?: any) {
|
||||
details = detailsToStr(details)
|
||||
console.debug(summary, details || '')
|
||||
this.add({
|
||||
id: genId(),
|
||||
type: 'warn',
|
||||
summary,
|
||||
details,
|
||||
ts: Date.now(),
|
||||
})
|
||||
}
|
||||
|
||||
error(summary: string, details?: any) {
|
||||
details = detailsToStr(details)
|
||||
console.debug(summary, details || '')
|
||||
this.add({
|
||||
id: genId(),
|
||||
type: 'error',
|
||||
summary,
|
||||
details,
|
||||
ts: Date.now(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function detailsToStr(details?: any) {
|
||||
if (details && typeof details !== 'string') {
|
||||
if (
|
||||
details instanceof XRPCInvalidResponseError ||
|
||||
details.constructor.name === 'XRPCInvalidResponseError'
|
||||
) {
|
||||
return `The server gave an ill-formatted response.\nMethod: ${
|
||||
details.lexiconNsid
|
||||
}.\nError: ${details.validationError.toString()}`
|
||||
} else if (
|
||||
details instanceof XRPCError ||
|
||||
details.constructor.name === 'XRPCError'
|
||||
) {
|
||||
return `An XRPC error occurred.\nStatus: ${details.status}\nError: ${details.error}\nMessage: ${details.message}`
|
||||
} else if (details instanceof Error) {
|
||||
return details.toString()
|
||||
}
|
||||
return JSON.stringify(details, null, 2)
|
||||
}
|
||||
return details
|
||||
}
|
||||
+16
-15
@@ -1,6 +1,6 @@
|
||||
import {makeAutoObservable, runInAction} from 'mobx'
|
||||
import {RootStoreModel} from './root-store'
|
||||
import {MembershipsViewModel} from './memberships-view'
|
||||
import {FeedModel} from './feed-view'
|
||||
import {NotificationsViewModel} from './notifications-view'
|
||||
import {isObj, hasProp} from '../lib/type-guards'
|
||||
|
||||
@@ -11,7 +11,7 @@ export class MeModel {
|
||||
description: string = ''
|
||||
avatar: string = ''
|
||||
notificationCount: number = 0
|
||||
memberships?: MembershipsViewModel
|
||||
mainFeed: FeedModel
|
||||
notifications: NotificationsViewModel
|
||||
|
||||
constructor(public rootStore: RootStoreModel) {
|
||||
@@ -20,6 +20,9 @@ export class MeModel {
|
||||
{rootStore: false, serialize: false, hydrate: false},
|
||||
{autoBind: true},
|
||||
)
|
||||
this.mainFeed = new FeedModel(this.rootStore, 'home', {
|
||||
algorithm: 'reverse-chronological',
|
||||
})
|
||||
this.notifications = new NotificationsViewModel(this.rootStore, {})
|
||||
}
|
||||
|
||||
@@ -30,7 +33,6 @@ export class MeModel {
|
||||
this.description = ''
|
||||
this.avatar = ''
|
||||
this.notificationCount = 0
|
||||
this.memberships = undefined
|
||||
}
|
||||
|
||||
serialize(): unknown {
|
||||
@@ -90,15 +92,18 @@ export class MeModel {
|
||||
this.avatar = ''
|
||||
}
|
||||
})
|
||||
this.memberships = new MembershipsViewModel(this.rootStore, {
|
||||
actor: this.did,
|
||||
})
|
||||
await this.memberships?.setup().catch(e => {
|
||||
console.error('Failed to setup memberships model', e)
|
||||
})
|
||||
await this.notifications.setup().catch(e => {
|
||||
console.error('Failed to setup notifications model', e)
|
||||
this.mainFeed = new FeedModel(this.rootStore, 'home', {
|
||||
algorithm: 'reverse-chronological',
|
||||
})
|
||||
this.notifications = new NotificationsViewModel(this.rootStore, {})
|
||||
await Promise.all([
|
||||
this.mainFeed.setup().catch(e => {
|
||||
this.rootStore.log.error('Failed to setup main feed model', e)
|
||||
}),
|
||||
this.notifications.setup().catch(e => {
|
||||
this.rootStore.log.error('Failed to setup notifications model', e)
|
||||
}),
|
||||
])
|
||||
} else {
|
||||
this.clear()
|
||||
}
|
||||
@@ -119,8 +124,4 @@ export class MeModel {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async refreshMemberships() {
|
||||
return this.memberships?.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
import {makeAutoObservable, runInAction} from 'mobx'
|
||||
import * as GetMembers from '../../third-party/api/src/client/types/app/bsky/graph/getMembers'
|
||||
import * as ActorRef from '../../third-party/api/src/client/types/app/bsky/actor/ref'
|
||||
import {APP_BSKY_GRAPH} from '../../third-party/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: 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.getMembers(
|
||||
this.params,
|
||||
)
|
||||
this._replaceAll(res)
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
this._xIdle(`Failed to load feed: ${e.toString()}`)
|
||||
}
|
||||
}
|
||||
|
||||
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,122 +0,0 @@
|
||||
import {makeAutoObservable} from 'mobx'
|
||||
import * as GetMemberships from '../../third-party/api/src/client/types/app/bsky/graph/getMemberships'
|
||||
import * as ActorRef from '../../third-party/api/src/client/types/app/bsky/actor/ref'
|
||||
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: 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.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)
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,35 @@
|
||||
import {makeAutoObservable, runInAction} from 'mobx'
|
||||
import * as ListNotifications from '../../third-party/api/src/client/types/app/bsky/notification/list'
|
||||
import * as ActorRef from '../../third-party/api/src/client/types/app/bsky/actor/ref'
|
||||
import {
|
||||
AppBskyNotificationList as ListNotifications,
|
||||
AppBskyActorRef as ActorRef,
|
||||
AppBskyFeedPost,
|
||||
AppBskyFeedRepost,
|
||||
AppBskyFeedVote,
|
||||
AppBskyGraphAssertion,
|
||||
AppBskyGraphFollow,
|
||||
} from '@atproto/api'
|
||||
import {RootStoreModel} from './root-store'
|
||||
import {PostThreadViewModel} from './post-thread-view'
|
||||
import {hasProp} from '../lib/type-guards'
|
||||
import {APP_BSKY_GRAPH} from '../../third-party/api'
|
||||
import {cleanError} from '../../lib/strings'
|
||||
|
||||
const UNGROUPABLE_REASONS = ['trend', 'assertion']
|
||||
const UNGROUPABLE_REASONS = ['assertion']
|
||||
const PAGE_SIZE = 30
|
||||
const MS_60MIN = 1e3 * 60 * 60
|
||||
|
||||
let _idCounter = 0
|
||||
|
||||
export interface GroupedNotification extends ListNotifications.Notification {
|
||||
additional?: ListNotifications.Notification[]
|
||||
}
|
||||
|
||||
export class NotificationsViewItemModel implements GroupedNotification {
|
||||
type SupportedRecord =
|
||||
| AppBskyFeedPost.Record
|
||||
| AppBskyFeedRepost.Record
|
||||
| AppBskyFeedVote.Record
|
||||
| AppBskyGraphAssertion.Record
|
||||
| AppBskyGraphFollow.Record
|
||||
|
||||
export class NotificationsViewItemModel {
|
||||
// ui state
|
||||
_reactKey: string = ''
|
||||
|
||||
@@ -25,11 +39,12 @@ export class NotificationsViewItemModel implements GroupedNotification {
|
||||
author: ActorRef.WithInfo = {
|
||||
did: '',
|
||||
handle: '',
|
||||
avatar: '',
|
||||
declaration: {cid: '', actorType: ''},
|
||||
}
|
||||
reason: string = ''
|
||||
reasonSubject?: string
|
||||
record: any = {}
|
||||
record?: SupportedRecord
|
||||
isRead: boolean = false
|
||||
indexedAt: string = ''
|
||||
additional?: NotificationsViewItemModel[]
|
||||
@@ -53,7 +68,7 @@ export class NotificationsViewItemModel implements GroupedNotification {
|
||||
this.author = v.author
|
||||
this.reason = v.reason
|
||||
this.reasonSubject = v.reasonSubject
|
||||
this.record = v.record
|
||||
this.record = this.toSupportedRecord(v.record)
|
||||
this.isRead = v.isRead
|
||||
this.indexedAt = v.indexedAt
|
||||
if (v.additional?.length) {
|
||||
@@ -76,10 +91,6 @@ export class NotificationsViewItemModel implements GroupedNotification {
|
||||
return this.reason === 'repost'
|
||||
}
|
||||
|
||||
get isTrend() {
|
||||
return this.reason === 'trend'
|
||||
}
|
||||
|
||||
get isMention() {
|
||||
return this.reason === 'mention'
|
||||
}
|
||||
@@ -97,37 +108,53 @@ export class NotificationsViewItemModel implements GroupedNotification {
|
||||
}
|
||||
|
||||
get needsAdditionalData() {
|
||||
if (
|
||||
this.isUpvote ||
|
||||
this.isRepost ||
|
||||
this.isTrend ||
|
||||
this.isReply ||
|
||||
this.isMention
|
||||
) {
|
||||
if (this.isUpvote || this.isRepost || this.isReply || this.isMention) {
|
||||
return !this.additionalPost
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
get isInvite() {
|
||||
return (
|
||||
this.isAssertion && this.record.assertion === APP_BSKY_GRAPH.AssertMember
|
||||
)
|
||||
}
|
||||
|
||||
get subjectUri() {
|
||||
get subjectUri(): string {
|
||||
if (this.reasonSubject) {
|
||||
return this.reasonSubject
|
||||
}
|
||||
const record = this.record
|
||||
if (
|
||||
hasProp(this.record, 'subject') &&
|
||||
typeof this.record.subject === 'string'
|
||||
AppBskyFeedRepost.isRecord(record) ||
|
||||
AppBskyFeedVote.isRecord(record)
|
||||
) {
|
||||
return this.record.subject
|
||||
return record.subject.uri
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
toSupportedRecord(v: unknown): SupportedRecord | undefined {
|
||||
for (const ns of [
|
||||
AppBskyFeedPost,
|
||||
AppBskyFeedRepost,
|
||||
AppBskyFeedVote,
|
||||
AppBskyGraphAssertion,
|
||||
AppBskyGraphFollow,
|
||||
]) {
|
||||
if (ns.isRecord(v)) {
|
||||
const valid = ns.validateRecord(v)
|
||||
if (valid.success) {
|
||||
return v
|
||||
} else {
|
||||
this.rootStore.log.warn('Received an invalid record', {
|
||||
record: v,
|
||||
error: valid.error,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
this.rootStore.log.warn(
|
||||
'app.bsky.notifications.list served an unsupported record type',
|
||||
v,
|
||||
)
|
||||
}
|
||||
|
||||
async fetchAdditionalData() {
|
||||
if (!this.needsAdditionalData) {
|
||||
return
|
||||
@@ -135,7 +162,7 @@ export class NotificationsViewItemModel implements GroupedNotification {
|
||||
let postUri
|
||||
if (this.isReply || this.isMention) {
|
||||
postUri = this.uri
|
||||
} else if (this.isUpvote || this.isRead || this.isTrend) {
|
||||
} else if (this.isUpvote || this.isRepost) {
|
||||
postUri = this.subjectUri
|
||||
}
|
||||
if (postUri) {
|
||||
@@ -144,7 +171,10 @@ export class NotificationsViewItemModel implements GroupedNotification {
|
||||
depth: 0,
|
||||
})
|
||||
await this.additionalPost.setup().catch(e => {
|
||||
console.error('Failed to load post needed by notification', e)
|
||||
this.rootStore.log.error(
|
||||
'Failed to load post needed by notification',
|
||||
e,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -257,8 +287,8 @@ export class NotificationsViewModel {
|
||||
seenAt: new Date().toISOString(),
|
||||
})
|
||||
this.rootStore.me.clearNotificationCount()
|
||||
} catch (e) {
|
||||
console.log('Failed to update notifications read state', e)
|
||||
} catch (e: any) {
|
||||
this.rootStore.log.warn('Failed to update notifications read state', e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,11 +301,15 @@ export class NotificationsViewModel {
|
||||
this.error = ''
|
||||
}
|
||||
|
||||
private _xIdle(err: string = '') {
|
||||
private _xIdle(err?: any) {
|
||||
this.isLoading = false
|
||||
this.isRefreshing = false
|
||||
this.hasLoaded = true
|
||||
this.error = cleanError(err)
|
||||
this.error = err ? cleanError(err) : ''
|
||||
if (err) {
|
||||
this.rootStore.log.error('Failed to fetch notifications', err)
|
||||
}
|
||||
}
|
||||
|
||||
// loader functions
|
||||
@@ -303,7 +337,7 @@ export class NotificationsViewModel {
|
||||
await this._replaceAll(res)
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
this._xIdle(`Failed to load notifications: ${e.toString()}`)
|
||||
this._xIdle(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,7 +355,7 @@ export class NotificationsViewModel {
|
||||
await this._appendAll(res)
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
this._xIdle(`Failed to load notifications: ${e.toString()}`)
|
||||
this._xIdle(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,11 +379,10 @@ export class NotificationsViewModel {
|
||||
this._updateAll(res)
|
||||
numToFetch -= res.data.notifications.length
|
||||
cursor = this.notifications[res.data.notifications.length - 1].indexedAt
|
||||
console.log(numToFetch, cursor, res.data.notifications.length)
|
||||
} while (numToFetch > 0)
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
this._xIdle(`Failed to update notifications: ${e.toString()}`)
|
||||
this._xIdle(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,13 +393,12 @@ export class NotificationsViewModel {
|
||||
private async _appendAll(res: ListNotifications.Response, replace = false) {
|
||||
this.loadMoreCursor = res.data.cursor
|
||||
this.hasMore = !!this.loadMoreCursor
|
||||
let counter = this.notifications.length
|
||||
const promises = []
|
||||
const itemModels: NotificationsViewItemModel[] = []
|
||||
for (const item of groupNotifications(res.data.notifications)) {
|
||||
const itemModel = new NotificationsViewItemModel(
|
||||
this.rootStore,
|
||||
`item-${counter++}`,
|
||||
`item-${_idCounter++}`,
|
||||
item,
|
||||
)
|
||||
if (itemModel.needsAdditionalData) {
|
||||
@@ -375,7 +407,7 @@ export class NotificationsViewModel {
|
||||
itemModels.push(itemModel)
|
||||
}
|
||||
await Promise.all(promises).catch(e => {
|
||||
console.error(
|
||||
this.rootStore.log.error(
|
||||
'Uncaught failure during notifications-view _appendAll()',
|
||||
e,
|
||||
)
|
||||
|
||||
@@ -1,20 +1,12 @@
|
||||
import {makeAutoObservable, runInAction} from 'mobx'
|
||||
import {AppBskyFeedGetPostThread as GetPostThread} from '../../third-party/api'
|
||||
import * as Embed from '../../third-party/api/src/client/types/app/bsky/feed/embed'
|
||||
import * as ActorRef from '../../third-party/api/src/client/types/app/bsky/actor/ref'
|
||||
import {
|
||||
AppBskyFeedGetPostThread as GetPostThread,
|
||||
AppBskyFeedPost as FeedPost,
|
||||
} from '@atproto/api'
|
||||
import {AtUri} from '../../third-party/uri'
|
||||
import _omit from 'lodash.omit'
|
||||
import {RootStoreModel} from './root-store'
|
||||
import * as apilib from '../lib/api'
|
||||
|
||||
type MaybePost =
|
||||
| GetPostThread.Post
|
||||
| GetPostThread.NotFoundPost
|
||||
| {
|
||||
$type: string
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
function* reactKeyGenerator(): Generator<string> {
|
||||
let counter = 0
|
||||
while (true) {
|
||||
@@ -22,199 +14,164 @@ function* reactKeyGenerator(): Generator<string> {
|
||||
}
|
||||
}
|
||||
|
||||
interface ReplyingTo {
|
||||
author: {
|
||||
handle: string
|
||||
displayName?: string
|
||||
avatar?: string
|
||||
}
|
||||
text: string
|
||||
}
|
||||
interface OriginalRecord {
|
||||
text: string
|
||||
}
|
||||
|
||||
export class PostThreadViewPostMyStateModel {
|
||||
repost?: string
|
||||
upvote?: string
|
||||
downvote?: string
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this)
|
||||
}
|
||||
}
|
||||
|
||||
export class PostThreadViewPostModel implements GetPostThread.Post {
|
||||
export class PostThreadViewPostModel {
|
||||
// ui state
|
||||
_reactKey: string = ''
|
||||
_depth = 0
|
||||
_isHighlightedPost = false
|
||||
_hasMore = false
|
||||
|
||||
// data
|
||||
$type: string = ''
|
||||
uri: string = ''
|
||||
cid: string = ''
|
||||
author: ActorRef.WithInfo = {
|
||||
did: '',
|
||||
handle: '',
|
||||
declaration: {cid: '', actorType: ''},
|
||||
}
|
||||
record: Record<string, unknown> = {}
|
||||
embed?: Embed.Main = undefined
|
||||
parent?: PostThreadViewPostModel
|
||||
replyCount: number = 0
|
||||
replies?: PostThreadViewPostModel[]
|
||||
repostCount: number = 0
|
||||
upvoteCount: number = 0
|
||||
downvoteCount: number = 0
|
||||
indexedAt: string = ''
|
||||
myState = new PostThreadViewPostMyStateModel()
|
||||
|
||||
// added data
|
||||
replyingTo?: ReplyingTo
|
||||
post: FeedPost.View
|
||||
postRecord?: FeedPost.Record
|
||||
parent?: PostThreadViewPostModel | GetPostThread.NotFoundPost
|
||||
replies?: (PostThreadViewPostModel | GetPostThread.NotFoundPost)[]
|
||||
|
||||
constructor(
|
||||
public rootStore: RootStoreModel,
|
||||
reactKey: string,
|
||||
v?: GetPostThread.Post,
|
||||
v: GetPostThread.ThreadViewPost,
|
||||
) {
|
||||
makeAutoObservable(this, {rootStore: false})
|
||||
this._reactKey = reactKey
|
||||
if (v) {
|
||||
Object.assign(this, _omit(v, 'parent', 'replies', 'myState')) // replies and parent are handled via assignTreeModels
|
||||
if (v.myState) {
|
||||
Object.assign(this.myState, v.myState)
|
||||
this.post = v.post
|
||||
if (FeedPost.isRecord(this.post.record)) {
|
||||
const valid = FeedPost.validateRecord(this.post.record)
|
||||
if (valid.success) {
|
||||
this.postRecord = this.post.record
|
||||
} else {
|
||||
rootStore.log.warn(
|
||||
'Received an invalid app.bsky.feed.post record',
|
||||
valid.error,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
rootStore.log.warn(
|
||||
'app.bsky.feed.getPostThread served an unexpected record type',
|
||||
this.post.record,
|
||||
)
|
||||
}
|
||||
// replies and parent are handled via assignTreeModels
|
||||
makeAutoObservable(this, {rootStore: false})
|
||||
}
|
||||
|
||||
assignTreeModels(
|
||||
keyGen: Generator<string>,
|
||||
v: GetPostThread.Post,
|
||||
v: GetPostThread.ThreadViewPost,
|
||||
includeParent = true,
|
||||
includeChildren = true,
|
||||
isFirstChild = true,
|
||||
) {
|
||||
// parents
|
||||
if (includeParent && v.parent) {
|
||||
// TODO: validate .record
|
||||
const parentModel = new PostThreadViewPostModel(
|
||||
this.rootStore,
|
||||
keyGen.next().value,
|
||||
v.parent,
|
||||
)
|
||||
parentModel._depth = this._depth - 1
|
||||
if (v.parent.parent) {
|
||||
parentModel.assignTreeModels(keyGen, v.parent, true, false)
|
||||
}
|
||||
this.parent = parentModel
|
||||
}
|
||||
if (!includeParent && v.parent?.author.handle && !isFirstChild) {
|
||||
this.replyingTo = {
|
||||
author: {
|
||||
handle: v.parent.author.handle,
|
||||
displayName: v.parent.author.displayName,
|
||||
avatar: v.parent.author.avatar,
|
||||
},
|
||||
text: (v.parent.record as OriginalRecord).text,
|
||||
if (GetPostThread.isThreadViewPost(v.parent)) {
|
||||
const parentModel = new PostThreadViewPostModel(
|
||||
this.rootStore,
|
||||
keyGen.next().value,
|
||||
v.parent,
|
||||
)
|
||||
parentModel._depth = this._depth - 1
|
||||
if (v.parent.parent) {
|
||||
parentModel.assignTreeModels(keyGen, v.parent, true, false)
|
||||
}
|
||||
this.parent = parentModel
|
||||
} else if (GetPostThread.isNotFoundPost(v.parent)) {
|
||||
this.parent = v.parent
|
||||
}
|
||||
}
|
||||
// replies
|
||||
if (includeChildren && v.replies) {
|
||||
const replies = []
|
||||
let isChildFirstChild = true
|
||||
for (const item of v.replies) {
|
||||
// TODO: validate .record
|
||||
const itemModel = new PostThreadViewPostModel(
|
||||
this.rootStore,
|
||||
keyGen.next().value,
|
||||
item,
|
||||
)
|
||||
itemModel._depth = this._depth + 1
|
||||
if (item.replies) {
|
||||
itemModel.assignTreeModels(
|
||||
keyGen,
|
||||
if (GetPostThread.isThreadViewPost(item)) {
|
||||
const itemModel = new PostThreadViewPostModel(
|
||||
this.rootStore,
|
||||
keyGen.next().value,
|
||||
item,
|
||||
false,
|
||||
true,
|
||||
isChildFirstChild,
|
||||
)
|
||||
itemModel._depth = this._depth + 1
|
||||
if (item.replies) {
|
||||
itemModel.assignTreeModels(keyGen, item, false, true)
|
||||
}
|
||||
replies.push(itemModel)
|
||||
} else if (GetPostThread.isNotFoundPost(item)) {
|
||||
replies.push(item)
|
||||
}
|
||||
isChildFirstChild = false
|
||||
replies.push(itemModel)
|
||||
}
|
||||
this.replies = replies
|
||||
}
|
||||
}
|
||||
|
||||
async toggleUpvote() {
|
||||
const wasUpvoted = !!this.myState.upvote
|
||||
const wasDownvoted = !!this.myState.downvote
|
||||
const wasUpvoted = !!this.post.viewer.upvote
|
||||
const wasDownvoted = !!this.post.viewer.downvote
|
||||
const res = await this.rootStore.api.app.bsky.feed.setVote({
|
||||
subject: {
|
||||
uri: this.uri,
|
||||
cid: this.cid,
|
||||
uri: this.post.uri,
|
||||
cid: this.post.cid,
|
||||
},
|
||||
direction: wasUpvoted ? 'none' : 'up',
|
||||
})
|
||||
runInAction(() => {
|
||||
if (wasDownvoted) {
|
||||
this.downvoteCount--
|
||||
this.post.downvoteCount--
|
||||
}
|
||||
if (wasUpvoted) {
|
||||
this.upvoteCount--
|
||||
this.post.upvoteCount--
|
||||
} else {
|
||||
this.upvoteCount++
|
||||
this.post.upvoteCount++
|
||||
}
|
||||
this.myState.upvote = res.data.upvote
|
||||
this.myState.downvote = res.data.downvote
|
||||
this.post.viewer.upvote = res.data.upvote
|
||||
this.post.viewer.downvote = res.data.downvote
|
||||
})
|
||||
}
|
||||
|
||||
async toggleDownvote() {
|
||||
const wasUpvoted = !!this.myState.upvote
|
||||
const wasDownvoted = !!this.myState.downvote
|
||||
const wasUpvoted = !!this.post.viewer.upvote
|
||||
const wasDownvoted = !!this.post.viewer.downvote
|
||||
const res = await this.rootStore.api.app.bsky.feed.setVote({
|
||||
subject: {
|
||||
uri: this.uri,
|
||||
cid: this.cid,
|
||||
uri: this.post.uri,
|
||||
cid: this.post.cid,
|
||||
},
|
||||
direction: wasDownvoted ? 'none' : 'down',
|
||||
})
|
||||
runInAction(() => {
|
||||
if (wasUpvoted) {
|
||||
this.upvoteCount--
|
||||
this.post.upvoteCount--
|
||||
}
|
||||
if (wasDownvoted) {
|
||||
this.downvoteCount--
|
||||
this.post.downvoteCount--
|
||||
} else {
|
||||
this.downvoteCount++
|
||||
this.post.downvoteCount++
|
||||
}
|
||||
this.myState.upvote = res.data.upvote
|
||||
this.myState.downvote = res.data.downvote
|
||||
this.post.viewer.upvote = res.data.upvote
|
||||
this.post.viewer.downvote = res.data.downvote
|
||||
})
|
||||
}
|
||||
|
||||
async toggleRepost() {
|
||||
if (this.myState.repost) {
|
||||
await apilib.unrepost(this.rootStore, this.myState.repost)
|
||||
if (this.post.viewer.repost) {
|
||||
await apilib.unrepost(this.rootStore, this.post.viewer.repost)
|
||||
runInAction(() => {
|
||||
this.repostCount--
|
||||
this.myState.repost = undefined
|
||||
this.post.repostCount--
|
||||
this.post.viewer.repost = undefined
|
||||
})
|
||||
} else {
|
||||
const res = await apilib.repost(this.rootStore, this.uri, this.cid)
|
||||
const res = await apilib.repost(
|
||||
this.rootStore,
|
||||
this.post.uri,
|
||||
this.post.cid,
|
||||
)
|
||||
runInAction(() => {
|
||||
this.repostCount++
|
||||
this.myState.repost = res.uri
|
||||
this.post.repostCount++
|
||||
this.post.viewer.repost = res.uri
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async delete() {
|
||||
await this.rootStore.api.app.bsky.feed.post.delete({
|
||||
did: this.author.did,
|
||||
rkey: new AtUri(this.uri).rkey,
|
||||
did: this.post.author.did,
|
||||
rkey: new AtUri(this.post.uri).rkey,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -299,11 +256,14 @@ export class PostThreadViewModel {
|
||||
this.notFound = false
|
||||
}
|
||||
|
||||
private _xIdle(err: any = undefined) {
|
||||
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 post thread', err)
|
||||
}
|
||||
this.notFound = err instanceof GetPostThread.NotFoundError
|
||||
}
|
||||
|
||||
@@ -313,7 +273,11 @@ export class PostThreadViewModel {
|
||||
private async _resolveUri() {
|
||||
const urip = new AtUri(this.params.uri)
|
||||
if (!urip.host.startsWith('did:')) {
|
||||
urip.host = await this.rootStore.resolveName(urip.host)
|
||||
try {
|
||||
urip.host = await this.rootStore.resolveName(urip.host)
|
||||
} catch (e: any) {
|
||||
this.error = e.toString()
|
||||
}
|
||||
}
|
||||
runInAction(() => {
|
||||
this.resolvedUri = urip.toString()
|
||||
@@ -334,20 +298,24 @@ export class PostThreadViewModel {
|
||||
}
|
||||
|
||||
private _replaceAll(res: GetPostThread.Response) {
|
||||
// TODO: validate .record
|
||||
sortThread(res.data.thread)
|
||||
// sortThread(res.data.thread) TODO needed?
|
||||
const keyGen = reactKeyGenerator()
|
||||
const thread = new PostThreadViewPostModel(
|
||||
this.rootStore,
|
||||
keyGen.next().value,
|
||||
res.data.thread as GetPostThread.Post,
|
||||
res.data.thread as GetPostThread.ThreadViewPost,
|
||||
)
|
||||
thread._isHighlightedPost = true
|
||||
thread.assignTreeModels(keyGen, res.data.thread as GetPostThread.Post)
|
||||
thread.assignTreeModels(
|
||||
keyGen,
|
||||
res.data.thread as GetPostThread.ThreadViewPost,
|
||||
)
|
||||
this.thread = thread
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
TODO needed?
|
||||
function sortThread(post: MaybePost) {
|
||||
if (post.notFound) {
|
||||
return
|
||||
@@ -378,3 +346,4 @@ function sortThread(post: MaybePost) {
|
||||
post.replies.forEach(reply => sortThread(reply))
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {makeAutoObservable} from 'mobx'
|
||||
import * as Post from '../../third-party/api/src/client/types/app/bsky/feed/post'
|
||||
import {AppBskyFeedPost as Post} from '@atproto/api'
|
||||
import {AtUri} from '../../third-party/uri'
|
||||
import {RootStoreModel} from './root-store'
|
||||
import {cleanError} from '../../lib/strings'
|
||||
@@ -63,10 +63,14 @@ export class PostModel implements RemoveIndex<Post.Record> {
|
||||
this.error = ''
|
||||
}
|
||||
|
||||
private _xIdle(err: string = '') {
|
||||
private _xIdle(err?: any) {
|
||||
this.isLoading = false
|
||||
this.hasLoaded = true
|
||||
this.error = cleanError(err)
|
||||
this.error = err ? cleanError(err) : ''
|
||||
if (err) {
|
||||
this.rootStore.log.error('Failed to fetch post', err)
|
||||
}
|
||||
}
|
||||
|
||||
// loader functions
|
||||
@@ -87,7 +91,7 @@ export class PostModel implements RemoveIndex<Post.Record> {
|
||||
this._replaceAll(res.value)
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
this._xIdle(e.toString())
|
||||
this._xIdle(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,14 @@
|
||||
import {makeAutoObservable} from 'mobx'
|
||||
import {RootStoreModel} from './root-store'
|
||||
import {ProfileViewModel} from './profile-view'
|
||||
import {MembersViewModel} from './members-view'
|
||||
import {MembershipsViewModel} from './memberships-view'
|
||||
import {FeedModel} from './feed-view'
|
||||
|
||||
export enum Sections {
|
||||
Posts = 'Posts',
|
||||
PostsWithReplies = 'Posts & replies',
|
||||
Scenes = 'Scenes',
|
||||
Trending = 'Trending',
|
||||
Members = 'Members',
|
||||
}
|
||||
|
||||
const USER_SELECTOR_ITEMS = [
|
||||
Sections.Posts,
|
||||
Sections.PostsWithReplies,
|
||||
Sections.Scenes,
|
||||
]
|
||||
const SCENE_SELECTOR_ITEMS = [Sections.Trending, Sections.Members]
|
||||
const USER_SELECTOR_ITEMS = [Sections.Posts, Sections.PostsWithReplies]
|
||||
|
||||
export interface ProfileUiParams {
|
||||
user: string
|
||||
@@ -28,8 +18,6 @@ export class ProfileUiModel {
|
||||
// data
|
||||
profile: ProfileViewModel
|
||||
feed: FeedModel
|
||||
memberships: MembershipsViewModel
|
||||
members: MembersViewModel
|
||||
|
||||
// ui state
|
||||
selectedViewIndex = 0
|
||||
@@ -51,24 +39,15 @@ export class ProfileUiModel {
|
||||
author: params.user,
|
||||
limit: 10,
|
||||
})
|
||||
this.memberships = new MembershipsViewModel(rootStore, {actor: params.user})
|
||||
this.members = new MembersViewModel(rootStore, {actor: params.user})
|
||||
}
|
||||
|
||||
get currentView(): FeedModel | MembershipsViewModel | MembersViewModel {
|
||||
get currentView(): FeedModel {
|
||||
if (
|
||||
this.selectedView === Sections.Posts ||
|
||||
this.selectedView === Sections.PostsWithReplies ||
|
||||
this.selectedView === Sections.Trending
|
||||
this.selectedView === Sections.PostsWithReplies
|
||||
) {
|
||||
return this.feed
|
||||
}
|
||||
if (this.selectedView === Sections.Scenes) {
|
||||
return this.memberships
|
||||
}
|
||||
if (this.selectedView === Sections.Members) {
|
||||
return this.members
|
||||
}
|
||||
throw new Error(`Invalid selector value: ${this.selectedViewIndex}`)
|
||||
}
|
||||
|
||||
@@ -85,15 +64,9 @@ export class ProfileUiModel {
|
||||
return this.profile.isUser
|
||||
}
|
||||
|
||||
get isScene() {
|
||||
return this.profile.isScene
|
||||
}
|
||||
|
||||
get selectorItems() {
|
||||
if (this.isUser) {
|
||||
return USER_SELECTOR_ITEMS
|
||||
} else if (this.isScene) {
|
||||
return SCENE_SELECTOR_ITEMS
|
||||
} else {
|
||||
return USER_SELECTOR_ITEMS
|
||||
}
|
||||
@@ -114,21 +87,11 @@ export class ProfileUiModel {
|
||||
await Promise.all([
|
||||
this.profile
|
||||
.setup()
|
||||
.catch(err => console.error('Failed to fetch profile', err)),
|
||||
.catch(err => this.rootStore.log.error('Failed to fetch profile', err)),
|
||||
this.feed
|
||||
.setup()
|
||||
.catch(err => console.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 => console.error('Failed to fetch members', err))
|
||||
}
|
||||
if (this.isScene) {
|
||||
await this.members
|
||||
.setup()
|
||||
.catch(err => console.error('Failed to fetch members', err))
|
||||
}
|
||||
}
|
||||
|
||||
async update() {
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import {makeAutoObservable, runInAction} from 'mobx'
|
||||
import {Image as PickedImage} from 'react-native-image-crop-picker'
|
||||
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 {Main as DeclRef} from '../../third-party/api/src/client/types/app/bsky/system/declRef'
|
||||
import {Entity} from '../../third-party/api/src/client/types/app/bsky/feed/post'
|
||||
import {
|
||||
AppBskyActorGetProfile as GetProfile,
|
||||
AppBskyActorProfile as Profile,
|
||||
AppBskySystemDeclRef,
|
||||
AppBskyFeedPost,
|
||||
} from '@atproto/api'
|
||||
type DeclRef = AppBskySystemDeclRef.Main
|
||||
type Entity = AppBskyFeedPost.Entity
|
||||
import {extractEntities} from '../../lib/strings'
|
||||
import {RootStoreModel} from './root-store'
|
||||
import * as apilib from '../lib/api'
|
||||
|
||||
export const ACTOR_TYPE_USER = 'app.bsky.system.actorUser'
|
||||
export const ACTOR_TYPE_SCENE = 'app.bsky.system.actorScene'
|
||||
|
||||
export class ProfileViewMyStateModel {
|
||||
follow?: string
|
||||
member?: string
|
||||
muted?: boolean
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this)
|
||||
@@ -39,15 +42,12 @@ export class ProfileViewModel {
|
||||
displayName?: string
|
||||
description?: string
|
||||
avatar?: string
|
||||
banner?: string
|
||||
followersCount: number = 0
|
||||
followsCount: number = 0
|
||||
membersCount: number = 0
|
||||
postsCount: number = 0
|
||||
myState = new ProfileViewMyStateModel()
|
||||
|
||||
// TODO TEMP data to be implemented in the protocol
|
||||
userBanner: string | null = null
|
||||
|
||||
// added data
|
||||
descriptionEntities?: Entity[]
|
||||
|
||||
@@ -82,10 +82,6 @@ export class ProfileViewModel {
|
||||
return this.declaration.actorType === ACTOR_TYPE_USER
|
||||
}
|
||||
|
||||
get isScene() {
|
||||
return this.declaration.actorType === ACTOR_TYPE_SCENE
|
||||
}
|
||||
|
||||
// public api
|
||||
// =
|
||||
|
||||
@@ -123,11 +119,8 @@ export class ProfileViewModel {
|
||||
async updateProfile(
|
||||
updates: Profile.Record,
|
||||
newUserAvatar: PickedImage | undefined,
|
||||
userBanner: string | null, // TODO TEMP
|
||||
newUserBanner: PickedImage | undefined,
|
||||
) {
|
||||
// TODO TEMP add userBanner to the protocol when suported
|
||||
this.userBanner = userBanner
|
||||
|
||||
if (newUserAvatar) {
|
||||
const res = await this.rootStore.api.com.atproto.blob.upload(
|
||||
newUserAvatar.path, // this will be special-cased by the fetch monkeypatch in /src/state/lib/api.ts
|
||||
@@ -140,11 +133,35 @@ export class ProfileViewModel {
|
||||
mimeType: newUserAvatar.mime,
|
||||
}
|
||||
}
|
||||
if (newUserBanner) {
|
||||
const res = await this.rootStore.api.com.atproto.blob.upload(
|
||||
newUserBanner.path, // this will be special-cased by the fetch monkeypatch in /src/state/lib/api.ts
|
||||
{
|
||||
encoding: newUserBanner.mime,
|
||||
},
|
||||
)
|
||||
updates.banner = {
|
||||
cid: res.data.cid,
|
||||
mimeType: newUserBanner.mime,
|
||||
}
|
||||
}
|
||||
await this.rootStore.api.app.bsky.actor.updateProfile(updates)
|
||||
await this.rootStore.me.load()
|
||||
await this.refresh()
|
||||
}
|
||||
|
||||
async muteAccount() {
|
||||
await this.rootStore.api.app.bsky.graph.mute({user: this.did})
|
||||
this.myState.muted = true
|
||||
await this.refresh()
|
||||
}
|
||||
|
||||
async unmuteAccount() {
|
||||
await this.rootStore.api.app.bsky.graph.unmute({user: this.did})
|
||||
this.myState.muted = false
|
||||
await this.refresh()
|
||||
}
|
||||
|
||||
// state transitions
|
||||
// =
|
||||
|
||||
@@ -154,11 +171,14 @@ export class ProfileViewModel {
|
||||
this.error = ''
|
||||
}
|
||||
|
||||
private _xIdle(err: string = '') {
|
||||
private _xIdle(err?: any) {
|
||||
this.isLoading = false
|
||||
this.isRefreshing = false
|
||||
this.hasLoaded = true
|
||||
this.error = err
|
||||
this.error = err ? err.toString() : ''
|
||||
if (err) {
|
||||
this.rootStore.log.error('Failed to fetch profile', err)
|
||||
}
|
||||
}
|
||||
|
||||
// loader functions
|
||||
@@ -174,12 +194,11 @@ export class ProfileViewModel {
|
||||
this._replaceAll(res)
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
this._xIdle(e.toString())
|
||||
this._xIdle(e)
|
||||
}
|
||||
}
|
||||
|
||||
private _replaceAll(res: GetProfile.Response) {
|
||||
console.log(res.data)
|
||||
this.did = res.data.did
|
||||
this.handle = res.data.handle
|
||||
Object.assign(this.declaration, res.data.declaration)
|
||||
@@ -187,9 +206,9 @@ export class ProfileViewModel {
|
||||
this.displayName = res.data.displayName
|
||||
this.description = res.data.description
|
||||
this.avatar = res.data.avatar
|
||||
this.banner = res.data.banner
|
||||
this.followersCount = res.data.followersCount
|
||||
this.followsCount = res.data.followsCount
|
||||
this.membersCount = res.data.membersCount
|
||||
this.postsCount = res.data.postsCount
|
||||
if (res.data.myState) {
|
||||
Object.assign(this.myState, res.data.myState)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {makeAutoObservable} from 'mobx'
|
||||
import {LRUMap} from 'lru_map'
|
||||
import {RootStoreModel} from './root-store'
|
||||
import * as GetProfile from '../../third-party/api/src/client/types/app/bsky/actor/getProfile'
|
||||
import {AppBskyActorGetProfile as GetProfile} from '@atproto/api'
|
||||
|
||||
type CacheValue = Promise<GetProfile.Response> | GetProfile.Response
|
||||
export class ProfilesViewModel {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import {makeAutoObservable, runInAction} from 'mobx'
|
||||
import {AtUri} from '../../third-party/uri'
|
||||
import * as GetRepostedBy from '../../third-party/api/src/client/types/app/bsky/feed/getRepostedBy'
|
||||
import {Main as DeclRef} from '../../third-party/api/src/client/types/app/bsky/system/declRef'
|
||||
import {
|
||||
AppBskyFeedGetRepostedBy as GetRepostedBy,
|
||||
AppBskySystemDeclRef,
|
||||
} from '@atproto/api'
|
||||
type DeclRef = AppBskySystemDeclRef.Main
|
||||
import {RootStoreModel} from './root-store'
|
||||
|
||||
export class RepostedByViewItemModel implements GetRepostedBy.RepostedBy {
|
||||
@@ -91,11 +94,14 @@ export class RepostedByViewModel {
|
||||
this.error = ''
|
||||
}
|
||||
|
||||
private _xIdle(err: string = '') {
|
||||
private _xIdle(err?: any) {
|
||||
this.isLoading = false
|
||||
this.isRefreshing = false
|
||||
this.hasLoaded = true
|
||||
this.error = err
|
||||
this.error = err ? err.toString() : ''
|
||||
if (err) {
|
||||
this.rootStore.log.error('Failed to fetch reposted by view', err)
|
||||
}
|
||||
}
|
||||
|
||||
// loader functions
|
||||
@@ -104,7 +110,11 @@ export class RepostedByViewModel {
|
||||
private async _resolveUri() {
|
||||
const urip = new AtUri(this.params.uri)
|
||||
if (!urip.host.startsWith('did:')) {
|
||||
urip.host = await this.rootStore.resolveName(urip.host)
|
||||
try {
|
||||
urip.host = await this.rootStore.resolveName(urip.host)
|
||||
} catch (e: any) {
|
||||
this.error = e.toString()
|
||||
}
|
||||
}
|
||||
runInAction(() => {
|
||||
this.resolvedUri = urip.toString()
|
||||
@@ -120,7 +130,7 @@ export class RepostedByViewModel {
|
||||
this._replaceAll(res)
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
this._xIdle(`Failed to load feed: ${e.toString()}`)
|
||||
this._xIdle(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
*/
|
||||
|
||||
import {makeAutoObservable} from 'mobx'
|
||||
import {sessionClient as AtpApi} from '../../third-party/api'
|
||||
import type {SessionServiceClient} from '../../third-party/api/src/index'
|
||||
import {sessionClient as AtpApi, SessionServiceClient} from '@atproto/api'
|
||||
import {createContext, useContext} from 'react'
|
||||
import {isObj, hasProp} from '../lib/type-guards'
|
||||
import {LogModel} from './log'
|
||||
import {SessionModel} from './session'
|
||||
import {NavigationModel} from './navigation'
|
||||
import {ShellUiModel} from './shell-ui'
|
||||
@@ -17,6 +17,7 @@ import {OnboardModel} from './onboard'
|
||||
import {isNetworkError} from '../../lib/errors'
|
||||
|
||||
export class RootStoreModel {
|
||||
log = new LogModel()
|
||||
session = new SessionModel(this)
|
||||
nav = new NavigationModel()
|
||||
shell = new ShellUiModel()
|
||||
@@ -54,27 +55,29 @@ export class RootStoreModel {
|
||||
await this.session.connect()
|
||||
}
|
||||
await this.me.fetchStateUpdate()
|
||||
} catch (e: unknown) {
|
||||
} catch (e: any) {
|
||||
if (isNetworkError(e)) {
|
||||
this.session.setOnline(false) // connection lost
|
||||
}
|
||||
console.error('Failed to fetch latest state', e)
|
||||
this.log.error('Failed to fetch latest state', e)
|
||||
}
|
||||
}
|
||||
|
||||
serialize(): unknown {
|
||||
return {
|
||||
log: this.log.serialize(),
|
||||
session: this.session.serialize(),
|
||||
me: this.me.serialize(),
|
||||
nav: this.nav.serialize(),
|
||||
onboard: this.onboard.serialize(),
|
||||
shell: this.shell.serialize(),
|
||||
}
|
||||
}
|
||||
|
||||
hydrate(v: unknown) {
|
||||
if (isObj(v)) {
|
||||
if (hasProp(v, 'session')) {
|
||||
this.session.hydrate(v.session)
|
||||
if (hasProp(v, 'log')) {
|
||||
this.log.hydrate(v.log)
|
||||
}
|
||||
if (hasProp(v, 'me')) {
|
||||
this.me.hydrate(v.me)
|
||||
@@ -85,6 +88,12 @@ export class RootStoreModel {
|
||||
if (hasProp(v, 'onboard')) {
|
||||
this.onboard.hydrate(v.onboard)
|
||||
}
|
||||
if (hasProp(v, 'session')) {
|
||||
this.session.hydrate(v.session)
|
||||
}
|
||||
if (hasProp(v, 'shell')) {
|
||||
this.shell.hydrate(v.shell)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+17
-12
@@ -1,10 +1,10 @@
|
||||
import {makeAutoObservable} from 'mobx'
|
||||
import {sessionClient as AtpApi} from '../../third-party/api/index'
|
||||
import type {
|
||||
SessionServiceClient,
|
||||
import {
|
||||
sessionClient as AtpApi,
|
||||
Session,
|
||||
} from '../../third-party/api/src/index'
|
||||
import type * as GetAccountsConfig from '../../third-party/api/src/client/types/com/atproto/server/getAccountsConfig'
|
||||
SessionServiceClient,
|
||||
ComAtprotoServerGetAccountsConfig as GetAccountsConfig,
|
||||
} from '@atproto/api'
|
||||
import {isObj, hasProp} from '../lib/type-guards'
|
||||
import {RootStoreModel} from './root-store'
|
||||
import {isNetworkError} from '../../lib/errors'
|
||||
@@ -89,6 +89,7 @@ export class SessionModel {
|
||||
|
||||
clear() {
|
||||
this.data = null
|
||||
this.setOnline(false)
|
||||
}
|
||||
|
||||
setState(data: SessionData) {
|
||||
@@ -120,11 +121,11 @@ export class SessionModel {
|
||||
try {
|
||||
const serviceUri = new URL(this.data.service)
|
||||
this.rootStore.api.xrpc.uri = serviceUri
|
||||
} catch (e) {
|
||||
console.error(
|
||||
} catch (e: any) {
|
||||
this.rootStore.log.error(
|
||||
`Invalid service URL: ${this.data.service}. Resetting session.`,
|
||||
e,
|
||||
)
|
||||
console.error(e)
|
||||
this.clear()
|
||||
return false
|
||||
}
|
||||
@@ -159,7 +160,7 @@ export class SessionModel {
|
||||
this.rootStore.me.clear()
|
||||
}
|
||||
this.rootStore.me.load().catch(e => {
|
||||
console.error('Failed to fetch local user information', e)
|
||||
this.rootStore.log.error('Failed to fetch local user information', e)
|
||||
})
|
||||
return // success
|
||||
}
|
||||
@@ -201,8 +202,9 @@ export class SessionModel {
|
||||
did: res.data.did,
|
||||
})
|
||||
this.configureApi()
|
||||
this.setOnline(true, false)
|
||||
this.rootStore.me.load().catch(e => {
|
||||
console.error('Failed to fetch local user information', e)
|
||||
this.rootStore.log.error('Failed to fetch local user information', e)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -238,7 +240,7 @@ export class SessionModel {
|
||||
this.rootStore.onboard.start()
|
||||
this.configureApi()
|
||||
this.rootStore.me.load().catch(e => {
|
||||
console.error('Failed to fetch local user information', e)
|
||||
this.rootStore.log.error('Failed to fetch local user information', e)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -246,7 +248,10 @@ export class SessionModel {
|
||||
async logout() {
|
||||
if (this.hasSession) {
|
||||
this.rootStore.api.com.atproto.session.delete().catch((e: any) => {
|
||||
console.error('(Minor issue) Failed to delete session on the server', e)
|
||||
this.rootStore.log.warn(
|
||||
'(Minor issue) Failed to delete session on the server',
|
||||
e,
|
||||
)
|
||||
})
|
||||
}
|
||||
this.rootStore.clearAll()
|
||||
|
||||
+125
-28
@@ -1,7 +1,8 @@
|
||||
import {makeAutoObservable} from 'mobx'
|
||||
import {ProfileViewModel} from './profile-view'
|
||||
import {isObj, hasProp} from '../lib/type-guards'
|
||||
|
||||
export class ConfirmModel {
|
||||
export class ConfirmModal {
|
||||
name = 'confirm'
|
||||
|
||||
constructor(
|
||||
@@ -13,7 +14,7 @@ export class ConfirmModel {
|
||||
}
|
||||
}
|
||||
|
||||
export class EditProfileModel {
|
||||
export class EditProfileModal {
|
||||
name = 'edit-profile'
|
||||
|
||||
constructor(
|
||||
@@ -24,23 +25,7 @@ export class EditProfileModel {
|
||||
}
|
||||
}
|
||||
|
||||
export class CreateSceneModel {
|
||||
name = 'create-scene'
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this)
|
||||
}
|
||||
}
|
||||
|
||||
export class InviteToSceneModel {
|
||||
name = 'invite-to-scene'
|
||||
|
||||
constructor(public profileView: ProfileViewModel) {
|
||||
makeAutoObservable(this)
|
||||
}
|
||||
}
|
||||
|
||||
export class ServerInputModel {
|
||||
export class ServerInputModal {
|
||||
name = 'server-input'
|
||||
|
||||
constructor(
|
||||
@@ -51,6 +36,74 @@ export class ServerInputModel {
|
||||
}
|
||||
}
|
||||
|
||||
export class ReportPostModal {
|
||||
name = 'report-post'
|
||||
|
||||
constructor(public postUrl: string) {
|
||||
makeAutoObservable(this)
|
||||
}
|
||||
}
|
||||
|
||||
export class ReportAccountModal {
|
||||
name = 'report-account'
|
||||
|
||||
constructor(public did: string) {
|
||||
makeAutoObservable(this)
|
||||
}
|
||||
}
|
||||
|
||||
interface LightboxModel {
|
||||
canSwipeLeft: boolean
|
||||
canSwipeRight: boolean
|
||||
onSwipeLeft: () => void
|
||||
onSwipeRight: () => void
|
||||
}
|
||||
|
||||
export class ProfileImageLightbox implements LightboxModel {
|
||||
name = 'profile-image'
|
||||
canSwipeLeft = false
|
||||
canSwipeRight = false
|
||||
constructor(public profileView: ProfileViewModel) {
|
||||
makeAutoObservable(this)
|
||||
}
|
||||
onSwipeLeft() {}
|
||||
onSwipeRight() {}
|
||||
}
|
||||
|
||||
export class ImageLightbox implements LightboxModel {
|
||||
name = 'image'
|
||||
canSwipeLeft = true
|
||||
canSwipeRight = true
|
||||
constructor(public uri: string) {
|
||||
makeAutoObservable(this)
|
||||
}
|
||||
onSwipeLeft() {}
|
||||
onSwipeRight() {}
|
||||
}
|
||||
|
||||
export class ImagesLightbox implements LightboxModel {
|
||||
name = 'images'
|
||||
get canSwipeLeft() {
|
||||
return this.index > 0
|
||||
}
|
||||
get canSwipeRight() {
|
||||
return this.index < this.uris.length - 1
|
||||
}
|
||||
constructor(public uris: string[], public index: number) {
|
||||
makeAutoObservable(this)
|
||||
}
|
||||
onSwipeLeft() {
|
||||
if (this.canSwipeLeft) {
|
||||
this.index = this.index - 1
|
||||
}
|
||||
}
|
||||
onSwipeRight() {
|
||||
if (this.canSwipeRight) {
|
||||
this.index = this.index + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface ComposerOptsPostRef {
|
||||
uri: string
|
||||
cid: string
|
||||
@@ -67,19 +120,50 @@ export interface ComposerOpts {
|
||||
}
|
||||
|
||||
export class ShellUiModel {
|
||||
darkMode = false
|
||||
minimalShellMode = false
|
||||
isMainMenuOpen = false
|
||||
isModalActive = false
|
||||
activeModal:
|
||||
| ConfirmModel
|
||||
| EditProfileModel
|
||||
| CreateSceneModel
|
||||
| ServerInputModel
|
||||
| ConfirmModal
|
||||
| EditProfileModal
|
||||
| ServerInputModal
|
||||
| ReportPostModal
|
||||
| ReportAccountModal
|
||||
| undefined
|
||||
isLightboxActive = false
|
||||
activeLightbox:
|
||||
| ProfileImageLightbox
|
||||
| ImageLightbox
|
||||
| ImagesLightbox
|
||||
| undefined
|
||||
isComposerActive = false
|
||||
composerOpts: ComposerOpts | undefined
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this)
|
||||
makeAutoObservable(this, {serialize: false, hydrate: false})
|
||||
}
|
||||
|
||||
serialize(): unknown {
|
||||
return {
|
||||
darkMode: this.darkMode,
|
||||
}
|
||||
}
|
||||
|
||||
hydrate(v: unknown) {
|
||||
if (isObj(v)) {
|
||||
if (hasProp(v, 'darkMode') && typeof v.darkMode === 'boolean') {
|
||||
this.darkMode = v.darkMode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setDarkMode(v: boolean) {
|
||||
this.darkMode = v
|
||||
}
|
||||
|
||||
setMinimalShellMode(v: boolean) {
|
||||
this.minimalShellMode = v
|
||||
}
|
||||
|
||||
setMainMenuOpen(v: boolean) {
|
||||
@@ -88,10 +172,11 @@ export class ShellUiModel {
|
||||
|
||||
openModal(
|
||||
modal:
|
||||
| ConfirmModel
|
||||
| EditProfileModel
|
||||
| CreateSceneModel
|
||||
| ServerInputModel,
|
||||
| ConfirmModal
|
||||
| EditProfileModal
|
||||
| ServerInputModal
|
||||
| ReportPostModal
|
||||
| ReportAccountModal,
|
||||
) {
|
||||
this.isModalActive = true
|
||||
this.activeModal = modal
|
||||
@@ -102,6 +187,18 @@ export class ShellUiModel {
|
||||
this.activeModal = undefined
|
||||
}
|
||||
|
||||
openLightbox(
|
||||
lightbox: ProfileImageLightbox | ImageLightbox | ImagesLightbox,
|
||||
) {
|
||||
this.isLightboxActive = true
|
||||
this.activeLightbox = lightbox
|
||||
}
|
||||
|
||||
closeLightbox() {
|
||||
this.isLightboxActive = false
|
||||
this.activeLightbox = undefined
|
||||
}
|
||||
|
||||
openComposer(opts: ComposerOpts) {
|
||||
this.isComposerActive = true
|
||||
this.composerOpts = opts
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {makeAutoObservable} from 'mobx'
|
||||
import * as GetSuggestions from '../../third-party/api/src/client/types/app/bsky/actor/getSuggestions'
|
||||
import {AppBskyActorGetSuggestions as GetSuggestions} from '@atproto/api'
|
||||
import {RootStoreModel} from './root-store'
|
||||
|
||||
export type SuggestedActor = GetSuggestions.Actor & {
|
||||
@@ -58,11 +58,14 @@ export class SuggestedActorsViewModel {
|
||||
this.error = ''
|
||||
}
|
||||
|
||||
private _xIdle(err: string = '') {
|
||||
private _xIdle(err?: any) {
|
||||
this.isLoading = false
|
||||
this.isRefreshing = false
|
||||
this.hasLoaded = true
|
||||
this.error = err
|
||||
this.error = err ? err.toString() : ''
|
||||
if (err) {
|
||||
this.rootStore.log.error('Failed to fetch suggested actors', err)
|
||||
}
|
||||
}
|
||||
|
||||
// loader functions
|
||||
@@ -88,7 +91,7 @@ export class SuggestedActorsViewModel {
|
||||
)
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
this._xIdle(e.toString())
|
||||
this._xIdle(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,133 +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 '../../third-party/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: string = '') {
|
||||
this.isLoading = false
|
||||
this.isRefreshing = false
|
||||
this.hasLoaded = true
|
||||
this.error = err
|
||||
}
|
||||
|
||||
// loader functions
|
||||
// =
|
||||
|
||||
private async _fetch(isRefreshing = false) {
|
||||
this._xLoading(isRefreshing)
|
||||
try {
|
||||
// TODO need to fetch all!
|
||||
await this.sceneAssertionsView.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) {
|
||||
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,6 +1,8 @@
|
||||
import {makeAutoObservable, runInAction} from 'mobx'
|
||||
import * as GetFollows from '../../third-party/api/src/client/types/app/bsky/graph/getFollows'
|
||||
import * as SearchTypeahead from '../../third-party/api/src/client/types/app/bsky/actor/searchTypeahead'
|
||||
import {
|
||||
AppBskyGraphGetFollows as GetFollows,
|
||||
AppBskyActorSearchTypeahead as SearchTypeahead,
|
||||
} from '@atproto/api'
|
||||
import {RootStoreModel} from './root-store'
|
||||
|
||||
export class UserAutocompleteViewModel {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {makeAutoObservable} from 'mobx'
|
||||
import * as GetFollowers from '../../third-party/api/src/client/types/app/bsky/graph/getFollowers'
|
||||
import * as ActorRef from '../../third-party/api/src/client/types/app/bsky/actor/ref'
|
||||
import {
|
||||
AppBskyGraphGetFollowers as GetFollowers,
|
||||
AppBskyActorRef as ActorRef,
|
||||
} from '@atproto/api'
|
||||
import {RootStoreModel} from './root-store'
|
||||
|
||||
export type FollowerItem = GetFollowers.Follower & {
|
||||
@@ -74,11 +76,14 @@ export class UserFollowersViewModel {
|
||||
this.error = ''
|
||||
}
|
||||
|
||||
private _xIdle(err: string = '') {
|
||||
private _xIdle(err?: any) {
|
||||
this.isLoading = false
|
||||
this.isRefreshing = false
|
||||
this.hasLoaded = true
|
||||
this.error = err
|
||||
this.error = err ? err.toString() : ''
|
||||
if (err) {
|
||||
this.rootStore.log.error('Failed to fetch user followers', err)
|
||||
}
|
||||
}
|
||||
|
||||
// loader functions
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {makeAutoObservable} from 'mobx'
|
||||
import * as GetFollows from '../../third-party/api/src/client/types/app/bsky/graph/getFollows'
|
||||
import * as ActorRef from '../../third-party/api/src/client/types/app/bsky/actor/ref'
|
||||
import {
|
||||
AppBskyGraphGetFollows as GetFollows,
|
||||
AppBskyActorRef as ActorRef,
|
||||
} from '@atproto/api'
|
||||
import {RootStoreModel} from './root-store'
|
||||
|
||||
export type FollowItem = GetFollows.Follow & {
|
||||
@@ -74,11 +76,14 @@ export class UserFollowsViewModel {
|
||||
this.error = ''
|
||||
}
|
||||
|
||||
private _xIdle(err: string = '') {
|
||||
private _xIdle(err?: any) {
|
||||
this.isLoading = false
|
||||
this.isRefreshing = false
|
||||
this.hasLoaded = true
|
||||
this.error = err
|
||||
this.error = err ? err.toString() : ''
|
||||
if (err) {
|
||||
this.rootStore.log.error('Failed to fetch user follows', err)
|
||||
}
|
||||
}
|
||||
|
||||
// loader functions
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import {makeAutoObservable, runInAction} from 'mobx'
|
||||
import {AtUri} from '../../third-party/uri'
|
||||
import * as GetVotes from '../../third-party/api/src/client/types/app/bsky/feed/getVotes'
|
||||
import * as ActorRef from '../../third-party/api/src/client/types/app/bsky/actor/ref'
|
||||
import {
|
||||
AppBskyFeedGetVotes as GetVotes,
|
||||
AppBskyActorRef as ActorRef,
|
||||
} from '@atproto/api'
|
||||
import {RootStoreModel} from './root-store'
|
||||
|
||||
export class VotesViewItemModel implements GetVotes.Vote {
|
||||
@@ -89,11 +91,14 @@ export class VotesViewModel {
|
||||
this.error = ''
|
||||
}
|
||||
|
||||
private _xIdle(err: string = '') {
|
||||
private _xIdle(err?: any) {
|
||||
this.isLoading = false
|
||||
this.isRefreshing = false
|
||||
this.hasLoaded = true
|
||||
this.error = err
|
||||
this.error = err ? err.toString() : ''
|
||||
if (err) {
|
||||
this.rootStore.log.error('Failed to fetch votes', err)
|
||||
}
|
||||
}
|
||||
|
||||
// loader functions
|
||||
@@ -102,7 +107,11 @@ export class VotesViewModel {
|
||||
private async _resolveUri() {
|
||||
const urip = new AtUri(this.params.uri)
|
||||
if (!urip.host.startsWith('did:')) {
|
||||
urip.host = await this.rootStore.resolveName(urip.host)
|
||||
try {
|
||||
urip.host = await this.rootStore.resolveName(urip.host)
|
||||
} catch (e: any) {
|
||||
this.error = e.toString()
|
||||
}
|
||||
}
|
||||
runInAction(() => {
|
||||
this.resolvedUri = urip.toString()
|
||||
@@ -118,7 +127,7 @@ export class VotesViewModel {
|
||||
this._replaceAll(res)
|
||||
this._xIdle()
|
||||
} catch (e: any) {
|
||||
this._xIdle(`Failed to load feed: ${e.toString()}`)
|
||||
this._xIdle(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Vendored
-8660
File diff suppressed because it is too large
Load Diff
Vendored
-7
File diff suppressed because one or more lines are too long
-455
@@ -1,455 +0,0 @@
|
||||
import { Client as XrpcClient, ServiceClient as XrpcServiceClient } from '@atproto/xrpc';
|
||||
import * as ComAtprotoAccountCreate from './types/com/atproto/account/create';
|
||||
import * as ComAtprotoAccountCreateInviteCode from './types/com/atproto/account/createInviteCode';
|
||||
import * as ComAtprotoAccountDelete from './types/com/atproto/account/delete';
|
||||
import * as ComAtprotoAccountGet from './types/com/atproto/account/get';
|
||||
import * as ComAtprotoAccountRequestPasswordReset from './types/com/atproto/account/requestPasswordReset';
|
||||
import * as ComAtprotoAccountResetPassword from './types/com/atproto/account/resetPassword';
|
||||
import * as ComAtprotoBlobUpload from './types/com/atproto/blob/upload';
|
||||
import * as ComAtprotoHandleResolve from './types/com/atproto/handle/resolve';
|
||||
import * as ComAtprotoRepoBatchWrite from './types/com/atproto/repo/batchWrite';
|
||||
import * as ComAtprotoRepoCreateRecord from './types/com/atproto/repo/createRecord';
|
||||
import * as ComAtprotoRepoDeleteRecord from './types/com/atproto/repo/deleteRecord';
|
||||
import * as ComAtprotoRepoDescribe from './types/com/atproto/repo/describe';
|
||||
import * as ComAtprotoRepoGetRecord from './types/com/atproto/repo/getRecord';
|
||||
import * as ComAtprotoRepoListRecords from './types/com/atproto/repo/listRecords';
|
||||
import * as ComAtprotoRepoPutRecord from './types/com/atproto/repo/putRecord';
|
||||
import * as ComAtprotoServerGetAccountsConfig from './types/com/atproto/server/getAccountsConfig';
|
||||
import * as ComAtprotoSessionCreate from './types/com/atproto/session/create';
|
||||
import * as ComAtprotoSessionDelete from './types/com/atproto/session/delete';
|
||||
import * as ComAtprotoSessionGet from './types/com/atproto/session/get';
|
||||
import * as ComAtprotoSessionRefresh from './types/com/atproto/session/refresh';
|
||||
import * as ComAtprotoSyncGetRepo from './types/com/atproto/sync/getRepo';
|
||||
import * as ComAtprotoSyncGetRoot from './types/com/atproto/sync/getRoot';
|
||||
import * as ComAtprotoSyncUpdateRepo from './types/com/atproto/sync/updateRepo';
|
||||
import * as AppBskyActorCreateScene from './types/app/bsky/actor/createScene';
|
||||
import * as AppBskyActorGetProfile from './types/app/bsky/actor/getProfile';
|
||||
import * as AppBskyActorGetSuggestions from './types/app/bsky/actor/getSuggestions';
|
||||
import * as AppBskyActorProfile from './types/app/bsky/actor/profile';
|
||||
import * as AppBskyActorSearch from './types/app/bsky/actor/search';
|
||||
import * as AppBskyActorSearchTypeahead from './types/app/bsky/actor/searchTypeahead';
|
||||
import * as AppBskyActorUpdateProfile from './types/app/bsky/actor/updateProfile';
|
||||
import * as AppBskyFeedGetAuthorFeed from './types/app/bsky/feed/getAuthorFeed';
|
||||
import * as AppBskyFeedGetPostThread from './types/app/bsky/feed/getPostThread';
|
||||
import * as AppBskyFeedGetRepostedBy from './types/app/bsky/feed/getRepostedBy';
|
||||
import * as AppBskyFeedGetTimeline from './types/app/bsky/feed/getTimeline';
|
||||
import * as AppBskyFeedGetVotes from './types/app/bsky/feed/getVotes';
|
||||
import * as AppBskyFeedPost from './types/app/bsky/feed/post';
|
||||
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 AppBskyGraphAssertion from './types/app/bsky/graph/assertion';
|
||||
import * as AppBskyGraphConfirmation from './types/app/bsky/graph/confirmation';
|
||||
import * as AppBskyGraphFollow from './types/app/bsky/graph/follow';
|
||||
import * as AppBskyGraphGetAssertions from './types/app/bsky/graph/getAssertions';
|
||||
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';
|
||||
import * as AppBskySystemDeclaration from './types/app/bsky/system/declaration';
|
||||
export * as ComAtprotoAccountCreate from './types/com/atproto/account/create';
|
||||
export * as ComAtprotoAccountCreateInviteCode from './types/com/atproto/account/createInviteCode';
|
||||
export * as ComAtprotoAccountDelete from './types/com/atproto/account/delete';
|
||||
export * as ComAtprotoAccountGet from './types/com/atproto/account/get';
|
||||
export * as ComAtprotoAccountRequestPasswordReset from './types/com/atproto/account/requestPasswordReset';
|
||||
export * as ComAtprotoAccountResetPassword from './types/com/atproto/account/resetPassword';
|
||||
export * as ComAtprotoBlobUpload from './types/com/atproto/blob/upload';
|
||||
export * as ComAtprotoHandleResolve from './types/com/atproto/handle/resolve';
|
||||
export * as ComAtprotoRepoBatchWrite from './types/com/atproto/repo/batchWrite';
|
||||
export * as ComAtprotoRepoCreateRecord from './types/com/atproto/repo/createRecord';
|
||||
export * as ComAtprotoRepoDeleteRecord from './types/com/atproto/repo/deleteRecord';
|
||||
export * as ComAtprotoRepoDescribe from './types/com/atproto/repo/describe';
|
||||
export * as ComAtprotoRepoGetRecord from './types/com/atproto/repo/getRecord';
|
||||
export * as ComAtprotoRepoListRecords from './types/com/atproto/repo/listRecords';
|
||||
export * as ComAtprotoRepoPutRecord from './types/com/atproto/repo/putRecord';
|
||||
export * as ComAtprotoRepoStrongRef from './types/com/atproto/repo/strongRef';
|
||||
export * as ComAtprotoServerGetAccountsConfig from './types/com/atproto/server/getAccountsConfig';
|
||||
export * as ComAtprotoSessionCreate from './types/com/atproto/session/create';
|
||||
export * as ComAtprotoSessionDelete from './types/com/atproto/session/delete';
|
||||
export * as ComAtprotoSessionGet from './types/com/atproto/session/get';
|
||||
export * as ComAtprotoSessionRefresh from './types/com/atproto/session/refresh';
|
||||
export * as ComAtprotoSyncGetRepo from './types/com/atproto/sync/getRepo';
|
||||
export * as ComAtprotoSyncGetRoot from './types/com/atproto/sync/getRoot';
|
||||
export * as ComAtprotoSyncUpdateRepo from './types/com/atproto/sync/updateRepo';
|
||||
export * as AppBskyActorCreateScene from './types/app/bsky/actor/createScene';
|
||||
export * as AppBskyActorGetProfile from './types/app/bsky/actor/getProfile';
|
||||
export * as AppBskyActorGetSuggestions from './types/app/bsky/actor/getSuggestions';
|
||||
export * as AppBskyActorProfile from './types/app/bsky/actor/profile';
|
||||
export * as AppBskyActorRef from './types/app/bsky/actor/ref';
|
||||
export * as AppBskyActorSearch from './types/app/bsky/actor/search';
|
||||
export * as AppBskyActorSearchTypeahead from './types/app/bsky/actor/searchTypeahead';
|
||||
export * as AppBskyActorUpdateProfile from './types/app/bsky/actor/updateProfile';
|
||||
export * as AppBskyFeedEmbed from './types/app/bsky/feed/embed';
|
||||
export * as AppBskyFeedGetAuthorFeed from './types/app/bsky/feed/getAuthorFeed';
|
||||
export * as AppBskyFeedGetPostThread from './types/app/bsky/feed/getPostThread';
|
||||
export * as AppBskyFeedGetRepostedBy from './types/app/bsky/feed/getRepostedBy';
|
||||
export * as AppBskyFeedGetTimeline from './types/app/bsky/feed/getTimeline';
|
||||
export * as AppBskyFeedGetVotes from './types/app/bsky/feed/getVotes';
|
||||
export * as AppBskyFeedPost from './types/app/bsky/feed/post';
|
||||
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 AppBskyGraphAssertCreator from './types/app/bsky/graph/assertCreator';
|
||||
export * as AppBskyGraphAssertMember from './types/app/bsky/graph/assertMember';
|
||||
export * as AppBskyGraphAssertion from './types/app/bsky/graph/assertion';
|
||||
export * as AppBskyGraphConfirmation from './types/app/bsky/graph/confirmation';
|
||||
export * as AppBskyGraphFollow from './types/app/bsky/graph/follow';
|
||||
export * as AppBskyGraphGetAssertions from './types/app/bsky/graph/getAssertions';
|
||||
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';
|
||||
export * as AppBskySystemActorScene from './types/app/bsky/system/actorScene';
|
||||
export * as AppBskySystemActorUser from './types/app/bsky/system/actorUser';
|
||||
export * as AppBskySystemDeclRef from './types/app/bsky/system/declRef';
|
||||
export * as AppBskySystemDeclaration from './types/app/bsky/system/declaration';
|
||||
export declare const APP_BSKY_GRAPH: {
|
||||
AssertCreator: string;
|
||||
AssertMember: string;
|
||||
};
|
||||
export declare const APP_BSKY_SYSTEM: {
|
||||
ActorScene: string;
|
||||
ActorUser: string;
|
||||
};
|
||||
export declare class Client {
|
||||
xrpc: XrpcClient;
|
||||
constructor();
|
||||
service(serviceUri: string | URL): ServiceClient;
|
||||
}
|
||||
declare const defaultInst: Client;
|
||||
export default defaultInst;
|
||||
export declare class ServiceClient {
|
||||
_baseClient: Client;
|
||||
xrpc: XrpcServiceClient;
|
||||
com: ComNS;
|
||||
app: AppNS;
|
||||
constructor(baseClient: Client, xrpcService: XrpcServiceClient);
|
||||
setHeader(key: string, value: string): void;
|
||||
}
|
||||
export declare class ComNS {
|
||||
_service: ServiceClient;
|
||||
atproto: AtprotoNS;
|
||||
constructor(service: ServiceClient);
|
||||
}
|
||||
export declare class AtprotoNS {
|
||||
_service: ServiceClient;
|
||||
account: AccountNS;
|
||||
blob: BlobNS;
|
||||
handle: HandleNS;
|
||||
repo: RepoNS;
|
||||
server: ServerNS;
|
||||
session: SessionNS;
|
||||
sync: SyncNS;
|
||||
constructor(service: ServiceClient);
|
||||
}
|
||||
export declare class AccountNS {
|
||||
_service: ServiceClient;
|
||||
constructor(service: ServiceClient);
|
||||
create(data?: ComAtprotoAccountCreate.InputSchema, opts?: ComAtprotoAccountCreate.CallOptions): Promise<ComAtprotoAccountCreate.Response>;
|
||||
createInviteCode(data?: ComAtprotoAccountCreateInviteCode.InputSchema, opts?: ComAtprotoAccountCreateInviteCode.CallOptions): Promise<ComAtprotoAccountCreateInviteCode.Response>;
|
||||
delete(data?: ComAtprotoAccountDelete.InputSchema, opts?: ComAtprotoAccountDelete.CallOptions): Promise<ComAtprotoAccountDelete.Response>;
|
||||
get(params?: ComAtprotoAccountGet.QueryParams, opts?: ComAtprotoAccountGet.CallOptions): Promise<ComAtprotoAccountGet.Response>;
|
||||
requestPasswordReset(data?: ComAtprotoAccountRequestPasswordReset.InputSchema, opts?: ComAtprotoAccountRequestPasswordReset.CallOptions): Promise<ComAtprotoAccountRequestPasswordReset.Response>;
|
||||
resetPassword(data?: ComAtprotoAccountResetPassword.InputSchema, opts?: ComAtprotoAccountResetPassword.CallOptions): Promise<ComAtprotoAccountResetPassword.Response>;
|
||||
}
|
||||
export declare class BlobNS {
|
||||
_service: ServiceClient;
|
||||
constructor(service: ServiceClient);
|
||||
upload(data?: ComAtprotoBlobUpload.InputSchema, opts?: ComAtprotoBlobUpload.CallOptions): Promise<ComAtprotoBlobUpload.Response>;
|
||||
}
|
||||
export declare class HandleNS {
|
||||
_service: ServiceClient;
|
||||
constructor(service: ServiceClient);
|
||||
resolve(params?: ComAtprotoHandleResolve.QueryParams, opts?: ComAtprotoHandleResolve.CallOptions): Promise<ComAtprotoHandleResolve.Response>;
|
||||
}
|
||||
export declare class RepoNS {
|
||||
_service: ServiceClient;
|
||||
constructor(service: ServiceClient);
|
||||
batchWrite(data?: ComAtprotoRepoBatchWrite.InputSchema, opts?: ComAtprotoRepoBatchWrite.CallOptions): Promise<ComAtprotoRepoBatchWrite.Response>;
|
||||
createRecord(data?: ComAtprotoRepoCreateRecord.InputSchema, opts?: ComAtprotoRepoCreateRecord.CallOptions): Promise<ComAtprotoRepoCreateRecord.Response>;
|
||||
deleteRecord(data?: ComAtprotoRepoDeleteRecord.InputSchema, opts?: ComAtprotoRepoDeleteRecord.CallOptions): Promise<ComAtprotoRepoDeleteRecord.Response>;
|
||||
describe(params?: ComAtprotoRepoDescribe.QueryParams, opts?: ComAtprotoRepoDescribe.CallOptions): Promise<ComAtprotoRepoDescribe.Response>;
|
||||
getRecord(params?: ComAtprotoRepoGetRecord.QueryParams, opts?: ComAtprotoRepoGetRecord.CallOptions): Promise<ComAtprotoRepoGetRecord.Response>;
|
||||
listRecords(params?: ComAtprotoRepoListRecords.QueryParams, opts?: ComAtprotoRepoListRecords.CallOptions): Promise<ComAtprotoRepoListRecords.Response>;
|
||||
putRecord(data?: ComAtprotoRepoPutRecord.InputSchema, opts?: ComAtprotoRepoPutRecord.CallOptions): Promise<ComAtprotoRepoPutRecord.Response>;
|
||||
}
|
||||
export declare class ServerNS {
|
||||
_service: ServiceClient;
|
||||
constructor(service: ServiceClient);
|
||||
getAccountsConfig(params?: ComAtprotoServerGetAccountsConfig.QueryParams, opts?: ComAtprotoServerGetAccountsConfig.CallOptions): Promise<ComAtprotoServerGetAccountsConfig.Response>;
|
||||
}
|
||||
export declare class SessionNS {
|
||||
_service: ServiceClient;
|
||||
constructor(service: ServiceClient);
|
||||
create(data?: ComAtprotoSessionCreate.InputSchema, opts?: ComAtprotoSessionCreate.CallOptions): Promise<ComAtprotoSessionCreate.Response>;
|
||||
delete(data?: ComAtprotoSessionDelete.InputSchema, opts?: ComAtprotoSessionDelete.CallOptions): Promise<ComAtprotoSessionDelete.Response>;
|
||||
get(params?: ComAtprotoSessionGet.QueryParams, opts?: ComAtprotoSessionGet.CallOptions): Promise<ComAtprotoSessionGet.Response>;
|
||||
refresh(data?: ComAtprotoSessionRefresh.InputSchema, opts?: ComAtprotoSessionRefresh.CallOptions): Promise<ComAtprotoSessionRefresh.Response>;
|
||||
}
|
||||
export declare class SyncNS {
|
||||
_service: ServiceClient;
|
||||
constructor(service: ServiceClient);
|
||||
getRepo(params?: ComAtprotoSyncGetRepo.QueryParams, opts?: ComAtprotoSyncGetRepo.CallOptions): Promise<ComAtprotoSyncGetRepo.Response>;
|
||||
getRoot(params?: ComAtprotoSyncGetRoot.QueryParams, opts?: ComAtprotoSyncGetRoot.CallOptions): Promise<ComAtprotoSyncGetRoot.Response>;
|
||||
updateRepo(data?: ComAtprotoSyncUpdateRepo.InputSchema, opts?: ComAtprotoSyncUpdateRepo.CallOptions): Promise<ComAtprotoSyncUpdateRepo.Response>;
|
||||
}
|
||||
export declare class AppNS {
|
||||
_service: ServiceClient;
|
||||
bsky: BskyNS;
|
||||
constructor(service: ServiceClient);
|
||||
}
|
||||
export declare class BskyNS {
|
||||
_service: ServiceClient;
|
||||
actor: ActorNS;
|
||||
feed: FeedNS;
|
||||
graph: GraphNS;
|
||||
notification: NotificationNS;
|
||||
system: SystemNS;
|
||||
constructor(service: ServiceClient);
|
||||
}
|
||||
export declare class ActorNS {
|
||||
_service: ServiceClient;
|
||||
profile: ProfileRecord;
|
||||
constructor(service: ServiceClient);
|
||||
createScene(data?: AppBskyActorCreateScene.InputSchema, opts?: AppBskyActorCreateScene.CallOptions): Promise<AppBskyActorCreateScene.Response>;
|
||||
getProfile(params?: AppBskyActorGetProfile.QueryParams, opts?: AppBskyActorGetProfile.CallOptions): Promise<AppBskyActorGetProfile.Response>;
|
||||
getSuggestions(params?: AppBskyActorGetSuggestions.QueryParams, opts?: AppBskyActorGetSuggestions.CallOptions): Promise<AppBskyActorGetSuggestions.Response>;
|
||||
search(params?: AppBskyActorSearch.QueryParams, opts?: AppBskyActorSearch.CallOptions): Promise<AppBskyActorSearch.Response>;
|
||||
searchTypeahead(params?: AppBskyActorSearchTypeahead.QueryParams, opts?: AppBskyActorSearchTypeahead.CallOptions): Promise<AppBskyActorSearchTypeahead.Response>;
|
||||
updateProfile(data?: AppBskyActorUpdateProfile.InputSchema, opts?: AppBskyActorUpdateProfile.CallOptions): Promise<AppBskyActorUpdateProfile.Response>;
|
||||
}
|
||||
export declare class ProfileRecord {
|
||||
_service: ServiceClient;
|
||||
constructor(service: ServiceClient);
|
||||
list(params: Omit<ComAtprotoRepoListRecords.QueryParams, 'collection'>): Promise<{
|
||||
cursor?: string;
|
||||
records: {
|
||||
uri: string;
|
||||
value: AppBskyActorProfile.Record;
|
||||
}[];
|
||||
}>;
|
||||
get(params: Omit<ComAtprotoRepoGetRecord.QueryParams, 'collection'>): Promise<{
|
||||
uri: string;
|
||||
cid: string;
|
||||
value: AppBskyActorProfile.Record;
|
||||
}>;
|
||||
create(params: Omit<ComAtprotoRepoCreateRecord.InputSchema, 'collection' | 'record'>, record: AppBskyActorProfile.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 FeedNS {
|
||||
_service: ServiceClient;
|
||||
post: PostRecord;
|
||||
repost: RepostRecord;
|
||||
trend: TrendRecord;
|
||||
vote: VoteRecord;
|
||||
constructor(service: ServiceClient);
|
||||
getAuthorFeed(params?: AppBskyFeedGetAuthorFeed.QueryParams, opts?: AppBskyFeedGetAuthorFeed.CallOptions): Promise<AppBskyFeedGetAuthorFeed.Response>;
|
||||
getPostThread(params?: AppBskyFeedGetPostThread.QueryParams, opts?: AppBskyFeedGetPostThread.CallOptions): Promise<AppBskyFeedGetPostThread.Response>;
|
||||
getRepostedBy(params?: AppBskyFeedGetRepostedBy.QueryParams, opts?: AppBskyFeedGetRepostedBy.CallOptions): Promise<AppBskyFeedGetRepostedBy.Response>;
|
||||
getTimeline(params?: AppBskyFeedGetTimeline.QueryParams, opts?: AppBskyFeedGetTimeline.CallOptions): Promise<AppBskyFeedGetTimeline.Response>;
|
||||
getVotes(params?: AppBskyFeedGetVotes.QueryParams, opts?: AppBskyFeedGetVotes.CallOptions): Promise<AppBskyFeedGetVotes.Response>;
|
||||
setVote(data?: AppBskyFeedSetVote.InputSchema, opts?: AppBskyFeedSetVote.CallOptions): Promise<AppBskyFeedSetVote.Response>;
|
||||
}
|
||||
export declare class PostRecord {
|
||||
_service: ServiceClient;
|
||||
constructor(service: ServiceClient);
|
||||
list(params: Omit<ComAtprotoRepoListRecords.QueryParams, 'collection'>): Promise<{
|
||||
cursor?: string;
|
||||
records: {
|
||||
uri: string;
|
||||
value: AppBskyFeedPost.Record;
|
||||
}[];
|
||||
}>;
|
||||
get(params: Omit<ComAtprotoRepoGetRecord.QueryParams, 'collection'>): Promise<{
|
||||
uri: string;
|
||||
cid: string;
|
||||
value: AppBskyFeedPost.Record;
|
||||
}>;
|
||||
create(params: Omit<ComAtprotoRepoCreateRecord.InputSchema, 'collection' | 'record'>, record: AppBskyFeedPost.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 RepostRecord {
|
||||
_service: ServiceClient;
|
||||
constructor(service: ServiceClient);
|
||||
list(params: Omit<ComAtprotoRepoListRecords.QueryParams, 'collection'>): Promise<{
|
||||
cursor?: string;
|
||||
records: {
|
||||
uri: string;
|
||||
value: AppBskyFeedRepost.Record;
|
||||
}[];
|
||||
}>;
|
||||
get(params: Omit<ComAtprotoRepoGetRecord.QueryParams, 'collection'>): Promise<{
|
||||
uri: string;
|
||||
cid: string;
|
||||
value: AppBskyFeedRepost.Record;
|
||||
}>;
|
||||
create(params: Omit<ComAtprotoRepoCreateRecord.InputSchema, 'collection' | 'record'>, record: AppBskyFeedRepost.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 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 {
|
||||
_service: ServiceClient;
|
||||
constructor(service: ServiceClient);
|
||||
list(params: Omit<ComAtprotoRepoListRecords.QueryParams, 'collection'>): Promise<{
|
||||
cursor?: string;
|
||||
records: {
|
||||
uri: string;
|
||||
value: AppBskyFeedVote.Record;
|
||||
}[];
|
||||
}>;
|
||||
get(params: Omit<ComAtprotoRepoGetRecord.QueryParams, 'collection'>): Promise<{
|
||||
uri: string;
|
||||
cid: string;
|
||||
value: AppBskyFeedVote.Record;
|
||||
}>;
|
||||
create(params: Omit<ComAtprotoRepoCreateRecord.InputSchema, 'collection' | 'record'>, record: AppBskyFeedVote.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 GraphNS {
|
||||
_service: ServiceClient;
|
||||
assertion: AssertionRecord;
|
||||
confirmation: ConfirmationRecord;
|
||||
follow: FollowRecord;
|
||||
constructor(service: ServiceClient);
|
||||
getAssertions(params?: AppBskyGraphGetAssertions.QueryParams, opts?: AppBskyGraphGetAssertions.CallOptions): Promise<AppBskyGraphGetAssertions.Response>;
|
||||
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;
|
||||
constructor(service: ServiceClient);
|
||||
list(params: Omit<ComAtprotoRepoListRecords.QueryParams, 'collection'>): Promise<{
|
||||
cursor?: string;
|
||||
records: {
|
||||
uri: string;
|
||||
value: AppBskyGraphAssertion.Record;
|
||||
}[];
|
||||
}>;
|
||||
get(params: Omit<ComAtprotoRepoGetRecord.QueryParams, 'collection'>): Promise<{
|
||||
uri: string;
|
||||
cid: string;
|
||||
value: AppBskyGraphAssertion.Record;
|
||||
}>;
|
||||
create(params: Omit<ComAtprotoRepoCreateRecord.InputSchema, 'collection' | 'record'>, record: AppBskyGraphAssertion.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 ConfirmationRecord {
|
||||
_service: ServiceClient;
|
||||
constructor(service: ServiceClient);
|
||||
list(params: Omit<ComAtprotoRepoListRecords.QueryParams, 'collection'>): Promise<{
|
||||
cursor?: string;
|
||||
records: {
|
||||
uri: string;
|
||||
value: AppBskyGraphConfirmation.Record;
|
||||
}[];
|
||||
}>;
|
||||
get(params: Omit<ComAtprotoRepoGetRecord.QueryParams, 'collection'>): Promise<{
|
||||
uri: string;
|
||||
cid: string;
|
||||
value: AppBskyGraphConfirmation.Record;
|
||||
}>;
|
||||
create(params: Omit<ComAtprotoRepoCreateRecord.InputSchema, 'collection' | 'record'>, record: AppBskyGraphConfirmation.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 FollowRecord {
|
||||
_service: ServiceClient;
|
||||
constructor(service: ServiceClient);
|
||||
list(params: Omit<ComAtprotoRepoListRecords.QueryParams, 'collection'>): Promise<{
|
||||
cursor?: string;
|
||||
records: {
|
||||
uri: string;
|
||||
value: AppBskyGraphFollow.Record;
|
||||
}[];
|
||||
}>;
|
||||
get(params: Omit<ComAtprotoRepoGetRecord.QueryParams, 'collection'>): Promise<{
|
||||
uri: string;
|
||||
cid: string;
|
||||
value: AppBskyGraphFollow.Record;
|
||||
}>;
|
||||
create(params: Omit<ComAtprotoRepoCreateRecord.InputSchema, 'collection' | 'record'>, record: AppBskyGraphFollow.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 NotificationNS {
|
||||
_service: ServiceClient;
|
||||
constructor(service: ServiceClient);
|
||||
getCount(params?: AppBskyNotificationGetCount.QueryParams, opts?: AppBskyNotificationGetCount.CallOptions): Promise<AppBskyNotificationGetCount.Response>;
|
||||
list(params?: AppBskyNotificationList.QueryParams, opts?: AppBskyNotificationList.CallOptions): Promise<AppBskyNotificationList.Response>;
|
||||
updateSeen(data?: AppBskyNotificationUpdateSeen.InputSchema, opts?: AppBskyNotificationUpdateSeen.CallOptions): Promise<AppBskyNotificationUpdateSeen.Response>;
|
||||
}
|
||||
export declare class SystemNS {
|
||||
_service: ServiceClient;
|
||||
declaration: DeclarationRecord;
|
||||
constructor(service: ServiceClient);
|
||||
}
|
||||
export declare class DeclarationRecord {
|
||||
_service: ServiceClient;
|
||||
constructor(service: ServiceClient);
|
||||
list(params: Omit<ComAtprotoRepoListRecords.QueryParams, 'collection'>): Promise<{
|
||||
cursor?: string;
|
||||
records: {
|
||||
uri: string;
|
||||
value: AppBskySystemDeclaration.Record;
|
||||
}[];
|
||||
}>;
|
||||
get(params: Omit<ComAtprotoRepoGetRecord.QueryParams, 'collection'>): Promise<{
|
||||
uri: string;
|
||||
cid: string;
|
||||
value: AppBskySystemDeclaration.Record;
|
||||
}>;
|
||||
create(params: Omit<ComAtprotoRepoCreateRecord.InputSchema, 'collection' | 'record'>, record: AppBskySystemDeclaration.Record, headers?: Record<string, string>): Promise<{
|
||||
uri: string;
|
||||
cid: string;
|
||||
}>;
|
||||
delete(params: Omit<ComAtprotoRepoDeleteRecord.InputSchema, 'collection'>, headers?: Record<string, string>): Promise<void>;
|
||||
}
|
||||
-64
@@ -1,64 +0,0 @@
|
||||
import { LexiconDoc } from '@atproto/lexicon';
|
||||
export declare const lexicons: LexiconDoc[];
|
||||
export declare const ids: {
|
||||
ComAtprotoAccountCreate: string;
|
||||
ComAtprotoAccountCreateInviteCode: string;
|
||||
ComAtprotoAccountDelete: string;
|
||||
ComAtprotoAccountGet: string;
|
||||
ComAtprotoAccountRequestPasswordReset: string;
|
||||
ComAtprotoAccountResetPassword: string;
|
||||
ComAtprotoBlobUpload: string;
|
||||
ComAtprotoHandleResolve: string;
|
||||
ComAtprotoRepoBatchWrite: string;
|
||||
ComAtprotoRepoCreateRecord: string;
|
||||
ComAtprotoRepoDeleteRecord: string;
|
||||
ComAtprotoRepoDescribe: string;
|
||||
ComAtprotoRepoGetRecord: string;
|
||||
ComAtprotoRepoListRecords: string;
|
||||
ComAtprotoRepoPutRecord: string;
|
||||
ComAtprotoRepoStrongRef: string;
|
||||
ComAtprotoServerGetAccountsConfig: string;
|
||||
ComAtprotoSessionCreate: string;
|
||||
ComAtprotoSessionDelete: string;
|
||||
ComAtprotoSessionGet: string;
|
||||
ComAtprotoSessionRefresh: string;
|
||||
ComAtprotoSyncGetRepo: string;
|
||||
ComAtprotoSyncGetRoot: string;
|
||||
ComAtprotoSyncUpdateRepo: string;
|
||||
AppBskyActorCreateScene: string;
|
||||
AppBskyActorGetProfile: string;
|
||||
AppBskyActorGetSuggestions: string;
|
||||
AppBskyActorProfile: string;
|
||||
AppBskyActorRef: string;
|
||||
AppBskyActorSearch: string;
|
||||
AppBskyActorSearchTypeahead: string;
|
||||
AppBskyActorUpdateProfile: string;
|
||||
AppBskyFeedEmbed: string;
|
||||
AppBskyFeedGetAuthorFeed: string;
|
||||
AppBskyFeedGetPostThread: string;
|
||||
AppBskyFeedGetRepostedBy: string;
|
||||
AppBskyFeedGetTimeline: string;
|
||||
AppBskyFeedGetVotes: string;
|
||||
AppBskyFeedPost: string;
|
||||
AppBskyFeedRepost: string;
|
||||
AppBskyFeedSetVote: string;
|
||||
AppBskyFeedTrend: string;
|
||||
AppBskyFeedVote: string;
|
||||
AppBskyGraphAssertCreator: string;
|
||||
AppBskyGraphAssertMember: string;
|
||||
AppBskyGraphAssertion: string;
|
||||
AppBskyGraphConfirmation: string;
|
||||
AppBskyGraphFollow: string;
|
||||
AppBskyGraphGetAssertions: string;
|
||||
AppBskyGraphGetFollowers: string;
|
||||
AppBskyGraphGetFollows: string;
|
||||
AppBskyGraphGetMembers: string;
|
||||
AppBskyGraphGetMemberships: string;
|
||||
AppBskyNotificationGetCount: string;
|
||||
AppBskyNotificationList: string;
|
||||
AppBskyNotificationUpdateSeen: string;
|
||||
AppBskySystemActorScene: string;
|
||||
AppBskySystemActorUser: string;
|
||||
AppBskySystemDeclRef: string;
|
||||
AppBskySystemDeclaration: string;
|
||||
};
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
import { MethodSchema, RecordSchema } from '@atproto/lexicon';
|
||||
export declare const methodSchemaDict: Record<string, MethodSchema>;
|
||||
export declare const methodSchemas: MethodSchema[];
|
||||
export declare const ids: {
|
||||
AppBskyActorProfile: string;
|
||||
AppBskyFeedMediaEmbed: string;
|
||||
AppBskyFeedPost: string;
|
||||
AppBskyFeedRepost: string;
|
||||
AppBskyFeedTrend: string;
|
||||
AppBskyFeedVote: string;
|
||||
AppBskyGraphAssertion: string;
|
||||
AppBskyGraphConfirmation: string;
|
||||
AppBskyGraphFollow: string;
|
||||
AppBskySystemDeclaration: string;
|
||||
};
|
||||
export declare const recordSchemaDict: Record<string, RecordSchema>;
|
||||
export declare const recordSchemas: RecordSchema[];
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Headers, XRPCError } from '@atproto/xrpc';
|
||||
import * as AppBskySystemDeclRef from '../system/declRef';
|
||||
export interface QueryParams {
|
||||
}
|
||||
export interface InputSchema {
|
||||
handle: string;
|
||||
recoveryKey?: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface OutputSchema {
|
||||
handle: string;
|
||||
did: string;
|
||||
declaration: AppBskySystemDeclRef.Main;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface CallOptions {
|
||||
headers?: Headers;
|
||||
qp?: QueryParams;
|
||||
encoding: 'application/json';
|
||||
}
|
||||
export interface Response {
|
||||
success: boolean;
|
||||
headers: Headers;
|
||||
data: OutputSchema;
|
||||
}
|
||||
export declare class InvalidHandleError extends XRPCError {
|
||||
constructor(src: XRPCError);
|
||||
}
|
||||
export declare class HandleNotAvailableError extends XRPCError {
|
||||
constructor(src: XRPCError);
|
||||
}
|
||||
export declare function toKnownErr(e: any): any;
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Headers } from '@atproto/xrpc';
|
||||
import * as AppBskySystemDeclRef from '../system/declRef';
|
||||
export interface QueryParams {
|
||||
actor: string;
|
||||
}
|
||||
export declare type InputSchema = undefined;
|
||||
export interface OutputSchema {
|
||||
did: string;
|
||||
declaration: AppBskySystemDeclRef.Main;
|
||||
handle: string;
|
||||
creator: string;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
avatar?: string;
|
||||
followersCount: number;
|
||||
followsCount: number;
|
||||
membersCount: number;
|
||||
postsCount: number;
|
||||
myState?: MyState;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface CallOptions {
|
||||
headers?: Headers;
|
||||
}
|
||||
export interface Response {
|
||||
success: boolean;
|
||||
headers: Headers;
|
||||
data: OutputSchema;
|
||||
}
|
||||
export declare function toKnownErr(e: any): any;
|
||||
export interface MyState {
|
||||
follow?: string;
|
||||
member?: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Headers } from '@atproto/xrpc';
|
||||
import * as AppBskySystemDeclRef from '../system/declRef';
|
||||
export interface QueryParams {
|
||||
limit?: number;
|
||||
cursor?: string;
|
||||
}
|
||||
export declare type InputSchema = undefined;
|
||||
export interface OutputSchema {
|
||||
cursor?: string;
|
||||
actors: Actor[];
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface CallOptions {
|
||||
headers?: Headers;
|
||||
}
|
||||
export interface Response {
|
||||
success: boolean;
|
||||
headers: Headers;
|
||||
data: OutputSchema;
|
||||
}
|
||||
export declare function toKnownErr(e: any): any;
|
||||
export interface Actor {
|
||||
did: string;
|
||||
declaration: AppBskySystemDeclRef.Main;
|
||||
handle: string;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
avatar?: string;
|
||||
indexedAt?: string;
|
||||
myState?: MyState;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface MyState {
|
||||
follow?: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
export interface Record {
|
||||
displayName: string;
|
||||
description?: string;
|
||||
avatar?: {
|
||||
cid: string;
|
||||
mimeType: string;
|
||||
[k: string]: unknown;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import * as AppBskySystemDeclRef from '../system/declRef';
|
||||
export interface Main {
|
||||
did: string;
|
||||
declarationCid: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface WithInfo {
|
||||
did: string;
|
||||
declaration: AppBskySystemDeclRef.Main;
|
||||
handle: string;
|
||||
displayName?: string;
|
||||
avatar?: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Headers } from '@atproto/xrpc';
|
||||
import * as AppBskySystemDeclRef from '../system/declRef';
|
||||
export interface QueryParams {
|
||||
term: string;
|
||||
limit?: number;
|
||||
before?: string;
|
||||
}
|
||||
export declare type InputSchema = undefined;
|
||||
export interface OutputSchema {
|
||||
cursor?: string;
|
||||
users: User[];
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface CallOptions {
|
||||
headers?: Headers;
|
||||
}
|
||||
export interface Response {
|
||||
success: boolean;
|
||||
headers: Headers;
|
||||
data: OutputSchema;
|
||||
}
|
||||
export declare function toKnownErr(e: any): any;
|
||||
export interface User {
|
||||
did: string;
|
||||
declaration: AppBskySystemDeclRef.Main;
|
||||
handle: string;
|
||||
displayName?: string;
|
||||
avatar?: string;
|
||||
description?: string;
|
||||
indexedAt?: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { Headers } from '@atproto/xrpc';
|
||||
import * as AppBskySystemDeclRef from '../system/declRef';
|
||||
export interface QueryParams {
|
||||
term: string;
|
||||
limit?: number;
|
||||
}
|
||||
export declare type InputSchema = undefined;
|
||||
export interface OutputSchema {
|
||||
users: User[];
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface CallOptions {
|
||||
headers?: Headers;
|
||||
}
|
||||
export interface Response {
|
||||
success: boolean;
|
||||
headers: Headers;
|
||||
data: OutputSchema;
|
||||
}
|
||||
export declare function toKnownErr(e: any): any;
|
||||
export interface User {
|
||||
did: string;
|
||||
declaration: AppBskySystemDeclRef.Main;
|
||||
handle: string;
|
||||
displayName?: string;
|
||||
avatar?: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { Headers } from '@atproto/xrpc';
|
||||
export interface QueryParams {
|
||||
}
|
||||
export interface InputSchema {
|
||||
did?: string;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
avatar?: {
|
||||
cid: string;
|
||||
mimeType: string;
|
||||
[k: string]: unknown;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface OutputSchema {
|
||||
uri: string;
|
||||
cid: string;
|
||||
record: {};
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface CallOptions {
|
||||
headers?: Headers;
|
||||
qp?: QueryParams;
|
||||
encoding: 'application/json';
|
||||
}
|
||||
export interface Response {
|
||||
success: boolean;
|
||||
headers: Headers;
|
||||
data: OutputSchema;
|
||||
}
|
||||
export declare function toKnownErr(e: any): any;
|
||||
@@ -1,57 +0,0 @@
|
||||
export interface Record {
|
||||
text: string;
|
||||
entities?: Entity[];
|
||||
reply?: Reply;
|
||||
createdAt: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export declare const SOMETOKEN = "app.bsky.feed.debug#someToken";
|
||||
export declare type Unknown = {};
|
||||
export declare type Boolean = boolean;
|
||||
export declare type Number = number;
|
||||
export declare type Integer = number;
|
||||
export declare type String = string;
|
||||
export declare type Blob = {
|
||||
cid: string;
|
||||
mimeType: string;
|
||||
[k: string]: unknown;
|
||||
};
|
||||
export declare type Image = {
|
||||
cid: string;
|
||||
mimeType: string;
|
||||
[k: string]: unknown;
|
||||
};
|
||||
export declare type Video = {
|
||||
cid: string;
|
||||
mimeType: string;
|
||||
[k: string]: unknown;
|
||||
};
|
||||
export declare type Audio = {
|
||||
cid: string;
|
||||
mimeType: string;
|
||||
[k: string]: unknown;
|
||||
};
|
||||
export declare type StringArr = string[];
|
||||
export declare type ReplyArr = Reply[];
|
||||
export declare type MultiArr = (Reply | PostRef | Entity)[];
|
||||
export interface Reply {
|
||||
root: PostRef;
|
||||
parent: PostRef;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface PostRef {
|
||||
uri: string;
|
||||
cid: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface Entity {
|
||||
index: TextSlice;
|
||||
type: string;
|
||||
value: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface TextSlice {
|
||||
start: number;
|
||||
end: number;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import * as AppBskyActorRef from '../actor/ref';
|
||||
export interface Main {
|
||||
items?: (Media | Record | External | {
|
||||
$type: string;
|
||||
[k: string]: unknown;
|
||||
})[];
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface Media {
|
||||
alt?: string;
|
||||
thumb?: {
|
||||
cid: string;
|
||||
mimeType: string;
|
||||
[k: string]: unknown;
|
||||
};
|
||||
original: {
|
||||
cid: string;
|
||||
mimeType: string;
|
||||
[k: string]: unknown;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface Record {
|
||||
type: 'record';
|
||||
author: AppBskyActorRef.WithInfo;
|
||||
record: {};
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface External {
|
||||
type: 'external';
|
||||
uri: string;
|
||||
title: string;
|
||||
description: string;
|
||||
imageUri: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { Headers } from '@atproto/xrpc';
|
||||
import * as AppBskyActorRef from '../actor/ref';
|
||||
import * as AppBskyFeedEmbed from './embed';
|
||||
export interface QueryParams {
|
||||
author: string;
|
||||
limit?: number;
|
||||
before?: string;
|
||||
}
|
||||
export declare type InputSchema = undefined;
|
||||
export interface OutputSchema {
|
||||
cursor?: string;
|
||||
feed: FeedItem[];
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface CallOptions {
|
||||
headers?: Headers;
|
||||
}
|
||||
export interface Response {
|
||||
success: boolean;
|
||||
headers: Headers;
|
||||
data: OutputSchema;
|
||||
}
|
||||
export declare function toKnownErr(e: any): any;
|
||||
export interface FeedItem {
|
||||
uri: string;
|
||||
cid: string;
|
||||
author: AppBskyActorRef.WithInfo;
|
||||
trendedBy?: AppBskyActorRef.WithInfo;
|
||||
repostedBy?: AppBskyActorRef.WithInfo;
|
||||
record: {};
|
||||
embed?: AppBskyFeedEmbed.Main;
|
||||
replyCount: number;
|
||||
repostCount: number;
|
||||
upvoteCount: number;
|
||||
downvoteCount: number;
|
||||
indexedAt: string;
|
||||
myState?: MyState;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface MyState {
|
||||
repost?: string;
|
||||
upvote?: string;
|
||||
downvote?: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Headers } from '@atproto/xrpc';
|
||||
export interface QueryParams {
|
||||
uri: string;
|
||||
cid?: string;
|
||||
limit?: number;
|
||||
before?: string;
|
||||
}
|
||||
export interface CallOptions {
|
||||
headers?: Headers;
|
||||
}
|
||||
export declare type InputSchema = undefined;
|
||||
export interface OutputSchema {
|
||||
uri: string;
|
||||
cid?: string;
|
||||
cursor?: string;
|
||||
likedBy: {
|
||||
did: string;
|
||||
handle: string;
|
||||
displayName?: string;
|
||||
createdAt?: string;
|
||||
indexedAt: string;
|
||||
}[];
|
||||
}
|
||||
export interface Response {
|
||||
success: boolean;
|
||||
headers: Headers;
|
||||
data: OutputSchema;
|
||||
}
|
||||
export declare function toKnownErr(e: any): any;
|
||||
@@ -1,60 +0,0 @@
|
||||
import { Headers, XRPCError } from '@atproto/xrpc';
|
||||
import * as AppBskyActorRef from '../actor/ref';
|
||||
import * as AppBskyFeedEmbed from './embed';
|
||||
export interface QueryParams {
|
||||
uri: string;
|
||||
depth?: number;
|
||||
}
|
||||
export declare type InputSchema = undefined;
|
||||
export interface OutputSchema {
|
||||
thread: Post | NotFoundPost | {
|
||||
$type: string;
|
||||
[k: string]: unknown;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface CallOptions {
|
||||
headers?: Headers;
|
||||
}
|
||||
export interface Response {
|
||||
success: boolean;
|
||||
headers: Headers;
|
||||
data: OutputSchema;
|
||||
}
|
||||
export declare class NotFoundError extends XRPCError {
|
||||
constructor(src: XRPCError);
|
||||
}
|
||||
export declare function toKnownErr(e: any): any;
|
||||
export interface Post {
|
||||
uri: string;
|
||||
cid: string;
|
||||
author: AppBskyActorRef.WithInfo;
|
||||
record: {};
|
||||
embed?: AppBskyFeedEmbed.Main;
|
||||
parent?: Post | NotFoundPost | {
|
||||
$type: string;
|
||||
[k: string]: unknown;
|
||||
};
|
||||
replyCount: number;
|
||||
replies?: (Post | NotFoundPost | {
|
||||
$type: string;
|
||||
[k: string]: unknown;
|
||||
})[];
|
||||
repostCount: number;
|
||||
upvoteCount: number;
|
||||
downvoteCount: number;
|
||||
indexedAt: string;
|
||||
myState?: MyState;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface NotFoundPost {
|
||||
uri: string;
|
||||
notFound: true;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface MyState {
|
||||
repost?: string;
|
||||
upvote?: string;
|
||||
downvote?: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Headers } from '@atproto/xrpc';
|
||||
import * as AppBskySystemDeclRef from '../system/declRef';
|
||||
export interface QueryParams {
|
||||
uri: string;
|
||||
cid?: string;
|
||||
limit?: number;
|
||||
before?: string;
|
||||
}
|
||||
export declare type InputSchema = undefined;
|
||||
export interface OutputSchema {
|
||||
uri: string;
|
||||
cid?: string;
|
||||
cursor?: string;
|
||||
repostedBy: RepostedBy[];
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface CallOptions {
|
||||
headers?: Headers;
|
||||
}
|
||||
export interface Response {
|
||||
success: boolean;
|
||||
headers: Headers;
|
||||
data: OutputSchema;
|
||||
}
|
||||
export declare function toKnownErr(e: any): any;
|
||||
export interface RepostedBy {
|
||||
did: string;
|
||||
declaration: AppBskySystemDeclRef.Main;
|
||||
handle: string;
|
||||
displayName?: string;
|
||||
avatar?: string;
|
||||
createdAt?: string;
|
||||
indexedAt: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { Headers } from '@atproto/xrpc';
|
||||
import * as AppBskyActorRef from '../actor/ref';
|
||||
import * as AppBskyFeedEmbed from './embed';
|
||||
export interface QueryParams {
|
||||
algorithm?: string;
|
||||
limit?: number;
|
||||
before?: string;
|
||||
}
|
||||
export declare type InputSchema = undefined;
|
||||
export interface OutputSchema {
|
||||
cursor?: string;
|
||||
feed: FeedItem[];
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface CallOptions {
|
||||
headers?: Headers;
|
||||
}
|
||||
export interface Response {
|
||||
success: boolean;
|
||||
headers: Headers;
|
||||
data: OutputSchema;
|
||||
}
|
||||
export declare function toKnownErr(e: any): any;
|
||||
export interface FeedItem {
|
||||
uri: string;
|
||||
cid: string;
|
||||
author: AppBskyActorRef.WithInfo;
|
||||
trendedBy?: AppBskyActorRef.WithInfo;
|
||||
repostedBy?: AppBskyActorRef.WithInfo;
|
||||
record: {};
|
||||
embed?: AppBskyFeedEmbed.Main;
|
||||
replyCount: number;
|
||||
repostCount: number;
|
||||
upvoteCount: number;
|
||||
downvoteCount: number;
|
||||
indexedAt: string;
|
||||
myState?: MyState;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface MyState {
|
||||
repost?: string;
|
||||
upvote?: string;
|
||||
downvote?: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { Headers } from '@atproto/xrpc';
|
||||
import * as AppBskyActorRef from '../actor/ref';
|
||||
export interface QueryParams {
|
||||
uri: string;
|
||||
cid?: string;
|
||||
direction?: 'up' | 'down';
|
||||
limit?: number;
|
||||
before?: string;
|
||||
}
|
||||
export declare type InputSchema = undefined;
|
||||
export interface OutputSchema {
|
||||
uri: string;
|
||||
cid?: string;
|
||||
cursor?: string;
|
||||
votes: Vote[];
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface CallOptions {
|
||||
headers?: Headers;
|
||||
}
|
||||
export interface Response {
|
||||
success: boolean;
|
||||
headers: Headers;
|
||||
data: OutputSchema;
|
||||
}
|
||||
export declare function toKnownErr(e: any): any;
|
||||
export interface Vote {
|
||||
direction: 'up' | 'down';
|
||||
indexedAt: string;
|
||||
createdAt: string;
|
||||
actor: AppBskyActorRef.WithInfo;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
export interface Record {
|
||||
subject: Subject;
|
||||
createdAt: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface Subject {
|
||||
uri: string;
|
||||
cid: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
export interface Main {
|
||||
media: MediaEmbed[];
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface MediaEmbed {
|
||||
alt?: string;
|
||||
thumb?: {
|
||||
cid: string;
|
||||
mimeType: string;
|
||||
[k: string]: unknown;
|
||||
};
|
||||
original: {
|
||||
cid: string;
|
||||
mimeType: string;
|
||||
[k: string]: unknown;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef';
|
||||
export interface Record {
|
||||
text: string;
|
||||
entities?: Entity[];
|
||||
reply?: ReplyRef;
|
||||
createdAt: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface ReplyRef {
|
||||
root: ComAtprotoRepoStrongRef.Main;
|
||||
parent: ComAtprotoRepoStrongRef.Main;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface Entity {
|
||||
index: TextSlice;
|
||||
type: string;
|
||||
value: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
export interface TextSlice {
|
||||
start: number;
|
||||
end: number;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef';
|
||||
export interface Record {
|
||||
subject: ComAtprotoRepoStrongRef.Main;
|
||||
createdAt: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user