Compare commits

..

12 Commits

Author SHA1 Message Date
João Ferreiro 7086b4a68c Revert "hotfix"
This reverts commit cd5175c663.
2022-12-13 16:30:46 +00:00
João Ferreiro cd5175c663 hotfix 2022-12-13 16:30:10 +00:00
João Ferreiro ed56365cc0 hotfix 2022-12-13 16:13:47 +00:00
João Ferreiro afea10e5dd hotfix 2022-12-13 16:00:13 +00:00
João Ferreiro 10360ecded update lib setup 2022-12-13 15:51:25 +00:00
João Ferreiro fe239abd26 adding more register usage (utils) 2022-12-13 15:14:42 +00:00
João Ferreiro 50818d73f6 adding more register usage 2022-12-13 11:20:02 +00:00
João Ferreiro 7021ea2745 remove release flag 2022-12-12 18:07:48 +00:00
João Ferreiro 441dd29685 using register 2022-12-12 18:07:30 +00:00
João Ferreiro 89ddcdfa52 Merge branch 'main' into improve-cold-boot-time 2022-12-12 16:59:03 +00:00
João Ferreiro a4422f1819 Revert "using register"
This reverts commit e28093ec35.
2022-12-12 16:58:33 +00:00
João Ferreiro e28093ec35 using register 2022-12-12 16:30:52 +00:00
389 changed files with 23295 additions and 19796 deletions
+4 -8
View File
@@ -2,18 +2,14 @@ module.exports = {
root: true,
extends: '@react-native-community',
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
ignorePatterns: [
'**/__mocks__/*.ts',
'src/third-party',
'ios',
'android',
'coverage',
],
// plugins: ['@typescript-eslint'],
overrides: [
{
files: ['*.js', '*.mjs', '*.ts', '*.tsx'],
rules: {
'@typescript-eslint/no-shadow': 'off',
'no-shadow': 'off',
'no-undef': 'off',
semi: [2, 'never'],
},
},
-38
View File
@@ -1,38 +0,0 @@
name: Lint
on:
pull_request:
branches:
- main
jobs:
linting:
name: Run linters
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v2
- name: Yarn install
run: yarn
- name: Lint Reporter
uses: wearerequired/lint-action@v2.2.0
with:
eslint: true
prettier: true
- name: Typescript & Lint check
run: yarn lint
testing:
name: Run tests
runs-on: ubuntu-latest
steps:
- name: Install node 18
uses: actions/setup-node@v3
with:
node-version: 18
- name: Check out Git repository
uses: actions/checkout@v2
- name: Yarn install
run: yarn
- name: Run tests
run: |
yarn test --forceExit
-4
View File
@@ -59,7 +59,3 @@ buck-out/
# Ruby / CocoaPods
/ios/Pods/
/vendor/bundle/
# Testing
coverage/
junit.xml
-5
View File
@@ -1,5 +0,0 @@
ios
android
src/third-party
src/app.json
public
+1 -1
View File
@@ -1 +1 @@
2.7.6
2.7.4
+2 -2
View File
@@ -1,6 +1,6 @@
source 'https://rubygems.org'
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
ruby File.read(File.join(__dir__, '.ruby-version')).strip
ruby '2.7.4'
gem 'cocoapods', '~> 1.11', '>= 1.11.3'
gem 'cocoapods', '~> 1.11', '>= 1.11.2'
+2 -4
View File
@@ -16,10 +16,8 @@ Uses:
- After initial setup:
- `cd ios ; pod install`
- Start the dev servers
- `git clone git@github.com:bluesky-social/atproto.git`
- `cd atproto`
- `yarn`
- `cd packages/dev-env && yarn start`
- `yarn dev-pds`
- `yarn dev-wallet`
- Run the dev app
- iOS: `yarn ios`
- Android: `yarn android`
-57
View File
@@ -1,57 +0,0 @@
import React from 'react'
import {View, ScrollView, Modal, FlatList, TextInput} from 'react-native'
const BottomSheetModalContext = React.createContext(null)
const BottomSheetModalProvider = (props: any) => {
return <BottomSheetModalContext.Provider {...props} value={{}} />
}
class BottomSheet extends React.Component {
snapToIndex() {}
snapToPosition() {}
expand() {}
collapse() {}
close() {
this.props.onClose?.()
}
forceClose() {}
render() {
return <View>{this.props.children}</View>
}
}
const BottomSheetModal = (props: any) => <Modal {...props} />
const BottomSheetBackdrop = (props: any) => <View {...props} />
const BottomSheetHandle = (props: any) => <View {...props} />
const BottomSheetFooter = (props: any) => <View {...props} />
const BottomSheetScrollView = (props: any) => <ScrollView {...props} />
const BottomSheetFlatList = (props: any) => <FlatList {...props} />
const BottomSheetTextInput = (props: any) => <TextInput {...props} />
const useBottomSheet = jest.fn()
const useBottomSheetModal = jest.fn()
const useBottomSheetSpringConfigs = jest.fn()
const useBottomSheetTimingConfigs = jest.fn()
const useBottomSheetInternal = jest.fn()
const useBottomSheetDynamicSnapPoints = jest.fn()
export {useBottomSheet}
export {useBottomSheetModal}
export {useBottomSheetSpringConfigs}
export {useBottomSheetTimingConfigs}
export {useBottomSheetInternal}
export {useBottomSheetDynamicSnapPoints}
export {
BottomSheetModalProvider,
BottomSheetBackdrop,
BottomSheetHandle,
BottomSheetModal,
BottomSheetFooter,
BottomSheetScrollView,
BottomSheetFlatList,
BottomSheetTextInput,
}
export default BottomSheet
-6
View File
@@ -1,6 +0,0 @@
export default {
requestPermission: jest.fn(),
onForegroundEvent: jest.fn(),
setBadgeCount: jest.fn(),
displayNotification: jest.fn(),
}
@@ -1,9 +0,0 @@
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
View File
@@ -0,0 +1 @@
export default from '@react-native-async-storage/async-storage/jest/async-storage-mock'
@@ -1,4 +0,0 @@
export default {
configure: jest.fn().mockResolvedValue(0),
finish: jest.fn(),
}
-1
View File
@@ -1 +0,0 @@
export default {}
-9
View File
@@ -1,9 +0,0 @@
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: ''}))
-10
View File
@@ -1,10 +0,0 @@
jest.mock('rn-fetch-blob', () => {
return {
__esModule: true,
default: {
fs: {
unlink: jest.fn(),
},
},
}
})
-937
View File
@@ -1,937 +0,0 @@
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().mockResolvedValue({}),
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().mockResolvedValue({}),
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().mockResolvedValue({}),
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'),
registerListeners: jest.fn().mockReturnValue(jest.fn()),
// 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(),
toNotifeeOpts: 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().mockResolvedValue({}),
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().mockResolvedValue({}),
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(),
fetchNotifications: 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: {
actor: {
searchTypeahead: jest.fn().mockResolvedValue({data: {users: []}}),
},
graph: {
getFollows: jest.fn().mockResolvedValue({data: {follows: []}}),
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().mockResolvedValue({}),
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().mockResolvedValue({}),
// 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().mockResolvedValue({}),
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().mockResolvedValue({}),
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
+16
View File
@@ -0,0 +1,16 @@
// /**
// * @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 />)
// })
// })
-241
View File
@@ -1,241 +0,0 @@
import React from 'react'
import {MobileShell} from '../src/view/shell/mobile'
import {cleanup, fireEvent, render, waitFor} from '../jest/test-utils'
import {createServer, TestPDS} from '../jest/test-pds'
import {RootStoreModel, setupState} from '../src/state'
const WAIT_OPTS = {timeout: 5e3}
describe('Account flows', () => {
let pds: TestPDS | undefined
let rootStore: RootStoreModel | undefined
beforeAll(async () => {
jest.useFakeTimers()
pds = await createServer()
rootStore = await setupState(pds.pdsUrl)
})
afterAll(async () => {
jest.clearAllMocks()
cleanup()
await pds?.close()
})
it('renders initial screen', () => {
const {getByTestId} = render(<MobileShell />, rootStore)
const signUpScreen = getByTestId('signinOrCreateAccount')
expect(signUpScreen).toBeTruthy()
})
it('completes signin to the server', async () => {
const {getByTestId} = render(<MobileShell />, rootStore)
// move to signin view
fireEvent.press(getByTestId('signInButton'))
expect(getByTestId('signIn')).toBeTruthy()
expect(getByTestId('loginForm')).toBeTruthy()
// input the target server
expect(getByTestId('loginSelectServiceButton')).toBeTruthy()
fireEvent.press(getByTestId('loginSelectServiceButton'))
expect(getByTestId('serverInputModal')).toBeTruthy()
fireEvent.changeText(
getByTestId('customServerTextInput'),
pds?.pdsUrl || '',
)
fireEvent.press(getByTestId('customServerSelectBtn'))
await waitFor(() => {
expect(getByTestId('loginUsernameInput')).toBeTruthy()
}, WAIT_OPTS)
// enter username & pass
fireEvent.changeText(getByTestId('loginUsernameInput'), 'alice')
fireEvent.changeText(getByTestId('loginPasswordInput'), 'hunter2')
await waitFor(() => {
expect(getByTestId('loginNextButton')).toBeTruthy()
}, WAIT_OPTS)
fireEvent.press(getByTestId('loginNextButton'))
// signed in
await waitFor(() => {
expect(getByTestId('homeFeed')).toBeTruthy()
expect(rootStore?.me?.displayName).toBe('Alice')
expect(rootStore?.me?.handle).toBe('alice.test')
expect(rootStore?.session.accounts.length).toBe(1)
}, WAIT_OPTS)
expect(rootStore?.me?.displayName).toBe('Alice')
expect(rootStore?.me?.handle).toBe('alice.test')
expect(rootStore?.session.accounts.length).toBe(1)
})
it('opens the login screen when "add account" is pressed', async () => {
const {getByTestId, getAllByTestId} = render(<MobileShell />, rootStore)
await waitFor(() => expect(getByTestId('homeFeed')).toBeTruthy(), WAIT_OPTS)
// open side menu
fireEvent.press(getAllByTestId('viewHeaderBackOrMenuBtn')[0])
await waitFor(() => expect(getByTestId('menuView')).toBeTruthy(), WAIT_OPTS)
// nav to settings
fireEvent.press(getByTestId('menuItemButton-Settings'))
await waitFor(
() => expect(getByTestId('settingsScreen')).toBeTruthy(),
WAIT_OPTS,
)
// press '+ new account' in switcher
fireEvent.press(getByTestId('switchToNewAccountBtn'))
await waitFor(
() => expect(getByTestId('signinOrCreateAccount')).toBeTruthy(),
WAIT_OPTS,
)
})
it('shows the "choose account" form when a previous session has been created', async () => {
const {getByTestId} = render(<MobileShell />, rootStore)
// move to signin view
fireEvent.press(getByTestId('signInButton'))
expect(getByTestId('signIn')).toBeTruthy()
expect(getByTestId('chooseAccountForm')).toBeTruthy()
})
it('logs directly into the account due to still possessing session tokens', async () => {
const {getByTestId} = render(<MobileShell />, rootStore)
// move to signin view
fireEvent.press(getByTestId('signInButton'))
expect(getByTestId('signIn')).toBeTruthy()
expect(getByTestId('chooseAccountForm')).toBeTruthy()
// select the previous account
fireEvent.press(getByTestId('chooseAccountBtn-alice.test'))
// signs in immediately
await waitFor(() => {
expect(getByTestId('homeFeed')).toBeTruthy()
expect(rootStore?.me?.displayName).toBe('Alice')
expect(rootStore?.me?.handle).toBe('alice.test')
expect(rootStore?.session.accounts.length).toBe(1)
}, WAIT_OPTS)
expect(rootStore?.me?.displayName).toBe('Alice')
expect(rootStore?.me?.handle).toBe('alice.test')
expect(rootStore?.session.accounts.length).toBe(1)
})
it('logs into a second account via the switcher', async () => {
const {getByTestId, getAllByTestId} = render(<MobileShell />, rootStore)
await waitFor(() => expect(getByTestId('homeFeed')).toBeTruthy(), WAIT_OPTS)
// open side menu
fireEvent.press(getAllByTestId('viewHeaderBackOrMenuBtn')[0])
await waitFor(() => expect(getByTestId('menuView')).toBeTruthy(), WAIT_OPTS)
// nav to settings
fireEvent.press(getByTestId('menuItemButton-Settings'))
await waitFor(
() => expect(getByTestId('settingsScreen')).toBeTruthy(),
WAIT_OPTS,
)
// press '+ new account' in switcher
fireEvent.press(getByTestId('switchToNewAccountBtn'))
await waitFor(
() => expect(getByTestId('signinOrCreateAccount')).toBeTruthy(),
WAIT_OPTS,
)
// move to signin view
fireEvent.press(getByTestId('signInButton'))
expect(getByTestId('signIn')).toBeTruthy()
expect(getByTestId('chooseAccountForm')).toBeTruthy()
// select a new account
fireEvent.press(getByTestId('chooseNewAccountBtn'))
expect(getByTestId('loginForm')).toBeTruthy()
// input the target server
expect(getByTestId('loginSelectServiceButton')).toBeTruthy()
fireEvent.press(getByTestId('loginSelectServiceButton'))
expect(getByTestId('serverInputModal')).toBeTruthy()
fireEvent.changeText(
getByTestId('customServerTextInput'),
pds?.pdsUrl || '',
)
fireEvent.press(getByTestId('customServerSelectBtn'))
await waitFor(
() => expect(getByTestId('loginUsernameInput')).toBeTruthy(),
WAIT_OPTS,
)
// enter username & pass
fireEvent.changeText(getByTestId('loginUsernameInput'), 'bob')
fireEvent.changeText(getByTestId('loginPasswordInput'), 'hunter2')
await waitFor(
() => expect(getByTestId('loginNextButton')).toBeTruthy(),
WAIT_OPTS,
)
fireEvent.press(getByTestId('loginNextButton'))
// signed in
await waitFor(() => {
expect(getByTestId('settingsScreen')).toBeTruthy() // we go back to settings in this situation
expect(rootStore?.me?.displayName).toBe('Bob')
expect(rootStore?.me?.handle).toBe('bob.test')
expect(rootStore?.session.accounts.length).toBe(2)
}, WAIT_OPTS)
expect(rootStore?.me?.displayName).toBe('Bob')
expect(rootStore?.me?.handle).toBe('bob.test')
expect(rootStore?.session.accounts.length).toBe(2)
})
it('can instantly switch between accounts', async () => {
const {getByTestId} = render(<MobileShell />, rootStore)
await waitFor(
() => expect(getByTestId('settingsScreen')).toBeTruthy(),
WAIT_OPTS,
)
// select the alice account
fireEvent.press(getByTestId('switchToAccountBtn-alice.test'))
// swapped account
await waitFor(() => {
expect(rootStore?.me?.displayName).toBe('Alice')
expect(rootStore?.me?.handle).toBe('alice.test')
expect(rootStore?.session.accounts.length).toBe(2)
}, WAIT_OPTS)
expect(rootStore?.me?.displayName).toBe('Alice')
expect(rootStore?.me?.handle).toBe('alice.test')
expect(rootStore?.session.accounts.length).toBe(2)
})
it('will prompt for a password if you sign out', async () => {
const {getByTestId} = render(<MobileShell />, rootStore)
await waitFor(
() => expect(getByTestId('settingsScreen')).toBeTruthy(),
WAIT_OPTS,
)
// press the sign out button
fireEvent.press(getByTestId('signOutBtn'))
// in the logged out state
await waitFor(
() => expect(getByTestId('signinOrCreateAccount')).toBeTruthy(),
WAIT_OPTS,
)
// move to signin view
fireEvent.press(getByTestId('signInButton'))
expect(getByTestId('signIn')).toBeTruthy()
expect(getByTestId('chooseAccountForm')).toBeTruthy()
// select an existing account
fireEvent.press(getByTestId('chooseAccountBtn-alice.test'))
// goes to login screen instead of straight back to settings
expect(getByTestId('loginForm')).toBeTruthy()
})
})
-47
View File
@@ -1,47 +0,0 @@
export const exampleComHtml = `<!doctype html>
<html>
<head>
<title>Example Domain</title>
<meta name="description" content="An example website">
<meta charset="utf-8" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type="text/css">
body {
background-color: #f0f0f2;
margin: 0;
padding: 0;
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
div {
width: 600px;
margin: 5em auto;
padding: 2em;
background-color: #fdfdff;
border-radius: 0.5em;
box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);
}
a:link, a:visited {
color: #38488f;
text-decoration: none;
}
@media (max-width: 700px) {
div {
margin: 0 auto;
width: auto;
}
}
</style>
</head>
<body>
<div>
<h1>Example Domain</h1>
<p>This domain is for use in illustrative examples in documents. You may use this
domain in literature without prior coordination or asking for permission.</p>
<p><a href="https://www.iana.org/domains/example">More information...</a></p>
</div>
</body>
</html>`
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-19
View File
@@ -1,19 +0,0 @@
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])
}
})
})
-116
View File
@@ -1,116 +0,0 @@
import {extractHtmlMeta} from '../../src/lib/extractHtmlMeta'
import {exampleComHtml} from './__mocks__/exampleComHtml'
import {youtubeHTML} from './__mocks__/youtubeHtml'
import {tiktokHtml} from './__mocks__/tiktokHtml'
describe('extractHtmlMeta', () => {
const cases = [
['', {}],
['nothing', {}],
['<title>title</title>', {title: 'title'}],
['<title> aSd!@#AC </title>', {title: 'aSd!@#AC'}],
['<title>\n title\n </title>', {title: 'title'}],
['<meta name="title" content="meta title">', {title: 'meta title'}],
[
'<meta name="description" content="meta description">',
{description: 'meta description'},
],
['<meta property="og:title" content="og title">', {title: 'og title'}],
[
'<meta property="og:description" content="og description">',
{description: 'og description'},
],
[
'<meta property="og:image" content="https://ogimage.com/foo.png">',
{image: 'https://ogimage.com/foo.png'},
],
[
'<meta property="twitter:title" content="twitter title">',
{title: 'twitter title'},
],
[
'<meta property="twitter:description" content="twitter description">',
{description: 'twitter description'},
],
[
'<meta property="twitter:image" content="https://twitterimage.com/foo.png">',
{image: 'https://twitterimage.com/foo.png'},
],
['<meta\n name="title"\n content="meta title"\n>', {title: 'meta title'}],
]
it.each(cases)(
'given the html tag %p, returns %p',
(input, expectedResult) => {
const output = extractHtmlMeta({html: input as string, hostname: ''})
expect(output).toEqual(expectedResult)
},
)
it('extracts title and description from a generic HTML page', () => {
const input = exampleComHtml
const expectedOutput = {
title: 'Example Domain',
description: 'An example website',
}
const output = extractHtmlMeta({html: input, hostname: 'example.com'})
expect(output).toEqual(expectedOutput)
})
it('extracts title and description from a Tiktok HTML page', () => {
const input = tiktokHtml
const expectedOutput = {
title:
'Coca-Cola and Mentos! Super Reaction! #cocacola #mentos #reaction #bal... | TikTok',
description:
'5.5M Likes, 20.8K Comments. TikTok video from Power Vision Tests (@_powervision_): &quot;Coca-Cola and Mentos! Super Reaction! #cocacola #mentos #reaction #balloon #sciencemoment #scienceexperiment #experiment #test #amazing #pvexp&quot;. оригинальный звук - Power Vision Tests.',
}
const output = extractHtmlMeta({html: input, hostname: 'tiktok.com'})
expect(output).toEqual(expectedOutput)
})
it('extracts title and description from a generic youtube page', () => {
const input = youtubeHTML
const expectedOutput = {
title: 'HD Video (1080p) with Relaxing Music of Native American Shamans',
description:
'Stunning HD Video ( 1080p ) of Patagonian Nature with Relaxing Native American Shamanic Music. HD footage used from ',
image: 'https://i.ytimg.com/vi/x6UITRjhijI/sddefault.jpg',
}
const output = extractHtmlMeta({html: input, hostname: 'youtube.com'})
expect(output).toEqual(expectedOutput)
})
it('extracts username from the url a twitter profile page', () => {
const expectedOutput = {
title: '@bluesky on Twitter',
}
const output = extractHtmlMeta({
hostname: 'twitter.com',
pathname: '/bluesky',
})
expect(output).toEqual(expectedOutput)
})
it('extracts username from the url a tweet', () => {
const expectedOutput = {
title: 'Tweet by @bluesky',
}
const output = extractHtmlMeta({
hostname: 'twitter.com',
pathname: '/bluesky/status/1582437529969917953',
})
expect(output).toEqual(expectedOutput)
})
it("does not extract username from the url when it's not a tweet or profile page", () => {
const expectedOutput = {
title: 'Twitter',
}
const output = extractHtmlMeta({
hostname: 'twitter.com',
pathname: '/i/articles/follows/-1675653703?time_window=24',
})
expect(output).toEqual(expectedOutput)
})
})
-93
View File
@@ -1,93 +0,0 @@
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',
100,
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()
})
})
-24
View File
@@ -1,24 +0,0 @@
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])
}
})
})
@@ -1,6 +1,52 @@
import {LikelyType, getLinkMeta, getLikelyType} from '../../src/lib/link-meta'
import {exampleComHtml} from './__mocks__/exampleComHtml'
import {mockedRootStore} from '../../__mocks__/state-mock'
import {LikelyType, getLinkMeta} from '../src/lib/link-meta'
const exampleComHtml = `<!doctype html>
<html>
<head>
<title>Example Domain</title>
<meta name="description" content="An example website">
<meta charset="utf-8" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type="text/css">
body {
background-color: #f0f0f2;
margin: 0;
padding: 0;
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
div {
width: 600px;
margin: 5em auto;
padding: 2em;
background-color: #fdfdff;
border-radius: 0.5em;
box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);
}
a:link, a:visited {
color: #38488f;
text-decoration: none;
}
@media (max-width: 700px) {
div {
margin: 0 auto;
width: auto;
}
}
</style>
</head>
<body>
<div>
<h1>Example Domain</h1>
<p>This domain is for use in illustrative examples in documents. You may use this
domain in literature without prior coordination or asking for permission.</p>
<p><a href="https://www.iana.org/domains/example">More information...</a></p>
</div>
</body>
</html>`
describe('getLinkMeta', () => {
const inputs = [
@@ -11,10 +57,7 @@ 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/',
'https://bsky.app/index.html',
]
const outputs = [
{
@@ -51,35 +94,15 @@ 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: '/',
title: 'Bluesky',
description: 'A new kind of social network',
},
{
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,
@@ -88,20 +111,8 @@ describe('getLinkMeta', () => {
})
})
const input = inputs[i]
const output = await getLinkMeta(mockedRootStore, input)
const output = await getLinkMeta(input)
expect(output).toEqual(outputs[i])
}
})
})
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)
})
})
@@ -1,72 +0,0 @@
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(rootStore, 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()
}
})
})
})
-153
View File
@@ -1,153 +0,0 @@
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}',
)
})
})
-180
View File
@@ -1,180 +0,0 @@
import {RootStoreModel} from '../../../src/state/models/root-store'
import {MeModel} from '../../../src/state/models/me'
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)
})
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)
})
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().mockResolvedValue({}),
} as unknown as NotificationsViewModel
jest
.spyOn(rootStore.api.app.bsky.notification, 'getCount')
.mockImplementationOnce((): Promise<any> => {
return Promise.resolve({
data: {
count: 1,
},
})
})
await meModel.fetchNotifications()
expect(meModel.notificationCount).toBe(1)
expect(meModel.notifications.refresh).toHaveBeenCalled()
})
})
-151
View File
@@ -1,151 +0,0 @@
import {NavigationModel} 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)
})
})
})
-46
View File
@@ -1,46 +0,0 @@
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)
})
})
-59
View File
@@ -1,59 +0,0 @@
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('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)
})
})
-61
View File
@@ -1,61 +0,0 @@
import {
ConfirmModal,
ImagesLightbox,
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', () => {
const m = new ConfirmModal('Test Modal', 'Look good?', () => {})
model.openModal(m)
expect(model.isModalActive).toEqual(true)
expect(model.activeModal).toEqual(m)
model.closeModal()
expect(model.isModalActive).toEqual(false)
expect(model.activeModal).toBeUndefined()
})
it('should call the openLightbox & closeLightbox method', () => {
const lt = new ImagesLightbox(['uri'], 0)
model.openLightbox(lt)
expect(model.isLightboxActive).toEqual(true)
expect(model.activeLightbox).toEqual(lt)
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()
})
})
@@ -1,17 +1,8 @@
import {
extractEntities,
detectLinkables,
pluralize,
makeRecordUri,
ago,
makeValidHandle,
createFullHandle,
enforceLen,
cleanError,
toNiceDomain,
toShortUrl,
toShareUrl,
} from '../../src/lib/strings'
extractHtmlMeta,
} from '../src/lib/strings'
describe('extractEntities', () => {
const knownHandles = new Set(['handle.com', 'full123.test-of-chars'])
@@ -285,205 +276,44 @@ describe('detectLinkables', () => {
})
})
describe('pluralize', () => {
const inputs: [number, string, string?][] = [
[1, 'follower'],
[1, 'member'],
[100, 'post'],
[1000, 'repost'],
[10000, 'upvote'],
[100000, 'other'],
[2, 'man', 'men'],
describe('extractHtmlMeta', () => {
const inputs = [
'',
'nothing',
'<title>title</title>',
'<title> aSd!@#AC </title>',
'<title>\n title\n </title>',
'<meta name="title" content="meta title">',
'<meta name="description" content="meta description">',
'<meta property="og:title" content="og title">',
'<meta property="og:description" content="og description">',
'<meta property="og:image" content="https://ogimage.com/foo.png">',
'<meta property="twitter:title" content="twitter title">',
'<meta property="twitter:description" content="twitter description">',
'<meta property="twitter:image" content="https://twitterimage.com/foo.png">',
'<meta\n name="title"\n content="meta title"\n>',
]
const outputs = [
'follower',
'member',
'posts',
'reposts',
'upvotes',
'others',
'men',
{},
{},
{title: 'title'},
{title: 'aSd!@#AC'},
{title: 'title'},
{title: 'meta title'},
{description: 'meta description'},
{title: 'og title'},
{description: 'og description'},
{image: 'https://ogimage.com/foo.png'},
{title: 'twitter title'},
{description: 'twitter description'},
{image: 'https://twitterimage.com/foo.png'},
{title: 'meta title'},
]
it('correctly pluralizes a set of words', () => {
it('correctly handles a set of text inputs', () => {
for (let i = 0; i < inputs.length; i++) {
const input = inputs[i]
const output = pluralize(...input)
const output = extractHtmlMeta(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])
}
})
})
@@ -1,43 +0,0 @@
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')
})
})
@@ -1,118 +0,0 @@
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',
undefined,
[],
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()
})
})
@@ -1,70 +0,0 @@
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'])
})
})
@@ -1,58 +0,0 @@
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 {
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()
})
})
@@ -1,110 +0,0 @@
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',
)
})
// TODO - this will only pass if the profile has an avatar image set
// 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()
})
})
@@ -1,17 +0,0 @@
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)
})
})
@@ -1,49 +0,0 @@
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,
)
})
})
-37
View File
@@ -1,37 +0,0 @@
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()
})
})
-21
View File
@@ -1,21 +0,0 @@
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('/')
})
})
-30
View File
@@ -1,30 +0,0 @@
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()
})
})
-57
View File
@@ -1,57 +0,0 @@
import React from 'react'
import {Menu} from '../../../../src/view/shell/mobile/Menu'
import {cleanup, fireEvent, render} from '../../../../jest/test-utils'
import {mockedNavigationStore} 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 {getByTestId} = render(<Menu {...mockedProps} />)
const menuItemButton = getByTestId('menuItemButton-Notifications')
fireEvent.press(menuItemButton)
expect(onCloseMock).toHaveBeenCalled()
expect(mockedNavigationStore.switchTo).toHaveBeenCalledWith(1, true)
})
})
@@ -1,100 +0,0 @@
import React from 'react'
import {Animated} 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()
})
// TODO - this throws currently, but the tabs selector isnt being used atm so I just disabled -prf
// 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)
})
})
+55
View File
@@ -0,0 +1,55 @@
# To learn about Buck see [Docs](https://buckbuild.com/).
# To run your application with Buck:
# - install Buck
# - `npm start` - to start the packager
# - `cd android`
# - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"`
# - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck
# - `buck install -r android/app` - compile, install and run application
#
load(":build_defs.bzl", "create_aar_targets", "create_jar_targets")
lib_deps = []
create_aar_targets(glob(["libs/*.aar"]))
create_jar_targets(glob(["libs/*.jar"]))
android_library(
name = "all-libs",
exported_deps = lib_deps,
)
android_library(
name = "app-code",
srcs = glob([
"src/main/java/**/*.java",
]),
deps = [
":all-libs",
":build_config",
":res",
],
)
android_build_config(
name = "build_config",
package = "xyz.blueskyweb.app",
)
android_resource(
name = "res",
package = "xyz.blueskyweb.app",
res = "src/main/res",
)
android_binary(
name = "app",
keystore = "//android/keystores:debug",
manifest = "src/main/AndroidManifest.xml",
package_type = "debug",
deps = [
":app-code",
],
)
+213 -64
View File
@@ -1,86 +1,128 @@
apply plugin: "com.android.application"
apply plugin: "com.facebook.react"
import com.android.build.OutputFile
import org.apache.tools.ant.taskdefs.condition.Os
/**
* This is the configuration block to customize your React Native Android app.
* By default you don't need to apply any configuration, just uncomment the lines you need.
* The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets
* and bundleReleaseJsAndAssets).
* These basically call `react-native bundle` with the correct arguments during the Android build
* cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the
* bundle directly from the development server. Below you can see all the possible configurations
* and their defaults. If you decide to add a configuration block, make sure to add it before the
* `apply from: "../../node_modules/react-native/react.gradle"` line.
*
* project.ext.react = [
* // the name of the generated asset file containing your JS bundle
* bundleAssetName: "index.android.bundle",
*
* // the entry file for bundle generation. If none specified and
* // "index.android.js" exists, it will be used. Otherwise "index.js" is
* // default. Can be overridden with ENTRY_FILE environment variable.
* entryFile: "index.android.js",
*
* // https://reactnative.dev/docs/performance#enable-the-ram-format
* bundleCommand: "ram-bundle",
*
* // whether to bundle JS and assets in debug mode
* bundleInDebug: false,
*
* // whether to bundle JS and assets in release mode
* bundleInRelease: true,
*
* // whether to bundle JS and assets in another build variant (if configured).
* // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants
* // The configuration property can be in the following formats
* // 'bundleIn${productFlavor}${buildType}'
* // 'bundleIn${buildType}'
* // bundleInFreeDebug: true,
* // bundleInPaidRelease: true,
* // bundleInBeta: true,
*
* // whether to disable dev mode in custom build variants (by default only disabled in release)
* // for example: to disable dev mode in the staging build type (if configured)
* devDisabledInStaging: true,
* // The configuration property can be in the following formats
* // 'devDisabledIn${productFlavor}${buildType}'
* // 'devDisabledIn${buildType}'
*
* // the root of your project, i.e. where "package.json" lives
* root: "../../",
*
* // where to put the JS bundle asset in debug mode
* jsBundleDirDebug: "$buildDir/intermediates/assets/debug",
*
* // where to put the JS bundle asset in release mode
* jsBundleDirRelease: "$buildDir/intermediates/assets/release",
*
* // where to put drawable resources / React Native assets, e.g. the ones you use via
* // require('./image.png')), in debug mode
* resourcesDirDebug: "$buildDir/intermediates/res/merged/debug",
*
* // where to put drawable resources / React Native assets, e.g. the ones you use via
* // require('./image.png')), in release mode
* resourcesDirRelease: "$buildDir/intermediates/res/merged/release",
*
* // by default the gradle tasks are skipped if none of the JS files or assets change; this means
* // that we don't look at files in android/ or ios/ to determine whether the tasks are up to
* // date; if you have any other folders that you want to ignore for performance reasons (gradle
* // indexes the entire tree), add them here. Alternatively, if you have JS files in android/
* // for example, you might want to remove it from here.
* inputExcludes: ["android/**", "ios/**"],
*
* // override which node gets called and with what additional arguments
* nodeExecutableAndArgs: ["node"],
*
* // supply additional arguments to the packager
* extraPackagerArgs: []
* ]
*/
react {
/* Folders */
// The root of your project, i.e. where "package.json" lives. Default is '..'
// root = file("../")
// The folder where the react-native NPM package is. Default is ../node_modules/react-native
// reactNativeDir = file("../node-modules/react-native")
// The folder where the react-native Codegen package is. Default is ../node_modules/react-native-codegen
// codegenDir = file("../node-modules/react-native-codegen")
// The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js
// cliFile = file("../node_modules/react-native/cli.js")
/* Variants */
// The list of variants to that are debuggable. For those we're going to
// skip the bundling of the JS bundle and the assets. By default is just 'debug'.
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
// debuggableVariants = ["liteDebug", "prodDebug"]
/* Bundling */
// A list containing the node command and its flags. Default is just 'node'.
// nodeExecutableAndArgs = ["node"]
//
// The command to run when bundling. By default is 'bundle'
// bundleCommand = "ram-bundle"
//
// The path to the CLI configuration file. Default is empty.
// bundleConfig = file(../rn-cli.config.js)
//
// The name of the generated asset file containing your JS bundle
// bundleAssetName = "MyApplication.android.bundle"
//
// The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
// entryFile = file("../js/MyApplication.android.js")
//
// A list of extra flags to pass to the 'bundle' commands.
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
// extraPackagerArgs = []
/* Hermes Commands */
// The hermes compiler command to run. By default it is 'hermesc'
// hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
//
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
// hermesFlags = ["-O", "-output-source-map"]
}
project.ext.react = [
enableHermes: false, // clean and rebuild if changing
]
apply from: "../../node_modules/react-native/react.gradle"
/**
* Set this to true to create four separate APKs instead of one,
* one for each native architecture. This is useful if you don't
* use App Bundles (https://developer.android.com/guide/app-bundle/)
* and want to have separate APKs to upload to the Play Store.
* Set this to true to create two separate APKs instead of one:
* - An APK that only works on ARM devices
* - An APK that only works on x86 devices
* The advantage is the size of the APK is reduced by about 4MB.
* Upload all the APKs to the Play Store and people will download
* the correct one based on the CPU architecture of their device.
*/
def enableSeparateBuildPerCPUArchitecture = false
/**
* Set this to true to Run Proguard on Release builds to minify the Java bytecode.
* Run Proguard to shrink the Java bytecode in release builds.
*/
def enableProguardInReleaseBuilds = false
/**
* The preferred build flavor of JavaScriptCore (JSC)
* The preferred build flavor of JavaScriptCore.
*
* For example, to use the international variant, you can use:
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
*
* The international variant includes ICU i18n library and necessary data
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
* give correct results when using with locales other than en-US. Note that
* give correct results when using with locales other than en-US. Note that
* this variant is about 6MiB larger per architecture than default.
*/
def jscFlavor = 'org.webkit:android-jsc:+'
/**
* Private function to get the list of Native Architectures you want to build.
* This reads the value from reactNativeArchitectures in your gradle.properties
* file and works together with the --active-arch-only flag of react-native run-android.
* Whether to enable the Hermes VM.
*
* This should be set on project.ext.react and that value will be read here. If it is not set
* on project.ext.react, JavaScript will not be compiled to Hermes Bytecode
* and the benefits of using Hermes will therefore be sharply reduced.
*/
def enableHermes = project.ext.react.get("enableHermes", false);
/**
* Architectures to build native code for.
*/
def reactNativeArchitectures() {
def value = project.getProperties().get("reactNativeArchitectures")
@@ -92,13 +134,84 @@ android {
compileSdkVersion rootProject.ext.compileSdkVersion
namespace "xyz.blueskyweb.app"
defaultConfig {
applicationId "xyz.blueskyweb.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
if (isNewArchitectureEnabled()) {
// We configure the NDK build only if you decide to opt-in for the New Architecture.
externalNativeBuild {
ndkBuild {
arguments "APP_PLATFORM=android-21",
"APP_STL=c++_shared",
"NDK_TOOLCHAIN_VERSION=clang",
"GENERATED_SRC_DIR=$buildDir/generated/source",
"PROJECT_BUILD_DIR=$buildDir",
"REACT_ANDROID_DIR=$rootDir/../node_modules/react-native/ReactAndroid",
"REACT_ANDROID_BUILD_DIR=$rootDir/../node_modules/react-native/ReactAndroid/build"
cFlags "-Wall", "-Werror", "-fexceptions", "-frtti", "-DWITH_INSPECTOR=1"
cppFlags "-std=c++17"
// Make sure this target name is the same you specify inside the
// src/main/jni/Android.mk file for the `LOCAL_MODULE` variable.
targets "app_appmodules"
// Fix for windows limit on number of character in file paths and in command lines
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
arguments "NDK_APP_SHORT_COMMANDS=true"
}
}
}
if (!enableSeparateBuildPerCPUArchitecture) {
ndk {
abiFilters (*reactNativeArchitectures())
}
}
}
}
if (isNewArchitectureEnabled()) {
// We configure the NDK build only if you decide to opt-in for the New Architecture.
externalNativeBuild {
ndkBuild {
path "$projectDir/src/main/jni/Android.mk"
}
}
def reactAndroidProjectDir = project(':ReactAndroid').projectDir
def packageReactNdkDebugLibs = tasks.register("packageReactNdkDebugLibs", Copy) {
dependsOn(":ReactAndroid:packageReactNdkDebugLibsForBuck")
from("$reactAndroidProjectDir/src/main/jni/prebuilt/lib")
into("$buildDir/react-ndk/exported")
}
def packageReactNdkReleaseLibs = tasks.register("packageReactNdkReleaseLibs", Copy) {
dependsOn(":ReactAndroid:packageReactNdkReleaseLibsForBuck")
from("$reactAndroidProjectDir/src/main/jni/prebuilt/lib")
into("$buildDir/react-ndk/exported")
}
afterEvaluate {
// If you wish to add a custom TurboModule or component locally,
// you should uncomment this line.
// preBuild.dependsOn("generateCodegenArtifactsFromSchema")
preDebugBuild.dependsOn(packageReactNdkDebugLibs)
preReleaseBuild.dependsOn(packageReactNdkReleaseLibs)
// Due to a bug inside AGP, we have to explicitly set a dependency
// between configureNdkBuild* tasks and the preBuild tasks.
// This can be removed once this is solved: https://issuetracker.google.com/issues/207403732
configureNdkBuildRelease.dependsOn(preReleaseBuild)
configureNdkBuildDebug.dependsOn(preDebugBuild)
reactNativeArchitectures().each { architecture ->
tasks.findByName("configureNdkBuildDebug[${architecture}]")?.configure {
dependsOn("preDebugBuild")
}
tasks.findByName("configureNdkBuildRelease[${architecture}]")?.configure {
dependsOn("preReleaseBuild")
}
}
}
}
splits {
@@ -148,24 +261,60 @@ android {
}
dependencies {
// The version of react-native is set by the React Native Gradle Plugin
implementation("com.facebook.react:react-android")
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.0.0")
implementation project(':rn-fetch-blob')
//noinspection GradleDynamicVersion
implementation "com.facebook.react:react-native:+" // From node_modules
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
exclude group:'com.facebook.fbjni'
}
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}")
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
exclude group:'com.facebook.flipper'
exclude group:'com.squareup.okhttp3', module:'okhttp'
}
debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}")
if (hermesEnabled.toBoolean()) {
implementation("com.facebook.react:hermes-android")
debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") {
exclude group:'com.facebook.flipper'
}
if (enableHermes) {
def hermesPath = "../../node_modules/hermes-engine/android/";
debugImplementation files(hermesPath + "hermes-debug.aar")
releaseImplementation files(hermesPath + "hermes-release.aar")
} else {
implementation jscFlavor
}
}
if (isNewArchitectureEnabled()) {
// If new architecture is enabled, we let you build RN from source
// Otherwise we fallback to a prebuilt .aar bundled in the NPM package.
// This will be applied to all the imported transtitive dependency.
configurations.all {
resolutionStrategy.dependencySubstitution {
substitute(module("com.facebook.react:react-native"))
.using(project(":ReactAndroid")).because("On New Architecture we're building React Native from source")
}
}
}
// Run this once to be able to run the application with BUCK
// puts all compile dependencies into folder libs for BUCK to use
task copyDownloadableDepsToLibs(type: Copy) {
from configurations.implementation
into 'libs'
}
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
def isNewArchitectureEnabled() {
// To opt-in for the New Architecture, you can either:
// - Set `newArchEnabled` to true inside the `gradle.properties` file
// - Invoke gradle with `-newArchEnabled=true`
// - Set an environment variable `ORG_GRADLE_PROJECT_newArchEnabled=true`
return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
}
+19
View File
@@ -0,0 +1,19 @@
"""Helper definitions to glob .aar and .jar targets"""
def create_aar_targets(aarfiles):
for aarfile in aarfiles:
name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")]
lib_deps.append(":" + name)
android_prebuilt_aar(
name = name,
aar = aarfile,
)
def create_jar_targets(jarfiles):
for jarfile in jarfiles:
name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")]
lib_deps.append(":" + name)
prebuilt_jar(
name = name,
binary_jar = jarfile,
)
@@ -25,16 +25,13 @@ import com.facebook.react.bridge.ReactContext;
import com.facebook.react.modules.network.NetworkingModule;
import okhttp3.OkHttpClient;
/**
* Class responsible of loading Flipper inside your React Native application. This is the debug
* flavor of it. Here you can add your own plugins and customize the Flipper setup.
*/
public class ReactNativeFlipper {
public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
if (FlipperUtils.shouldEnableFlipper(context)) {
final FlipperClient client = AndroidFlipperClient.getInstance(context);
client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()));
client.addPlugin(new ReactFlipperPlugin());
client.addPlugin(new DatabasesFlipperPlugin(context));
client.addPlugin(new SharedPreferencesFlipperPlugin(context));
client.addPlugin(CrashReporterPlugin.getInstance());
+1 -4
View File
@@ -2,9 +2,6 @@
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" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:name=".MainApplication"
@@ -18,7 +15,7 @@
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustPan"
android:windowSoftInputMode="adjustResize"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -2,8 +2,7 @@ package xyz.blueskyweb.app;
import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
import com.facebook.react.defaults.DefaultReactActivityDelegate;
import com.facebook.react.ReactRootView;
import android.os.Bundle;
public class MainActivity extends ReactActivity {
@@ -18,19 +17,30 @@ public class MainActivity extends ReactActivity {
}
/**
* Returns the instance of the {@link ReactActivityDelegate}. Here we use a util class {@link
* DefaultReactActivityDelegate} which allows you to easily enable Fabric and Concurrent React
* (aka React 18) with two boolean flags.
* Returns the instance of the {@link ReactActivityDelegate}. There the RootView is created and
* you can specify the rendered you wish to use (Fabric or the older renderer).
*/
@Override
protected ReactActivityDelegate createReactActivityDelegate() {
return new DefaultReactActivityDelegate(
this,
getMainComponentName(),
return new MainActivityDelegate(this, getMainComponentName());
}
public static class MainActivityDelegate extends ReactActivityDelegate {
public MainActivityDelegate(ReactActivity activity, String mainComponentName) {
super(activity, mainComponentName);
}
@Override
protected ReactRootView createRootView() {
ReactRootView reactRootView = new ReactRootView(getContext());
// If you opted-in for the New Architecture, we enable the Fabric Renderer.
DefaultNewArchitectureEntryPoint.getFabricEnabled(), // fabricEnabled
// If you opted-in for the New Architecture, we enable Concurrent React (i.e. React 18).
DefaultNewArchitectureEntryPoint.getConcurrentReactEnabled() // concurrentRootEnabled
);
reactRootView.setIsFabric(BuildConfig.IS_NEW_ARCHITECTURE_ENABLED);
return reactRootView;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(null);
}
}
}
@@ -1,12 +1,13 @@
package xyz.blueskyweb.app;
import android.app.Application;
import android.content.Context;
import com.facebook.react.PackageList;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
import com.facebook.react.defaults.DefaultReactNativeHost;
import com.facebook.react.config.ReactFeatureFlags;
import com.facebook.soloader.SoLoader;
import xyz.blueskyweb.app.newarchitecture.MainApplicationReactNativeHost;
import java.lang.reflect.InvocationTargetException;
@@ -15,7 +16,7 @@ import java.util.List;
public class MainApplication extends Application implements ReactApplication {
private final ReactNativeHost mReactNativeHost =
new DefaultReactNativeHost(this) {
new ReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
@@ -34,20 +35,18 @@ public class MainApplication extends Application implements ReactApplication {
protected String getJSMainModuleName() {
return "index";
}
@Override
protected boolean isNewArchEnabled() {
return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
}
@Override
protected Boolean isHermesEnabled() {
return BuildConfig.IS_HERMES_ENABLED;
}
};
private final ReactNativeHost mNewArchitectureNativeHost =
new MainApplicationReactNativeHost(this);
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
return mNewArchitectureNativeHost;
} else {
return mReactNativeHost;
}
}
@Override
@@ -56,10 +55,37 @@ public class MainApplication extends Application implements ReactApplication {
// If you opted-in for the New Architecture, we enable the TurboModule system
ReactFeatureFlags.useTurboModules = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
SoLoader.init(this, /* native exopackage */ false);
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
DefaultNewArchitectureEntryPoint.load();
initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
}
/**
* Loads Flipper in React Native templates. Call this in the onCreate method with something like
* initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
*
* @param context
* @param reactInstanceManager
*/
private static void initializeFlipper(
Context context, ReactInstanceManager reactInstanceManager) {
if (BuildConfig.DEBUG) {
try {
/*
We use reflection here to pick up the class that initializes Flipper,
since Flipper library is not available in release mode
*/
Class<?> aClass = Class.forName("com.app.ReactNativeFlipper");
aClass
.getMethod("initializeFlipper", Context.class, ReactInstanceManager.class)
.invoke(null, context, reactInstanceManager);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
ReactNativeFlipper.initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
}
}
@@ -0,0 +1,116 @@
package xyz.blueskyweb.app.newarchitecture;
import android.app.Application;
import androidx.annotation.NonNull;
import com.facebook.react.PackageList;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.ReactPackageTurboModuleManagerDelegate;
import com.facebook.react.bridge.JSIModulePackage;
import com.facebook.react.bridge.JSIModuleProvider;
import com.facebook.react.bridge.JSIModuleSpec;
import com.facebook.react.bridge.JSIModuleType;
import com.facebook.react.bridge.JavaScriptContextHolder;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.UIManager;
import com.facebook.react.fabric.ComponentFactory;
import com.facebook.react.fabric.CoreComponentsRegistry;
import com.facebook.react.fabric.EmptyReactNativeConfig;
import com.facebook.react.fabric.FabricJSIModuleProvider;
import com.facebook.react.uimanager.ViewManagerRegistry;
import xyz.blueskyweb.app.BuildConfig;
import xyz.blueskyweb.app.newarchitecture.components.MainComponentsRegistry;
import xyz.blueskyweb.app.newarchitecture.modules.MainApplicationTurboModuleManagerDelegate;
import java.util.ArrayList;
import java.util.List;
/**
* A {@link ReactNativeHost} that helps you load everything needed for the New Architecture, both
* TurboModule delegates and the Fabric Renderer.
*
* <p>Please note that this class is used ONLY if you opt-in for the New Architecture (see the
* `newArchEnabled` property). Is ignored otherwise.
*/
public class MainApplicationReactNativeHost extends ReactNativeHost {
public MainApplicationReactNativeHost(Application application) {
super(application);
}
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
List<ReactPackage> packages = new PackageList(this).getPackages();
// Packages that cannot be autolinked yet can be added manually here, for example:
// packages.add(new MyReactNativePackage());
// TurboModules must also be loaded here providing a valid TurboReactPackage implementation:
// packages.add(new TurboReactPackage() { ... });
// If you have custom Fabric Components, their ViewManagers should also be loaded here
// inside a ReactPackage.
return packages;
}
@Override
protected String getJSMainModuleName() {
return "index";
}
@NonNull
@Override
protected ReactPackageTurboModuleManagerDelegate.Builder
getReactPackageTurboModuleManagerDelegateBuilder() {
// Here we provide the ReactPackageTurboModuleManagerDelegate Builder. This is necessary
// for the new architecture and to use TurboModules correctly.
return new MainApplicationTurboModuleManagerDelegate.Builder();
}
@Override
protected JSIModulePackage getJSIModulePackage() {
return new JSIModulePackage() {
@Override
public List<JSIModuleSpec> getJSIModules(
final ReactApplicationContext reactApplicationContext,
final JavaScriptContextHolder jsContext) {
final List<JSIModuleSpec> specs = new ArrayList<>();
// Here we provide a new JSIModuleSpec that will be responsible of providing the
// custom Fabric Components.
specs.add(
new JSIModuleSpec() {
@Override
public JSIModuleType getJSIModuleType() {
return JSIModuleType.UIManager;
}
@Override
public JSIModuleProvider<UIManager> getJSIModuleProvider() {
final ComponentFactory componentFactory = new ComponentFactory();
CoreComponentsRegistry.register(componentFactory);
// Here we register a Components Registry.
// The one that is generated with the template contains no components
// and just provides you the one from React Native core.
MainComponentsRegistry.register(componentFactory);
final ReactInstanceManager reactInstanceManager = getReactInstanceManager();
ViewManagerRegistry viewManagerRegistry =
new ViewManagerRegistry(
reactInstanceManager.getOrCreateViewManagers(reactApplicationContext));
return new FabricJSIModuleProvider(
reactApplicationContext,
componentFactory,
new EmptyReactNativeConfig(),
viewManagerRegistry);
}
});
return specs;
}
};
}
}
@@ -0,0 +1,36 @@
package xyz.blueskyweb.app.newarchitecture.components;
import com.facebook.jni.HybridData;
import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.react.fabric.ComponentFactory;
import com.facebook.soloader.SoLoader;
/**
* Class responsible to load the custom Fabric Components. This class has native methods and needs a
* corresponding C++ implementation/header file to work correctly (already placed inside the jni/
* folder for you).
*
* <p>Please note that this class is used ONLY if you opt-in for the New Architecture (see the
* `newArchEnabled` property). Is ignored otherwise.
*/
@DoNotStrip
public class MainComponentsRegistry {
static {
SoLoader.loadLibrary("fabricjni");
}
@DoNotStrip private final HybridData mHybridData;
@DoNotStrip
private native HybridData initHybrid(ComponentFactory componentFactory);
@DoNotStrip
private MainComponentsRegistry(ComponentFactory componentFactory) {
mHybridData = initHybrid(componentFactory);
}
@DoNotStrip
public static MainComponentsRegistry register(ComponentFactory componentFactory) {
return new MainComponentsRegistry(componentFactory);
}
}
@@ -0,0 +1,48 @@
package xyz.blueskyweb.app.newarchitecture.modules;
import com.facebook.jni.HybridData;
import com.facebook.react.ReactPackage;
import com.facebook.react.ReactPackageTurboModuleManagerDelegate;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.soloader.SoLoader;
import java.util.List;
/**
* Class responsible to load the TurboModules. This class has native methods and needs a
* corresponding C++ implementation/header file to work correctly (already placed inside the jni/
* folder for you).
*
* <p>Please note that this class is used ONLY if you opt-in for the New Architecture (see the
* `newArchEnabled` property). Is ignored otherwise.
*/
public class MainApplicationTurboModuleManagerDelegate
extends ReactPackageTurboModuleManagerDelegate {
private static volatile boolean sIsSoLibraryLoaded;
protected MainApplicationTurboModuleManagerDelegate(
ReactApplicationContext reactApplicationContext, List<ReactPackage> packages) {
super(reactApplicationContext, packages);
}
protected native HybridData initHybrid();
native boolean canCreateTurboModule(String moduleName);
public static class Builder extends ReactPackageTurboModuleManagerDelegate.Builder {
protected MainApplicationTurboModuleManagerDelegate build(
ReactApplicationContext context, List<ReactPackage> packages) {
return new MainApplicationTurboModuleManagerDelegate(context, packages);
}
}
@Override
protected synchronized void maybeLoadOtherSoLibraries() {
if (!sIsSoLibraryLoaded) {
// If you change the name of your application .so file in the Android.mk file,
// make sure you update the name here as well.
SoLoader.loadLibrary("app_appmodules");
sIsSoLibraryLoaded = true;
}
}
}
@@ -0,0 +1,24 @@
#include "MainApplicationModuleProvider.h"
#include <rncore.h>
namespace facebook {
namespace react {
std::shared_ptr<TurboModule> MainApplicationModuleProvider(
const std::string moduleName,
const JavaTurboModule::InitParams &params) {
// Here you can provide your own module provider for TurboModules coming from
// either your application or from external libraries. The approach to follow
// is similar to the following (for a library called `samplelibrary`:
//
// auto module = samplelibrary_ModuleProvider(moduleName, params);
// if (module != nullptr) {
// return module;
// }
// return rncore_ModuleProvider(moduleName, params);
return rncore_ModuleProvider(moduleName, params);
}
} // namespace react
} // namespace facebook
@@ -0,0 +1,16 @@
#pragma once
#include <memory>
#include <string>
#include <ReactCommon/JavaTurboModule.h>
namespace facebook {
namespace react {
std::shared_ptr<TurboModule> MainApplicationModuleProvider(
const std::string moduleName,
const JavaTurboModule::InitParams &params);
} // namespace react
} // namespace facebook
@@ -0,0 +1,45 @@
#include "MainApplicationTurboModuleManagerDelegate.h"
#include "MainApplicationModuleProvider.h"
namespace facebook {
namespace react {
jni::local_ref<MainApplicationTurboModuleManagerDelegate::jhybriddata>
MainApplicationTurboModuleManagerDelegate::initHybrid(
jni::alias_ref<jhybridobject>) {
return makeCxxInstance();
}
void MainApplicationTurboModuleManagerDelegate::registerNatives() {
registerHybrid({
makeNativeMethod(
"initHybrid", MainApplicationTurboModuleManagerDelegate::initHybrid),
makeNativeMethod(
"canCreateTurboModule",
MainApplicationTurboModuleManagerDelegate::canCreateTurboModule),
});
}
std::shared_ptr<TurboModule>
MainApplicationTurboModuleManagerDelegate::getTurboModule(
const std::string name,
const std::shared_ptr<CallInvoker> jsInvoker) {
// Not implemented yet: provide pure-C++ NativeModules here.
return nullptr;
}
std::shared_ptr<TurboModule>
MainApplicationTurboModuleManagerDelegate::getTurboModule(
const std::string name,
const JavaTurboModule::InitParams &params) {
return MainApplicationModuleProvider(name, params);
}
bool MainApplicationTurboModuleManagerDelegate::canCreateTurboModule(
std::string name) {
return getTurboModule(name, nullptr) != nullptr ||
getTurboModule(name, {.moduleName = name}) != nullptr;
}
} // namespace react
} // namespace facebook
@@ -0,0 +1,38 @@
#include <memory>
#include <string>
#include <ReactCommon/TurboModuleManagerDelegate.h>
#include <fbjni/fbjni.h>
namespace facebook {
namespace react {
class MainApplicationTurboModuleManagerDelegate
: public jni::HybridClass<
MainApplicationTurboModuleManagerDelegate,
TurboModuleManagerDelegate> {
public:
// Adapt it to the package you used for your Java class.
static constexpr auto kJavaDescriptor =
"Lcom/app/newarchitecture/modules/MainApplicationTurboModuleManagerDelegate;";
static jni::local_ref<jhybriddata> initHybrid(jni::alias_ref<jhybridobject>);
static void registerNatives();
std::shared_ptr<TurboModule> getTurboModule(
const std::string name,
const std::shared_ptr<CallInvoker> jsInvoker) override;
std::shared_ptr<TurboModule> getTurboModule(
const std::string name,
const JavaTurboModule::InitParams &params) override;
/**
* Test-only method. Allows user to verify whether a TurboModule can be
* created by instances of this class.
*/
bool canCreateTurboModule(std::string name);
};
} // namespace react
} // namespace facebook
@@ -0,0 +1,61 @@
#include "MainComponentsRegistry.h"
#include <CoreComponentsRegistry.h>
#include <fbjni/fbjni.h>
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
#include <react/renderer/components/rncore/ComponentDescriptors.h>
namespace facebook {
namespace react {
MainComponentsRegistry::MainComponentsRegistry(ComponentFactory *delegate) {}
std::shared_ptr<ComponentDescriptorProviderRegistry const>
MainComponentsRegistry::sharedProviderRegistry() {
auto providerRegistry = CoreComponentsRegistry::sharedProviderRegistry();
// Custom Fabric Components go here. You can register custom
// components coming from your App or from 3rd party libraries here.
//
// providerRegistry->add(concreteComponentDescriptorProvider<
// AocViewerComponentDescriptor>());
return providerRegistry;
}
jni::local_ref<MainComponentsRegistry::jhybriddata>
MainComponentsRegistry::initHybrid(
jni::alias_ref<jclass>,
ComponentFactory *delegate) {
auto instance = makeCxxInstance(delegate);
auto buildRegistryFunction =
[](EventDispatcher::Weak const &eventDispatcher,
ContextContainer::Shared const &contextContainer)
-> ComponentDescriptorRegistry::Shared {
auto registry = MainComponentsRegistry::sharedProviderRegistry()
->createComponentDescriptorRegistry(
{eventDispatcher, contextContainer});
auto mutableRegistry =
std::const_pointer_cast<ComponentDescriptorRegistry>(registry);
mutableRegistry->setFallbackComponentDescriptor(
std::make_shared<UnimplementedNativeViewComponentDescriptor>(
ComponentDescriptorParameters{
eventDispatcher, contextContainer, nullptr}));
return registry;
};
delegate->buildRegistryFunction = buildRegistryFunction;
return instance;
}
void MainComponentsRegistry::registerNatives() {
registerHybrid({
makeNativeMethod("initHybrid", MainComponentsRegistry::initHybrid),
});
}
} // namespace react
} // namespace facebook
@@ -0,0 +1,32 @@
#pragma once
#include <ComponentFactory.h>
#include <fbjni/fbjni.h>
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
#include <react/renderer/componentregistry/ComponentDescriptorRegistry.h>
namespace facebook {
namespace react {
class MainComponentsRegistry
: public facebook::jni::HybridClass<MainComponentsRegistry> {
public:
// Adapt it to the package you used for your Java class.
constexpr static auto kJavaDescriptor =
"Lcom/app/newarchitecture/components/MainComponentsRegistry;";
static void registerNatives();
MainComponentsRegistry(ComponentFactory *delegate);
private:
static std::shared_ptr<ComponentDescriptorProviderRegistry const>
sharedProviderRegistry();
static jni::local_ref<jhybriddata> initHybrid(
jni::alias_ref<jclass>,
ComponentFactory *delegate);
};
} // namespace react
} // namespace facebook
+11
View File
@@ -0,0 +1,11 @@
#include <fbjni/fbjni.h>
#include "MainApplicationTurboModuleManagerDelegate.h"
#include "MainComponentsRegistry.h"
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
return facebook::jni::initialize(vm, [] {
facebook::react::MainApplicationTurboModuleManagerDelegate::
registerNatives();
facebook::react::MainComponentsRegistry::registerNatives();
});
}
@@ -1,18 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
* directory of this source tree.
*/
package xyz.blueskyweb.app;
import android.content.Context;
import com.facebook.react.ReactInstanceManager;
/**
* Class responsible of loading Flipper inside your React Native application. This is the release
* flavor of it so it's empty as we don't want to load Flipper.
*/
public class ReactNativeFlipper {
public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
// Do nothing as we don't want to initialize Flipper on Release.
}
}
+36 -6
View File
@@ -4,20 +4,50 @@ import org.apache.tools.ant.taskdefs.condition.Os
buildscript {
ext {
buildToolsVersion = "33.0.0"
buildToolsVersion = "31.0.0"
minSdkVersion = 21
compileSdkVersion = 33
targetSdkVersion = 33
compileSdkVersion = 31
targetSdkVersion = 31
// We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP.
ndkVersion = "23.1.7779620"
if (System.properties['os.arch'] == "aarch64") {
// For M1 Users we need to use the NDK 24 which added support for aarch64
ndkVersion = "24.0.8215888"
} else {
// Otherwise we default to the side-by-side NDK version from AGP.
ndkVersion = "21.4.7075529"
}
}
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:7.3.1")
classpath('com.android.tools.build:gradle:7.2.1')
classpath("com.facebook.react:react-native-gradle-plugin")
classpath("de.undercouch:gradle-download-task:4.1.2")
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
url("$rootDir/../node_modules/react-native/android")
}
maven {
// Android JSC is installed from npm
url("$rootDir/../node_modules/jsc-android/dist")
}
mavenCentral {
// We don't want to fetch react-native from Maven Central as there are
// older versions over there.
content {
excludeGroup "com.facebook.react"
}
}
google()
maven { url 'https://www.jitpack.io' }
}
}
-4
View File
@@ -38,7 +38,3 @@ reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
# to write custom TurboModules/Fabric components OR use libraries that
# are providing them.
newArchEnabled=false
# Use this property to enable or disable the Hermes JS engine.
# If set to false, you will be using JSC instead.
hermesEnabled=true
+1 -1
View File
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+5 -2
View File
@@ -2,5 +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')
}
-10
View File
@@ -1,10 +0,0 @@
# This `.xcode.env` file is versioned and is used to source the environment
# used when running script phases inside Xcode.
# To customize your local environment, you can create an `.xcode.env.local`
# file that is not versioned.
# NODE_BINARY variable contains the PATH to the node executable.
#
# Customize the NODE_BINARY variable here.
# For example, to use nvm with brew, add the following line
# . "$(brew --prefix nvm)/nvm.sh" --no-use
export NODE_BINARY=$(command -v node)
+10 -24
View File
@@ -1,16 +1,8 @@
require_relative '../node_modules/react-native/scripts/react_native_pods'
require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'
platform :ios, min_ios_version_supported
prepare_react_native_project!
flipper_config = ENV['NO_FLIPPER'] == "1" ? FlipperConfiguration.disabled : FlipperConfiguration.enabled
linkage = ENV['USE_FRAMEWORKS']
if linkage != nil
Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green
use_frameworks! :linkage => linkage.to_sym
end
platform :ios, '11.0'
install! 'cocoapods', :deterministic_uuids => false
target 'app' do
pod 'React-RCTLinking', :path => '../node_modules/react-native/Libraries/LinkingIOS'
@@ -21,16 +13,9 @@ target 'app' do
use_react_native!(
:path => config[:reactNativePath],
# Hermes is now enabled by default. Disable by setting this flag to false.
# Upcoming versions of React Native may rely on get_default_flags(), but
# we make it explicit here to aid in the React Native upgrade process.
# to enable hermes on iOS, change `false` to `true` and then install pods
:hermes_enabled => flags[:hermes_enabled],
:fabric_enabled => flags[:fabric_enabled],
# Enables Flipper.
#
# Note that if you have use_frameworks! enabled, Flipper will not work and
# you should disable the next line.
#:flipper_configuration => flipper_config,
# An absolute path to your application root.
:app_path => "#{Pod::Config.instance.installation_root}/.."
)
@@ -40,13 +25,14 @@ target 'app' do
# Pods for testing
end
# Enables Flipper.
#
# Note that if you have use_frameworks! enabled, Flipper will not work and
# you should disable the next line.
#use_flipper!()
post_install do |installer|
react_native_post_install(
installer,
# Set `mac_catalyst_enabled` to `true` in order to apply patches
# necessary for Mac Catalyst builds
:mac_catalyst_enabled => false
)
react_native_post_install(installer)
__apply_Xcode_12_5_M1_post_install_workaround(installer)
# iOS fix
+267 -381
View File
@@ -3,250 +3,227 @@ PODS:
- BVLinearGradient (2.6.2):
- React-Core
- DoubleConversion (1.1.6)
- FBLazyVector (0.71.0)
- FBReactNativeSpec (0.71.0):
- RCT-Folly (= 2021.07.22.00)
- RCTRequired (= 0.71.0)
- RCTTypeSafety (= 0.71.0)
- React-Core (= 0.71.0)
- React-jsi (= 0.71.0)
- ReactCommon/turbomodule/core (= 0.71.0)
- FBLazyVector (0.68.2)
- FBReactNativeSpec (0.68.2):
- RCT-Folly (= 2021.06.28.00-v2)
- RCTRequired (= 0.68.2)
- RCTTypeSafety (= 0.68.2)
- React-Core (= 0.68.2)
- React-jsi (= 0.68.2)
- ReactCommon/turbomodule/core (= 0.68.2)
- fmt (6.2.1)
- glog (0.3.5)
- hermes-engine (0.71.0):
- hermes-engine/Pre-built (= 0.71.0)
- hermes-engine/Pre-built (0.71.0)
- libevent (2.1.12)
- RCT-Folly (2021.07.22.00):
- RCT-Folly (2021.06.28.00-v2):
- boost
- DoubleConversion
- fmt (~> 6.2.1)
- glog
- RCT-Folly/Default (= 2021.07.22.00)
- RCT-Folly/Default (2021.07.22.00):
- RCT-Folly/Default (= 2021.06.28.00-v2)
- RCT-Folly/Default (2021.06.28.00-v2):
- boost
- DoubleConversion
- fmt (~> 6.2.1)
- glog
- RCT-Folly/Futures (2021.07.22.00):
- boost
- DoubleConversion
- fmt (~> 6.2.1)
- RCTRequired (0.68.2)
- RCTTypeSafety (0.68.2):
- FBLazyVector (= 0.68.2)
- RCT-Folly (= 2021.06.28.00-v2)
- RCTRequired (= 0.68.2)
- React-Core (= 0.68.2)
- React (0.68.2):
- React-Core (= 0.68.2)
- React-Core/DevSupport (= 0.68.2)
- React-Core/RCTWebSocket (= 0.68.2)
- React-RCTActionSheet (= 0.68.2)
- React-RCTAnimation (= 0.68.2)
- React-RCTBlob (= 0.68.2)
- React-RCTImage (= 0.68.2)
- React-RCTLinking (= 0.68.2)
- React-RCTNetwork (= 0.68.2)
- React-RCTSettings (= 0.68.2)
- React-RCTText (= 0.68.2)
- React-RCTVibration (= 0.68.2)
- React-callinvoker (0.68.2)
- React-Codegen (0.68.2):
- FBReactNativeSpec (= 0.68.2)
- RCT-Folly (= 2021.06.28.00-v2)
- RCTRequired (= 0.68.2)
- RCTTypeSafety (= 0.68.2)
- React-Core (= 0.68.2)
- React-jsi (= 0.68.2)
- React-jsiexecutor (= 0.68.2)
- ReactCommon/turbomodule/core (= 0.68.2)
- React-Core (0.68.2):
- glog
- libevent
- RCTRequired (0.71.0)
- RCTTypeSafety (0.71.0):
- FBLazyVector (= 0.71.0)
- RCTRequired (= 0.71.0)
- React-Core (= 0.71.0)
- React (0.71.0):
- React-Core (= 0.71.0)
- React-Core/DevSupport (= 0.71.0)
- React-Core/RCTWebSocket (= 0.71.0)
- React-RCTActionSheet (= 0.71.0)
- React-RCTAnimation (= 0.71.0)
- React-RCTBlob (= 0.71.0)
- React-RCTImage (= 0.71.0)
- React-RCTLinking (= 0.71.0)
- React-RCTNetwork (= 0.71.0)
- React-RCTSettings (= 0.71.0)
- React-RCTText (= 0.71.0)
- React-RCTVibration (= 0.71.0)
- React-callinvoker (0.71.0)
- React-Codegen (0.71.0):
- FBReactNativeSpec
- hermes-engine
- RCT-Folly
- RCTRequired
- RCTTypeSafety
- React-Core
- React-jsi
- React-jsiexecutor
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- React-Core (0.71.0):
- glog
- RCT-Folly (= 2021.07.22.00)
- React-Core/Default (= 0.71.0)
- React-cxxreact (= 0.71.0)
- React-jsi (= 0.71.0)
- React-jsiexecutor (= 0.71.0)
- React-perflogger (= 0.71.0)
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default (= 0.68.2)
- React-cxxreact (= 0.68.2)
- React-jsi (= 0.68.2)
- React-jsiexecutor (= 0.68.2)
- React-perflogger (= 0.68.2)
- Yoga
- React-Core/CoreModulesHeaders (0.71.0):
- React-Core/CoreModulesHeaders (0.68.2):
- glog
- RCT-Folly (= 2021.07.22.00)
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.71.0)
- React-jsi (= 0.71.0)
- React-jsiexecutor (= 0.71.0)
- React-perflogger (= 0.71.0)
- React-cxxreact (= 0.68.2)
- React-jsi (= 0.68.2)
- React-jsiexecutor (= 0.68.2)
- React-perflogger (= 0.68.2)
- Yoga
- React-Core/Default (0.71.0):
- React-Core/Default (0.68.2):
- glog
- RCT-Folly (= 2021.07.22.00)
- React-cxxreact (= 0.71.0)
- React-jsi (= 0.71.0)
- React-jsiexecutor (= 0.71.0)
- React-perflogger (= 0.71.0)
- RCT-Folly (= 2021.06.28.00-v2)
- React-cxxreact (= 0.68.2)
- React-jsi (= 0.68.2)
- React-jsiexecutor (= 0.68.2)
- React-perflogger (= 0.68.2)
- Yoga
- React-Core/DevSupport (0.71.0):
- React-Core/DevSupport (0.68.2):
- glog
- RCT-Folly (= 2021.07.22.00)
- React-Core/Default (= 0.71.0)
- React-Core/RCTWebSocket (= 0.71.0)
- React-cxxreact (= 0.71.0)
- React-jsi (= 0.71.0)
- React-jsiexecutor (= 0.71.0)
- React-jsinspector (= 0.71.0)
- React-perflogger (= 0.71.0)
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default (= 0.68.2)
- React-Core/RCTWebSocket (= 0.68.2)
- React-cxxreact (= 0.68.2)
- React-jsi (= 0.68.2)
- React-jsiexecutor (= 0.68.2)
- React-jsinspector (= 0.68.2)
- React-perflogger (= 0.68.2)
- Yoga
- React-Core/RCTActionSheetHeaders (0.71.0):
- React-Core/RCTActionSheetHeaders (0.68.2):
- glog
- RCT-Folly (= 2021.07.22.00)
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.71.0)
- React-jsi (= 0.71.0)
- React-jsiexecutor (= 0.71.0)
- React-perflogger (= 0.71.0)
- React-cxxreact (= 0.68.2)
- React-jsi (= 0.68.2)
- React-jsiexecutor (= 0.68.2)
- React-perflogger (= 0.68.2)
- Yoga
- React-Core/RCTAnimationHeaders (0.71.0):
- React-Core/RCTAnimationHeaders (0.68.2):
- glog
- RCT-Folly (= 2021.07.22.00)
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.71.0)
- React-jsi (= 0.71.0)
- React-jsiexecutor (= 0.71.0)
- React-perflogger (= 0.71.0)
- React-cxxreact (= 0.68.2)
- React-jsi (= 0.68.2)
- React-jsiexecutor (= 0.68.2)
- React-perflogger (= 0.68.2)
- Yoga
- React-Core/RCTBlobHeaders (0.71.0):
- React-Core/RCTBlobHeaders (0.68.2):
- glog
- RCT-Folly (= 2021.07.22.00)
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.71.0)
- React-jsi (= 0.71.0)
- React-jsiexecutor (= 0.71.0)
- React-perflogger (= 0.71.0)
- React-cxxreact (= 0.68.2)
- React-jsi (= 0.68.2)
- React-jsiexecutor (= 0.68.2)
- React-perflogger (= 0.68.2)
- Yoga
- React-Core/RCTImageHeaders (0.71.0):
- React-Core/RCTImageHeaders (0.68.2):
- glog
- RCT-Folly (= 2021.07.22.00)
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.71.0)
- React-jsi (= 0.71.0)
- React-jsiexecutor (= 0.71.0)
- React-perflogger (= 0.71.0)
- React-cxxreact (= 0.68.2)
- React-jsi (= 0.68.2)
- React-jsiexecutor (= 0.68.2)
- React-perflogger (= 0.68.2)
- Yoga
- React-Core/RCTLinkingHeaders (0.71.0):
- React-Core/RCTLinkingHeaders (0.68.2):
- glog
- RCT-Folly (= 2021.07.22.00)
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.71.0)
- React-jsi (= 0.71.0)
- React-jsiexecutor (= 0.71.0)
- React-perflogger (= 0.71.0)
- React-cxxreact (= 0.68.2)
- React-jsi (= 0.68.2)
- React-jsiexecutor (= 0.68.2)
- React-perflogger (= 0.68.2)
- Yoga
- React-Core/RCTNetworkHeaders (0.71.0):
- React-Core/RCTNetworkHeaders (0.68.2):
- glog
- RCT-Folly (= 2021.07.22.00)
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.71.0)
- React-jsi (= 0.71.0)
- React-jsiexecutor (= 0.71.0)
- React-perflogger (= 0.71.0)
- React-cxxreact (= 0.68.2)
- React-jsi (= 0.68.2)
- React-jsiexecutor (= 0.68.2)
- React-perflogger (= 0.68.2)
- Yoga
- React-Core/RCTSettingsHeaders (0.71.0):
- React-Core/RCTSettingsHeaders (0.68.2):
- glog
- RCT-Folly (= 2021.07.22.00)
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.71.0)
- React-jsi (= 0.71.0)
- React-jsiexecutor (= 0.71.0)
- React-perflogger (= 0.71.0)
- React-cxxreact (= 0.68.2)
- React-jsi (= 0.68.2)
- React-jsiexecutor (= 0.68.2)
- React-perflogger (= 0.68.2)
- Yoga
- React-Core/RCTTextHeaders (0.71.0):
- React-Core/RCTTextHeaders (0.68.2):
- glog
- RCT-Folly (= 2021.07.22.00)
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.71.0)
- React-jsi (= 0.71.0)
- React-jsiexecutor (= 0.71.0)
- React-perflogger (= 0.71.0)
- React-cxxreact (= 0.68.2)
- React-jsi (= 0.68.2)
- React-jsiexecutor (= 0.68.2)
- React-perflogger (= 0.68.2)
- Yoga
- React-Core/RCTVibrationHeaders (0.71.0):
- React-Core/RCTVibrationHeaders (0.68.2):
- glog
- RCT-Folly (= 2021.07.22.00)
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.71.0)
- React-jsi (= 0.71.0)
- React-jsiexecutor (= 0.71.0)
- React-perflogger (= 0.71.0)
- React-cxxreact (= 0.68.2)
- React-jsi (= 0.68.2)
- React-jsiexecutor (= 0.68.2)
- React-perflogger (= 0.68.2)
- Yoga
- React-Core/RCTWebSocket (0.71.0):
- React-Core/RCTWebSocket (0.68.2):
- glog
- RCT-Folly (= 2021.07.22.00)
- React-Core/Default (= 0.71.0)
- React-cxxreact (= 0.71.0)
- React-jsi (= 0.71.0)
- React-jsiexecutor (= 0.71.0)
- React-perflogger (= 0.71.0)
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default (= 0.68.2)
- React-cxxreact (= 0.68.2)
- React-jsi (= 0.68.2)
- React-jsiexecutor (= 0.68.2)
- React-perflogger (= 0.68.2)
- Yoga
- React-CoreModules (0.71.0):
- RCT-Folly (= 2021.07.22.00)
- RCTTypeSafety (= 0.71.0)
- React-Codegen (= 0.71.0)
- React-Core/CoreModulesHeaders (= 0.71.0)
- React-jsi (= 0.71.0)
- React-RCTImage (= 0.71.0)
- ReactCommon/turbomodule/core (= 0.71.0)
- React-cxxreact (0.71.0):
- React-CoreModules (0.68.2):
- RCT-Folly (= 2021.06.28.00-v2)
- RCTTypeSafety (= 0.68.2)
- React-Codegen (= 0.68.2)
- React-Core/CoreModulesHeaders (= 0.68.2)
- React-jsi (= 0.68.2)
- React-RCTImage (= 0.68.2)
- ReactCommon/turbomodule/core (= 0.68.2)
- React-cxxreact (0.68.2):
- boost (= 1.76.0)
- DoubleConversion
- glog
- RCT-Folly (= 2021.07.22.00)
- React-callinvoker (= 0.71.0)
- React-jsi (= 0.71.0)
- React-jsinspector (= 0.71.0)
- React-logger (= 0.71.0)
- React-perflogger (= 0.71.0)
- React-runtimeexecutor (= 0.71.0)
- React-hermes (0.71.0):
- DoubleConversion
- glog
- hermes-engine
- RCT-Folly (= 2021.07.22.00)
- RCT-Folly/Futures (= 2021.07.22.00)
- React-cxxreact (= 0.71.0)
- React-jsiexecutor (= 0.71.0)
- React-jsinspector (= 0.71.0)
- React-perflogger (= 0.71.0)
- React-jsi (0.71.0):
- RCT-Folly (= 2021.06.28.00-v2)
- React-callinvoker (= 0.68.2)
- React-jsi (= 0.68.2)
- React-jsinspector (= 0.68.2)
- React-logger (= 0.68.2)
- React-perflogger (= 0.68.2)
- React-runtimeexecutor (= 0.68.2)
- React-jsi (0.68.2):
- boost (= 1.76.0)
- DoubleConversion
- glog
- hermes-engine
- RCT-Folly (= 2021.07.22.00)
- React-jsiexecutor (0.71.0):
- RCT-Folly (= 2021.06.28.00-v2)
- React-jsi/Default (= 0.68.2)
- React-jsi/Default (0.68.2):
- boost (= 1.76.0)
- DoubleConversion
- glog
- RCT-Folly (= 2021.07.22.00)
- React-cxxreact (= 0.71.0)
- React-jsi (= 0.71.0)
- React-perflogger (= 0.71.0)
- React-jsinspector (0.71.0)
- React-logger (0.71.0):
- RCT-Folly (= 2021.06.28.00-v2)
- React-jsiexecutor (0.68.2):
- DoubleConversion
- glog
- react-native-blur (4.3.0):
- RCT-Folly (= 2021.06.28.00-v2)
- React-cxxreact (= 0.68.2)
- React-jsi (= 0.68.2)
- React-perflogger (= 0.68.2)
- React-jsinspector (0.68.2)
- React-logger (0.68.2):
- glog
- react-native-cameraroll (5.1.0):
- React-Core
- react-native-cameraroll (5.2.0):
- react-native-pager-view (6.0.2):
- React-Core
- react-native-image-resizer (3.0.4):
- React-Core
- react-native-pager-view (6.1.2):
- React-Core
- react-native-paste-input (0.6.0):
- React-Core
- Swime (= 3.0.6)
- react-native-safe-area-context (4.4.1):
- RCT-Folly
- RCTRequired
@@ -257,98 +234,76 @@ PODS:
- React-Core
- react-native-version-number (0.3.6):
- React
- React-perflogger (0.71.0)
- React-RCTActionSheet (0.71.0):
- React-Core/RCTActionSheetHeaders (= 0.71.0)
- React-RCTAnimation (0.71.0):
- RCT-Folly (= 2021.07.22.00)
- RCTTypeSafety (= 0.71.0)
- React-Codegen (= 0.71.0)
- React-Core/RCTAnimationHeaders (= 0.71.0)
- React-jsi (= 0.71.0)
- ReactCommon/turbomodule/core (= 0.71.0)
- React-RCTAppDelegate (0.71.0):
- RCT-Folly
- RCTRequired
- RCTTypeSafety
- React-Core
- ReactCommon/turbomodule/core
- React-RCTBlob (0.71.0):
- RCT-Folly (= 2021.07.22.00)
- React-Codegen (= 0.71.0)
- React-Core/RCTBlobHeaders (= 0.71.0)
- React-Core/RCTWebSocket (= 0.71.0)
- React-jsi (= 0.71.0)
- React-RCTNetwork (= 0.71.0)
- ReactCommon/turbomodule/core (= 0.71.0)
- React-RCTImage (0.71.0):
- RCT-Folly (= 2021.07.22.00)
- RCTTypeSafety (= 0.71.0)
- React-Codegen (= 0.71.0)
- React-Core/RCTImageHeaders (= 0.71.0)
- React-jsi (= 0.71.0)
- React-RCTNetwork (= 0.71.0)
- ReactCommon/turbomodule/core (= 0.71.0)
- React-RCTLinking (0.71.0):
- React-Codegen (= 0.71.0)
- React-Core/RCTLinkingHeaders (= 0.71.0)
- React-jsi (= 0.71.0)
- ReactCommon/turbomodule/core (= 0.71.0)
- React-RCTNetwork (0.71.0):
- RCT-Folly (= 2021.07.22.00)
- RCTTypeSafety (= 0.71.0)
- React-Codegen (= 0.71.0)
- React-Core/RCTNetworkHeaders (= 0.71.0)
- React-jsi (= 0.71.0)
- ReactCommon/turbomodule/core (= 0.71.0)
- React-RCTSettings (0.71.0):
- RCT-Folly (= 2021.07.22.00)
- RCTTypeSafety (= 0.71.0)
- React-Codegen (= 0.71.0)
- React-Core/RCTSettingsHeaders (= 0.71.0)
- React-jsi (= 0.71.0)
- ReactCommon/turbomodule/core (= 0.71.0)
- React-RCTText (0.71.0):
- React-Core/RCTTextHeaders (= 0.71.0)
- React-RCTVibration (0.71.0):
- RCT-Folly (= 2021.07.22.00)
- React-Codegen (= 0.71.0)
- React-Core/RCTVibrationHeaders (= 0.71.0)
- React-jsi (= 0.71.0)
- ReactCommon/turbomodule/core (= 0.71.0)
- React-runtimeexecutor (0.71.0):
- React-jsi (= 0.71.0)
- ReactCommon/turbomodule/bridging (0.71.0):
- React-perflogger (0.68.2)
- React-RCTActionSheet (0.68.2):
- React-Core/RCTActionSheetHeaders (= 0.68.2)
- React-RCTAnimation (0.68.2):
- RCT-Folly (= 2021.06.28.00-v2)
- RCTTypeSafety (= 0.68.2)
- React-Codegen (= 0.68.2)
- React-Core/RCTAnimationHeaders (= 0.68.2)
- React-jsi (= 0.68.2)
- ReactCommon/turbomodule/core (= 0.68.2)
- React-RCTBlob (0.68.2):
- RCT-Folly (= 2021.06.28.00-v2)
- React-Codegen (= 0.68.2)
- React-Core/RCTBlobHeaders (= 0.68.2)
- React-Core/RCTWebSocket (= 0.68.2)
- React-jsi (= 0.68.2)
- React-RCTNetwork (= 0.68.2)
- ReactCommon/turbomodule/core (= 0.68.2)
- React-RCTImage (0.68.2):
- RCT-Folly (= 2021.06.28.00-v2)
- RCTTypeSafety (= 0.68.2)
- React-Codegen (= 0.68.2)
- React-Core/RCTImageHeaders (= 0.68.2)
- React-jsi (= 0.68.2)
- React-RCTNetwork (= 0.68.2)
- ReactCommon/turbomodule/core (= 0.68.2)
- React-RCTLinking (0.68.2):
- React-Codegen (= 0.68.2)
- React-Core/RCTLinkingHeaders (= 0.68.2)
- React-jsi (= 0.68.2)
- ReactCommon/turbomodule/core (= 0.68.2)
- React-RCTNetwork (0.68.2):
- RCT-Folly (= 2021.06.28.00-v2)
- RCTTypeSafety (= 0.68.2)
- React-Codegen (= 0.68.2)
- React-Core/RCTNetworkHeaders (= 0.68.2)
- React-jsi (= 0.68.2)
- ReactCommon/turbomodule/core (= 0.68.2)
- React-RCTSettings (0.68.2):
- RCT-Folly (= 2021.06.28.00-v2)
- RCTTypeSafety (= 0.68.2)
- React-Codegen (= 0.68.2)
- React-Core/RCTSettingsHeaders (= 0.68.2)
- React-jsi (= 0.68.2)
- ReactCommon/turbomodule/core (= 0.68.2)
- React-RCTText (0.68.2):
- React-Core/RCTTextHeaders (= 0.68.2)
- React-RCTVibration (0.68.2):
- RCT-Folly (= 2021.06.28.00-v2)
- React-Codegen (= 0.68.2)
- React-Core/RCTVibrationHeaders (= 0.68.2)
- React-jsi (= 0.68.2)
- ReactCommon/turbomodule/core (= 0.68.2)
- React-runtimeexecutor (0.68.2):
- React-jsi (= 0.68.2)
- ReactCommon/turbomodule/core (0.68.2):
- DoubleConversion
- glog
- RCT-Folly (= 2021.07.22.00)
- React-callinvoker (= 0.71.0)
- React-Core (= 0.71.0)
- React-cxxreact (= 0.71.0)
- React-jsi (= 0.71.0)
- React-logger (= 0.71.0)
- React-perflogger (= 0.71.0)
- ReactCommon/turbomodule/core (0.71.0):
- DoubleConversion
- glog
- RCT-Folly (= 2021.07.22.00)
- React-callinvoker (= 0.71.0)
- React-Core (= 0.71.0)
- React-cxxreact (= 0.71.0)
- React-jsi (= 0.71.0)
- React-logger (= 0.71.0)
- React-perflogger (= 0.71.0)
- rn-fetch-blob (0.12.0):
- React-Core
- RNBackgroundFetch (4.1.8):
- React-Core
- RNCAsyncStorage (1.17.11):
- RCT-Folly (= 2021.06.28.00-v2)
- React-callinvoker (= 0.68.2)
- React-Core (= 0.68.2)
- React-cxxreact (= 0.68.2)
- React-jsi (= 0.68.2)
- React-logger (= 0.68.2)
- React-perflogger (= 0.68.2)
- RNCAsyncStorage (1.17.10):
- React-Core
- RNCClipboard (1.11.1):
- React-Core
- RNFS (2.20.0):
- React-Core
- RNGestureHandler (2.8.0):
- RNGestureHandler (2.6.1):
- React-Core
- RNImageCropPicker (0.38.1):
- React-Core
@@ -361,14 +316,7 @@ PODS:
- TOCropViewController
- RNInAppBrowser (3.7.0):
- React-Core
- RNNotifee (7.4.0):
- React-Core
- RNNotifee/NotifeeCore (= 7.4.0)
- RNNotifee/NotifeeCore (7.4.0):
- React-Core
- RNReactNativeHapticFeedback (1.14.0):
- React-Core
- RNReanimated (2.13.0):
- RNReanimated (2.10.0):
- DoubleConversion
- FBLazyVector
- FBReactNativeSpec
@@ -395,17 +343,11 @@ PODS:
- React-RCTText
- ReactCommon/turbomodule/core
- Yoga
- RNScreens (3.18.2):
- RNScreens (3.17.0):
- React-Core
- React-RCTImage
- RNSVG (12.5.0):
- RNSVG (12.4.4):
- React-Core
- segment-analytics-react-native (2.10.1):
- React-Core
- sovran-react-native
- sovran-react-native (0.4.5):
- React-Core
- Swime (3.0.6)
- TOCropViewController (2.6.1)
- Yoga (1.14.0)
@@ -416,8 +358,6 @@ DEPENDENCIES:
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
- FBReactNativeSpec (from `../node_modules/react-native/React/FBReactNativeSpec`)
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
- hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
- libevent (~> 2.1.12)
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
- RCTRequired (from `../node_modules/react-native/Libraries/RCTRequired`)
- RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`)
@@ -425,26 +365,22 @@ DEPENDENCIES:
- React-callinvoker (from `../node_modules/react-native/ReactCommon/callinvoker`)
- React-Codegen (from `build/generated/ios`)
- React-Core (from `../node_modules/react-native/`)
- React-Core/DevSupport (from `../node_modules/react-native/`)
- React-Core/RCTWebSocket (from `../node_modules/react-native/`)
- React-CoreModules (from `../node_modules/react-native/React/CoreModules`)
- React-cxxreact (from `../node_modules/react-native/ReactCommon/cxxreact`)
- React-hermes (from `../node_modules/react-native/ReactCommon/hermes`)
- React-jsi (from `../node_modules/react-native/ReactCommon/jsi`)
- React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`)
- React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`)
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
- "react-native-blur (from `../node_modules/@react-native-community/blur`)"
- "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-paste-input (from `../node_modules/@mattermost/react-native-paste-input`)"
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
- react-native-splash-screen (from `../node_modules/react-native-splash-screen`)
- react-native-version-number (from `../node_modules/react-native-version-number`)
- React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
- React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`)
- React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`)
- React-RCTAppDelegate (from `../node_modules/react-native/Libraries/AppDelegate`)
- React-RCTBlob (from `../node_modules/react-native/Libraries/Blob`)
- React-RCTImage (from `../node_modules/react-native/Libraries/Image`)
- React-RCTLinking (from `../node_modules/react-native/Libraries/LinkingIOS`)
@@ -454,28 +390,19 @@ 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`)
- RNBackgroundFetch (from `../node_modules/react-native-background-fetch`)
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
- "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)"
- RNFS (from `../node_modules/react-native-fs`)
- 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`)
- "RNNotifee (from `../node_modules/@notifee/react-native`)"
- 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`)
- "segment-analytics-react-native (from `../node_modules/@segment/analytics-react-native`)"
- "sovran-react-native (from `../node_modules/@segment/sovran-react-native`)"
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
SPEC REPOS:
trunk:
- fmt
- libevent
- Swime
- TOCropViewController
EXTERNAL SOURCES:
@@ -491,8 +418,6 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/React/FBReactNativeSpec"
glog:
:podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec"
hermes-engine:
:podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec"
RCT-Folly:
:podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec"
RCTRequired:
@@ -511,8 +436,6 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/React/CoreModules"
React-cxxreact:
:path: "../node_modules/react-native/ReactCommon/cxxreact"
React-hermes:
:path: "../node_modules/react-native/ReactCommon/hermes"
React-jsi:
:path: "../node_modules/react-native/ReactCommon/jsi"
React-jsiexecutor:
@@ -521,16 +444,10 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/jsinspector"
React-logger:
:path: "../node_modules/react-native/ReactCommon/logger"
react-native-blur:
:path: "../node_modules/@react-native-community/blur"
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-paste-input:
:path: "../node_modules/@mattermost/react-native-paste-input"
react-native-safe-area-context:
:path: "../node_modules/react-native-safe-area-context"
react-native-splash-screen:
@@ -543,8 +460,6 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/Libraries/ActionSheetIOS"
React-RCTAnimation:
:path: "../node_modules/react-native/Libraries/NativeAnimation"
React-RCTAppDelegate:
:path: "../node_modules/react-native/Libraries/AppDelegate"
React-RCTBlob:
:path: "../node_modules/react-native/Libraries/Blob"
React-RCTImage:
@@ -563,36 +478,22 @@ 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"
RNBackgroundFetch:
:path: "../node_modules/react-native-background-fetch"
RNCAsyncStorage:
:path: "../node_modules/@react-native-async-storage/async-storage"
RNCClipboard:
:path: "../node_modules/@react-native-clipboard/clipboard"
RNFS:
:path: "../node_modules/react-native-fs"
RNGestureHandler:
:path: "../node_modules/react-native-gesture-handler"
RNImageCropPicker:
:path: "../node_modules/react-native-image-crop-picker"
RNInAppBrowser:
:path: "../node_modules/react-native-inappbrowser-reborn"
RNNotifee:
:path: "../node_modules/@notifee/react-native"
RNReactNativeHapticFeedback:
:path: "../node_modules/react-native-haptic-feedback"
RNReanimated:
:path: "../node_modules/react-native-reanimated"
RNScreens:
:path: "../node_modules/react-native-screens"
RNSVG:
:path: "../node_modules/react-native-svg"
segment-analytics-react-native:
:path: "../node_modules/@segment/analytics-react-native"
sovran-react-native:
:path: "../node_modules/@segment/sovran-react-native"
Yoga:
:path: "../node_modules/react-native/ReactCommon/yoga"
@@ -600,66 +501,51 @@ SPEC CHECKSUMS:
boost: a7c83b31436843459a1961bfd74b96033dc77234
BVLinearGradient: 34a999fda29036898a09c6a6b728b0b4189e1a44
DoubleConversion: 831926d9b8bf8166fd87886c4abab286c2422662
FBLazyVector: 61839cba7a48c570b7ac3e1cd8a4d0948382202f
FBReactNativeSpec: 5a14398ccf5e27c1ca2d7109eb920594ce93c10d
FBLazyVector: a7a655862f6b09625d11c772296b01cd5164b648
FBReactNativeSpec: 81ce99032d5b586fddd6a38d450f8595f7e04be4
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
glog: 476ee3e89abb49e07f822b48323c51c57124b572
hermes-engine: f6e715aa6c8bd38de6c13bc85e07b0a337edaa89
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1
RCTRequired: dea3e4163184ea57c50288c15c32c1529265c58f
RCTTypeSafety: a0834ab89159a346731e8aae55ad6e2cce61c327
React: d877d055ff2137ca0325a4babdef3411e11f3cb7
React-callinvoker: 77bd2701eee3acac154b11ec219e68d5a1f780ad
React-Codegen: bccc516adc1551ccfe04b0de27e345d38829b204
React-Core: 4035f59e5bec8f3053583c6108d99c7516deb760
React-CoreModules: b6a1f76423fea57a03e0d7a2f79d3b55cf193f2c
React-cxxreact: fe5f6ec8ae875bebc71309d1e8ef89bb966d61a6
React-hermes: 3c8ea5e8f402db2a08b57051206d7f2ba9c75565
React-jsi: dbf0f82c93bfd828fa05c50f2ee74dc81f711050
React-jsiexecutor: 060dd495f1e2af3d87216f7ca8a94c55ec885b4f
React-jsinspector: 5061fcbec93fd672183dfb39cc2f65e55a0835db
React-logger: a6c0b3a807a8e81f6d7fea2e72660766f55daa50
react-native-blur: 50c9feabacbc5f49b61337ebc32192c6be7ec3c3
react-native-cameraroll: 0ff04cc4e0ff5f19a94ff4313e5c8bc4503cd86d
react-native-image-resizer: 794abf75ec13ed1f0dbb1f134e27504ea65e9e66
react-native-pager-view: 54bed894cecebe28cede54c01038d9d1e122de43
react-native-paste-input: 5182843692fd2ec72be50f241a38a49796e225d7
RCT-Folly: 4d8508a426467c48885f1151029bc15fa5d7b3b8
RCTRequired: 3e917ea5377751094f38145fdece525aa90545a0
RCTTypeSafety: c43c072a4bd60feb49a9570b0517892b4305c45e
React: 176dd882de001854ced260fad41bb68a31aa4bd0
React-callinvoker: c2864d1818d6e64928d2faf774a3800dfc38fe1f
React-Codegen: 98b6f97f0a7abf7d67e4ce435c77c05b7a95cf05
React-Core: fdaa2916b1c893f39f02cff0476d1fb0cab1e352
React-CoreModules: fd8705b80699ec36c2cdd635c2ce9d874b9cfdfc
React-cxxreact: 1832d971f7b0cb2c7b943dc0ec962762c90c906e
React-jsi: 72af715135abe8c3f0dcf3b2548b71d048b69a7e
React-jsiexecutor: b7b553412f2ec768fe6c8f27cd6bafdb9d8719e6
React-jsinspector: c5989c77cb89ae6a69561095a61cce56a44ae8e8
React-logger: a0833912d93b36b791b7a521672d8ee89107aff1
react-native-cameraroll: a40b082318eb1ecd0336a2f29d9f74b7f2c8cae8
react-native-pager-view: 592421df0259bf7a7a4fe85b74c24f3f39905605
react-native-safe-area-context: 99b24a0c5acd0d5dcac2b1a7f18c49ea317be99a
react-native-splash-screen: 4312f786b13a81b5169ef346d76d33bc0c6dc457
react-native-version-number: b415bbec6a13f2df62bf978e85bc0d699462f37f
React-perflogger: e5fc4149e9bbb972b8520277f3b23141faa47a36
React-RCTActionSheet: 991de88216bf03ab9bb1d213d73c62ecbe64ade7
React-RCTAnimation: b74e3d1bf5280891a573e447b487fa1db0713b5b
React-RCTAppDelegate: f52667f2dbc510f87b7988c5204e8764d50bf0c1
React-RCTBlob: 6762787c01d5d8d18efed03764b0d58d3b79595a
React-RCTImage: 9ed7eba8dd192a49def2cad2ecaedee7e7e315b4
React-RCTLinking: 0b58eed9af0645a161b80bf412b6b721e4585c66
React-RCTNetwork: dc075b0eea00d8a98c928f011d9bc2458acc7092
React-RCTSettings: 30fb3f498cfaf8a4bb47334ff9ffbe318ef78766
React-RCTText: a631564e84a227fe24bae7c04446f36faea7fcf5
React-RCTVibration: 55c91eccdbd435d7634efbe847086944389475b0
React-runtimeexecutor: ac80782d9d76ba2b0f709f4de0c427fe33c352dc
ReactCommon: 20e38a9be5fe1341b5e422220877cc94034776ba
rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba
RNBackgroundFetch: 8e16176ff415daac743a6eb57afc8e9e14dbe623
RNCAsyncStorage: 8616bd5a58af409453ea4e1b246521bb76578d60
React-perflogger: a18b4f0bd933b8b24ecf9f3c54f9bf65180f3fe6
React-RCTActionSheet: 547fe42fdb4b6089598d79f8e1d855d7c23e2162
React-RCTAnimation: bc9440a1c37b06ae9ebbb532d244f607805c6034
React-RCTBlob: a1295c8e183756d7ef30ba6e8f8144dfe8a19215
React-RCTImage: a30d1ee09b1334067fbb6f30789aae2d7ac150c9
React-RCTLinking: ffc6d5b88d1cb9aca13c54c2ec6507fbf07f2ac4
React-RCTNetwork: f807a2facab6cf5cf36d592e634611de9cf12d81
React-RCTSettings: 861806819226ed8332e6a8f90df2951a34bb3e7f
React-RCTText: f3fb464cc41a50fc7a1aba4deeb76a9ad8282cb9
React-RCTVibration: 79040b92bfa9c3c2d2cb4f57e981164ec7ab9374
React-runtimeexecutor: b960b687d2dfef0d3761fbb187e01812ebab8b23
ReactCommon: 095366164a276d91ea704ce53cb03825c487a3f2
RNCAsyncStorage: 0c357f3156fcb16c8589ede67cc036330b6698ca
RNCClipboard: 2834e1c4af68697089cdd455ee4a4cdd198fa7dd
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
RNGestureHandler: 62232ba8f562f7dea5ba1b3383494eb5bf97a4d3
RNGestureHandler: 28ad20bf02257791f7f137b31beef34b9549f54b
RNImageCropPicker: 648356d68fbf9911a1016b3e3723885d28373eda
RNInAppBrowser: e36d6935517101ccba0e875bac8ad7b0cb655364
RNNotifee: da8dcf09f079ea22f46e239d7c406e10d4525a5f
RNReactNativeHapticFeedback: 1e3efeca9628ff9876ee7cdd9edec1b336913f8c
RNReanimated: d8d9d3d3801bda5e35e85cdffc871577d044dc2e
RNScreens: 34cc502acf1b916c582c60003dc3089fa01dc66d
RNSVG: 6adc5c52d2488a476248413064b7f2832e639057
segment-analytics-react-native: cb097e393c3560a0d4cfd877044293e37b0050d9
sovran-react-native: fd3dc8f1a4b14acdc4ad25fc6b4ac4f52a2a2a15
Swime: d7b2c277503b6cea317774aedc2dce05613f8b0b
RNReanimated: 5bdcbcc3a72aedeee7bb099604262403aa75a1e5
RNScreens: 0df01424e9e0ed7827200d6ed1087ddd06c493f9
RNSVG: ecd661f380a07ba690c9c5929c475a44f432d674
TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863
Yoga: c618b544ff8bd8865cdca602f00cbcdb92fd6d31
Yoga: 99652481fcd320aefa4a7ef90095b95acd181952
PODFILE CHECKSUM: 0975a639c66f07f4d49706dd0bf7c3aa4dc833cf
PODFILE CHECKSUM: cf94853ebcb0d8e0d027dca9ab7a4ede886a8f20
COCOAPODS: 1.11.3
+72 -113
View File
@@ -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 */; };
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 */; };
379AA1F602EF704C84D7A02B /* libPods-app-appTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 05B90550DA1387B5C44FB80A /* libPods-app-appTests.a */; };
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
8896F504D7727F539A45F5E3 /* libPods-app.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FC3B8692C2BCABAF5171340D /* libPods-app.a */; };
E4BBD590292C1F5200296224 /* app.entitlements in Resources */ = {isa = PBXBuildFile; fileRef = E4437C9E28581FA7006DA9E7 /* app.entitlements */; };
/* End PBXBuildFile section */
@@ -31,21 +31,21 @@
00E356EE1AD99517003FC87E /* appTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = appTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
00E356F21AD99517003FC87E /* appTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = appTests.m; sourceTree = "<group>"; };
05B90550DA1387B5C44FB80A /* libPods-app-appTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-app-appTests.a"; sourceTree = BUILT_PRODUCTS_DIR; };
13B07F961A680F5B00A75B9A /* app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = app.app; sourceTree = BUILT_PRODUCTS_DIR; };
13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = app/AppDelegate.h; sourceTree = "<group>"; };
13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = app/AppDelegate.mm; sourceTree = "<group>"; };
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>"; };
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>"; };
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; };
98E4A0BAD72B0C24AF5986D9 /* 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>"; };
9FA2BF4F6574C986E6B102F0 /* 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>"; };
BC19D4073A1A93D07CD74999 /* 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>"; };
E1EFCDF82E4E346591FBB028 /* 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>"; };
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; };
FC3B8692C2BCABAF5171340D /* libPods-app.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-app.a"; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -53,7 +53,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
5698CA584FD738B2091BD18F /* libPods-app-appTests.a in Frameworks */,
379AA1F602EF704C84D7A02B /* libPods-app-appTests.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -61,7 +61,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
67BF1AE6AABFC881715B2D6A /* libPods-app.a in Frameworks */,
8896F504D7727F539A45F5E3 /* libPods-app.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -103,8 +103,8 @@
isa = PBXGroup;
children = (
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
8789F612EDA2C48C6064ADD6 /* libPods-app.a */,
C0C7BDE7769B84011D3747DC /* libPods-app-appTests.a */,
FC3B8692C2BCABAF5171340D /* libPods-app.a */,
05B90550DA1387B5C44FB80A /* libPods-app-appTests.a */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -143,10 +143,10 @@
BBD78D7AC51CEA395F1C20DB /* Pods */ = {
isa = PBXGroup;
children = (
8BB4EDB104E125B8A1913E74 /* Pods-app.debug.xcconfig */,
53DBA218C184B95B107AC33E /* Pods-app.release.xcconfig */,
C01FB6762BC17DADC0319338 /* Pods-app-appTests.debug.xcconfig */,
A8E093A0B5DA947150924A68 /* Pods-app-appTests.release.xcconfig */,
98E4A0BAD72B0C24AF5986D9 /* Pods-app.debug.xcconfig */,
9FA2BF4F6574C986E6B102F0 /* Pods-app.release.xcconfig */,
E1EFCDF82E4E346591FBB028 /* Pods-app-appTests.debug.xcconfig */,
BC19D4073A1A93D07CD74999 /* Pods-app-appTests.release.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
@@ -158,12 +158,11 @@
isa = PBXNativeTarget;
buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "appTests" */;
buildPhases = (
A6849EFFDCB214D4F4EAE972 /* [CP] Check Pods Manifest.lock */,
420689A074E4E7426A392B09 /* [CP] Check Pods Manifest.lock */,
00E356EA1AD99517003FC87E /* Sources */,
00E356EB1AD99517003FC87E /* Frameworks */,
00E356EC1AD99517003FC87E /* Resources */,
DFA0C2B14E2F369C4B2337CE /* [CP] Copy Pods Resources */,
B73FFC16945F7B215A06B698 /* [CP] Embed Pods Frameworks */,
C5618360E9363F58ED30C68E /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -179,14 +178,13 @@
isa = PBXNativeTarget;
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "app" */;
buildPhases = (
DDF15D430A078CE70E577FDA /* [CP] Check Pods Manifest.lock */,
1C451C95EB728E95B9CDD2FF /* [CP] Check Pods Manifest.lock */,
FD10A7F022414F080027D42C /* Start Packager */,
13B07F871A680F5B00A75B9A /* Sources */,
13B07F8C1A680F5B00A75B9A /* Frameworks */,
13B07F8E1A680F5B00A75B9A /* Resources */,
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
45E56D79C207C80AF89FBAA2 /* [CP] Copy Pods Resources */,
B0229D18A3C8908C642F9131 /* [CP] Embed Pods Frameworks */,
BC7FC72051CB585D346FAE10 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -260,90 +258,15 @@
files = (
);
inputPaths = (
"$(SRCROOT)/.xcode.env.local",
"$(SRCROOT)/.xcode.env",
);
name = "Bundle React Native code and images";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "set -e\n\nWITH_ENVIRONMENT=\"../node_modules/react-native/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"../node_modules/react-native/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n";
shellScript = "set -e\n\nexport BUNDLE_COMMAND=\"ram-bundle\"\nexport NODE_BINARY=node\n../node_modules/react-native/scripts/react-native-xcode.sh\n";
};
45E56D79C207C80AF89FBAA2 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${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/Pods-app-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-app/Pods-app-resources.sh\"\n";
showEnvVarsInLog = 0;
};
A6849EFFDCB214D4F4EAE972 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-app-appTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
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;
};
B0229D18A3C8908C642F9131 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-app/Pods-app-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-app/Pods-app-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-app/Pods-app-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
B73FFC16945F7B215A06B698 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-app-appTests/Pods-app-appTests-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-app-appTests/Pods-app-appTests-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-app-appTests/Pods-app-appTests-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
DDF15D430A078CE70E577FDA /* [CP] Check Pods Manifest.lock */ = {
1C451C95EB728E95B9CDD2FF /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -365,7 +288,46 @@
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;
};
DFA0C2B14E2F369C4B2337CE /* [CP] Copy Pods Resources */ = {
420689A074E4E7426A392B09 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-app-appTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
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;
};
BC7FC72051CB585D346FAE10 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${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/Pods-app-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-app/Pods-app-resources.sh\"\n";
showEnvVarsInLog = 0;
};
C5618360E9363F58ED30C68E /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -434,7 +396,7 @@
/* Begin XCBuildConfiguration section */
00E356F61AD99517003FC87E /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = C01FB6762BC17DADC0319338 /* Pods-app-appTests.debug.xcconfig */;
baseConfigurationReference = E1EFCDF82E4E346591FBB028 /* Pods-app-appTests.debug.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUNDLE_LOADER = "$(TEST_HOST)";
@@ -462,7 +424,7 @@
};
00E356F71AD99517003FC87E /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = A8E093A0B5DA947150924A68 /* Pods-app-appTests.release.xcconfig */;
baseConfigurationReference = BC19D4073A1A93D07CD74999 /* Pods-app-appTests.release.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUNDLE_LOADER = "$(TEST_HOST)";
@@ -487,7 +449,7 @@
};
13B07F941A680F5B00A75B9A /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 8BB4EDB104E125B8A1913E74 /* Pods-app.debug.xcconfig */;
baseConfigurationReference = 98E4A0BAD72B0C24AF5986D9 /* Pods-app.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
@@ -497,7 +459,7 @@
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
INFOPLIST_FILE = app/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.4;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -517,7 +479,7 @@
};
13B07F951A680F5B00A75B9A /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 53DBA218C184B95B107AC33E /* Pods-app.release.xcconfig */;
baseConfigurationReference = 9FA2BF4F6574C986E6B102F0 /* Pods-app.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
@@ -526,12 +488,11 @@
DEVELOPMENT_TEAM = B3LX46C5HS;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
INFOPLIST_FILE = app/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.4;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -577,7 +538,7 @@
COPY_PHASE_STRIP = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "";
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
@@ -593,7 +554,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.4;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
/usr/lib/swift,
"$(inherited)",
@@ -611,7 +572,6 @@
"-DFOLLY_MOBILE=1",
"-DFOLLY_USE_LIBCPP=1",
);
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
};
name = Debug;
@@ -649,7 +609,7 @@
COPY_PHASE_STRIP = YES;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "";
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
@@ -658,7 +618,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.4;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
/usr/lib/swift,
"$(inherited)",
@@ -675,7 +635,6 @@
"-DFOLLY_MOBILE=1",
"-DFOLLY_USE_LIBCPP=1",
);
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
VALIDATE_PRODUCT = YES;
};
+4 -2
View File
@@ -1,6 +1,8 @@
#import <RCTAppDelegate.h>
#import <React/RCTBridgeDelegate.h>
#import <UIKit/UIKit.h>
@interface AppDelegate : RCTAppDelegate
@interface AppDelegate : UIResponder <UIApplicationDelegate, RCTBridgeDelegate>
@property (nonatomic, strong) UIWindow *window;
@end
+88 -11
View File
@@ -1,6 +1,10 @@
#import "AppDelegate.h"
#import <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import <React/RCTAppSetupUtils.h>
// universal links
#import <React/RCTLinkingManager.h>
@@ -8,15 +12,59 @@
// splash screen
#import "RNSplashScreen.h"
#if RCT_NEW_ARCH_ENABLED
#import <React/CoreModulesPlugins.h>
#import <React/RCTCxxBridgeDelegate.h>
#import <React/RCTFabricSurfaceHostingProxyRootView.h>
#import <React/RCTSurfacePresenter.h>
#import <React/RCTSurfacePresenterBridgeAdapter.h>
#import <ReactCommon/RCTTurboModuleManager.h>
#import <react/config/ReactNativeConfig.h>
@interface AppDelegate () <RCTCxxBridgeDelegate, RCTTurboModuleManagerDelegate> {
RCTTurboModuleManager *_turboModuleManager;
RCTSurfacePresenterBridgeAdapter *_bridgeAdapter;
std::shared_ptr<const facebook::react::ReactNativeConfig> _reactNativeConfig;
facebook::react::ContextContainer::Shared _contextContainer;
}
@end
#endif
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Show the splash screen
// [RNSplashScreen show];
RCTAppSetupPrepareApp(application);
self.moduleName = @"xyz.blueskyweb.app";
return [super application:application didFinishLaunchingWithOptions:launchOptions];
RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];
#if RCT_NEW_ARCH_ENABLED
_contextContainer = std::make_shared<facebook::react::ContextContainer const>();
_reactNativeConfig = std::make_shared<facebook::react::EmptyReactNativeConfig const>();
_contextContainer->insert("ReactNativeConfig", _reactNativeConfig);
_bridgeAdapter = [[RCTSurfacePresenterBridgeAdapter alloc] initWithBridge:bridge contextContainer:_contextContainer];
bridge.surfacePresenter = _bridgeAdapter.surfacePresenter;
#endif
UIView *rootView = RCTAppSetupDefaultRootView(bridge, @"xyz.blueskyweb.app", nil);
if (@available(iOS 13.0, *)) {
rootView.backgroundColor = [UIColor systemBackgroundColor];
} else {
rootView.backgroundColor = [UIColor whiteColor];
}
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIViewController *rootViewController = [UIViewController new];
rootViewController.view = rootView;
self.window.rootViewController = rootViewController;
[self.window makeKeyAndVisible];
// Show the splash screen
[RNSplashScreen show];
return YES;
}
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
@@ -28,16 +76,45 @@
#endif
}
/// This method controls whether the `concurrentRoot`feature of React18 is turned on or off.
///
/// @see: https://reactjs.org/blog/2022/03/29/react-v18.html
/// @note: This requires to be rendering on Fabric (i.e. on the New Architecture).
/// @return: `true` if the `concurrentRoot` feature is enabled. Otherwise, it returns `false`.
- (BOOL)concurrentRootEnabled
#if RCT_NEW_ARCH_ENABLED
#pragma mark - RCTCxxBridgeDelegate
- (std::unique_ptr<facebook::react::JSExecutorFactory>)jsExecutorFactoryForBridge:(RCTBridge *)bridge
{
return true;
_turboModuleManager = [[RCTTurboModuleManager alloc] initWithBridge:bridge
delegate:self
jsInvoker:bridge.jsCallInvoker];
return RCTAppSetupDefaultJsExecutorFactory(bridge, _turboModuleManager);
}
#pragma mark RCTTurboModuleManagerDelegate
- (Class)getModuleClassFromName:(const char *)name
{
return RCTCoreModulesClassProvider(name);
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const std::string &)name
jsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
{
return nullptr;
}
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const std::string &)name
initParams:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return nullptr;
}
- (id<RCTTurboModule>)getModuleInstanceFromClass:(Class)moduleClass
{
return RCTAppSetupDefaultModuleFromClass(moduleClass);
}
#endif
// universal links
- (BOOL)application:(UIApplication *)application
openURL:(NSURL *)url
+3
View File
@@ -0,0 +1,3 @@
jest.mock('@react-native-async-storage/async-storage', () =>
require('@react-native-async-storage/async-storage/jest/async-storage-mock'),
)
-57
View File
@@ -1,57 +0,0 @@
/* global jest */
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('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,
}))
jest.mock('@segment/analytics-react-native', () => ({
createClient: () => ({
add: jest.fn(),
}),
useAnalytics: () => ({
track: jest.fn(),
identify: jest.fn(),
reset: jest.fn(),
group: jest.fn(),
screen: jest.fn(),
alias: jest.fn(),
flush: jest.fn(),
}),
}))
-198
View File
@@ -1,198 +0,0 @@
import {AddressInfo} from 'net'
import os from 'os'
import path from 'path'
import * as crypto from '@atproto/crypto'
import PDSServer, {
Database as PDSDatabase,
MemoryBlobStore,
ServerConfig as PDSServerConfig,
} from '@atproto/pds'
import * as plc from '@atproto/plc'
import AtpApi, {ServiceClient} from '@atproto/api'
export interface TestUser {
email: string
did: string
declarationCid: string
handle: string
password: string
api: ServiceClient
}
export interface TestUsers {
alice: TestUser
bob: TestUser
carla: TestUser
}
export interface TestPDS {
pdsUrl: string
users: TestUsers
close: () => Promise<void>
}
// NOTE
// deterministic date generator
// we use this to ensure the mock dataset is always the same
// which is very useful when testing
function* dateGen() {
let start = 1657846031914
while (true) {
yield new Date(start).toISOString()
start += 1e3
}
}
export async function createServer(): Promise<TestPDS> {
const keypair = await crypto.EcdsaKeypair.create()
// run plc server
const plcDb = plc.Database.memory()
await plcDb.migrateToLatestOrThrow()
const plcServer = plc.PlcServer.create({db: plcDb})
const plcListener = await plcServer.start()
const plcPort = (plcListener.address() as AddressInfo).port
const plcUrl = `http://localhost:${plcPort}`
const recoveryKey = (await crypto.EcdsaKeypair.create()).did()
const plcClient = new plc.PlcClient(plcUrl)
const serverDid = await plcClient.createDid(
keypair,
recoveryKey,
'localhost',
'https://pds.public.url',
)
const blobstoreLoc = path.join(os.tmpdir(), crypto.randomStr(5, 'base32'))
const cfg = new PDSServerConfig({
debugMode: true,
version: '0.0.0',
scheme: 'http',
hostname: 'localhost',
serverDid,
recoveryKey,
adminPassword: 'admin-pass',
inviteRequired: false,
didPlcUrl: plcUrl,
jwtSecret: 'jwt-secret',
availableUserDomains: ['.test'],
appUrlPasswordReset: 'app://forgot-password',
emailNoReplyAddress: 'noreply@blueskyweb.xyz',
publicUrl: 'https://pds.public.url',
imgUriSalt: '9dd04221f5755bce5f55f47464c27e1e',
imgUriKey:
'f23ecd142835025f42c3db2cf25dd813956c178392760256211f9d315f8ab4d8',
dbPostgresUrl: process.env.DB_POSTGRES_URL,
blobstoreLocation: `${blobstoreLoc}/blobs`,
blobstoreTmp: `${blobstoreLoc}/tmp`,
})
const db = PDSDatabase.memory()
await db.migrateToLatestOrThrow()
const blobstore = new MemoryBlobStore()
const pds = PDSServer.create({db, blobstore, keypair, config: cfg})
const pdsServer = await pds.start()
const pdsPort = (pdsServer.address() as AddressInfo).port
const pdsUrl = `http://localhost:${pdsPort}`
const testUsers = await genMockData(pdsUrl)
return {
pdsUrl,
users: testUsers,
async close() {
await pds.destroy()
await plcServer.destroy()
},
}
}
async function genMockData(pdsUrl: string): Promise<TestUsers> {
const date = dateGen()
const clients = {
loggedout: AtpApi.service(pdsUrl),
alice: AtpApi.service(pdsUrl),
bob: AtpApi.service(pdsUrl),
carla: AtpApi.service(pdsUrl),
}
const users: TestUser[] = [
{
email: 'alice@test.com',
did: '',
declarationCid: '',
handle: 'alice.test',
password: 'hunter2',
api: clients.alice,
},
{
email: 'bob@test.com',
did: '',
declarationCid: '',
handle: 'bob.test',
password: 'hunter2',
api: clients.bob,
},
{
email: 'carla@test.com',
did: '',
declarationCid: '',
handle: 'carla.test',
password: 'hunter2',
api: clients.carla,
},
]
const alice = users[0]
const bob = users[1]
const carla = users[2]
let _i = 1
for (const user of users) {
const res = await clients.loggedout.com.atproto.account.create({
email: user.email,
handle: user.handle,
password: user.password,
})
user.api.setHeader('Authorization', `Bearer ${res.data.accessJwt}`)
const {data: profile} = await user.api.app.bsky.actor.getProfile({
actor: user.handle,
})
user.did = res.data.did
user.declarationCid = profile.declaration.cid
await user.api.app.bsky.actor.profile.create(
{did: user.did},
{
displayName: ucfirst(user.handle).slice(0, -5),
description: `Test user ${_i++}`,
},
)
}
// everybody follows everybody
const follow = async (author: TestUser, subject: TestUser) => {
await author.api.app.bsky.graph.follow.create(
{did: author.did},
{
subject: {
did: subject.did,
declarationCid: subject.declarationCid,
},
createdAt: date.next().value,
},
)
}
await follow(alice, bob)
await follow(alice, carla)
await follow(bob, alice)
await follow(bob, carla)
await follow(carla, alice)
await follow(carla, bob)
return {alice, bob, carla}
}
function ucfirst(str: string): string {
return str.at(0)?.toUpperCase() + str.slice(1)
}
-29
View File
@@ -1,29 +0,0 @@
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 {ThemeProvider} from '../src/view/lib/ThemeContext'
import {mockedRootStore} from '../__mocks__/state-mock'
const customRender = (ui: any, rootStore?: any) =>
render(
// eslint-disable-next-line react-native/no-inline-styles
<GestureHandlerRootView style={{flex: 1}}>
<RootSiblingParent>
<RootStoreProvider
value={rootStore != null ? rootStore : mockedRootStore}>
<ThemeProvider theme="light">
<SafeAreaProvider>{ui}</SafeAreaProvider>
</ThemeProvider>
</RootStoreProvider>
</RootSiblingParent>
</GestureHandlerRootView>,
)
// re-export everything
export * from '@testing-library/react-native'
// override render method
export {customRender as render}
+2
View File
@@ -4,6 +4,8 @@
*
* @format
*/
const metroResolver = require('metro-resolver')
const path = require('path')
module.exports = {
transformer: {
+14 -56
View File
@@ -8,30 +8,18 @@
"web": "react-scripts start",
"start": "react-native start",
"clean-cache": "rm -rf node_modules/.cache/babel-loader/*",
"test": "jest --forceExit",
"test-watch": "jest --watchAll",
"test-ci": "jest --ci --forceExit --reporters=default --reporters=jest-junit",
"test-coverage": "jest --coverage",
"test": "jest",
"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",
"@fortawesome/react-native-fontawesome": "^0.3.0",
"@gorhom/bottom-sheet": "^4",
"@mattermost/react-native-paste-input": "^0.6.0",
"@notifee/react-native": "^7.4.0",
"@react-native-async-storage/async-storage": "^1.17.6",
"@react-native-camera-roll/camera-roll": "^5.1.0",
"@react-native-clipboard/clipboard": "^1.10.0",
"@react-native-community/blur": "^4.3.0",
"@segment/analytics-react-native": "^2.10.1",
"@segment/sovran-react-native": "^0.4.5",
"@zxing/text-encoding": "^0.9.0",
"base64-js": "^1.5.1",
"email-validator": "^2.0.4",
@@ -41,17 +29,14 @@
"lru_map": "^0.4.1",
"mobx": "^6.6.1",
"mobx-react-lite": "^3.4.0",
"react": "18.2.0",
"react": "17.0.2",
"react-circular-progressbar": "^2.1.0",
"react-dom": "17.0.2",
"react-native": "0.71.0",
"react-native": "0.68.2",
"react-native-appstate-hook": "^1.0.6",
"react-native-background-fetch": "^4.1.8",
"react-native-fs": "^2.20.0",
"react-native-bundle-splitter": "^2.2.3",
"react-native-gesture-handler": "^2.5.0",
"react-native-haptic-feedback": "^1.14.0",
"react-native-image-crop-picker": "^0.38.1",
"react-native-image-viewing": "^0.2.2",
"react-native-inappbrowser-reborn": "^3.6.3",
"react-native-linear-gradient": "^2.6.2",
"react-native-pager-view": "^6.0.2",
@@ -67,36 +52,28 @@
"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",
"zod": "^3.20.2"
"tlds": "^1.234.0"
},
"devDependencies": {
"@atproto/pds": "^0.0.1",
"@babel/core": "^7.12.9",
"@babel/preset-env": "^7.14.0",
"@babel/runtime": "^7.12.5",
"@react-native-community/eslint-config": "^3.0.0",
"@testing-library/jest-native": "^5.3.3",
"@testing-library/react-native": "^11.5.0",
"@react-native-community/eslint-config": "^2.0.0",
"@types/he": "^1.1.2",
"@types/jest": "^26.0.23",
"@types/lodash.chunk": "^4.2.7",
"@types/lodash.omit": "^4.5.7",
"@types/react-native": "^0.67.3",
"@types/react-test-renderer": "^17.0.1",
"@typescript-eslint/eslint-plugin": "^5.48.2",
"@typescript-eslint/parser": "^5.48.2",
"babel-jest": "^29.2.1",
"@typescript-eslint/eslint-plugin": "^5.17.0",
"@typescript-eslint/parser": "^5.17.0",
"babel-jest": "^26.6.3",
"babel-plugin-react-native-web": "^0.17.7",
"eslint": "^8.19.0",
"jest": "^29.2.1",
"jest-junit": "^15.0.0",
"metro-react-native-babel-preset": "0.73.5",
"prettier": "^2.8.3",
"eslint": "^7.32.0",
"jest": "^26.6.3",
"metro-react-native-babel-preset": "^0.67.0",
"react-native-dotenv": "^3.3.1",
"react-scripts": "^5.0.1",
"react-test-renderer": "18.2.0",
"react-test-renderer": "17.0.2",
"typescript": "^4.4.4"
},
"resolutions": {
@@ -105,11 +82,7 @@
"jest": {
"preset": "react-native",
"setupFiles": [
"./jest/jestSetup.js",
"./node_modules/react-native-gesture-handler/jestSetup.js"
],
"setupFilesAfterEnv": [
"@testing-library/jest-native/extend-expect"
"./jest.js"
],
"moduleFileExtensions": [
"ts",
@@ -121,21 +94,6 @@
],
"transformIgnorePatterns": [
"node_modules/(?!(jest-)?react-native|react-clone-referenced-element|@react-native-community|rollbar-react-native|@fortawesome|@react-native|@react-navigation)"
],
"modulePathIgnorePatterns": [
"__tests__/.*/__mocks__"
],
"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"
],
"reporters": [
"default",
"jest-junit"
]
},
"browserslist": {
Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

+8 -35
View File
@@ -5,34 +5,18 @@ 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 {
createClient,
SegmentClient,
AnalyticsProvider,
} from '@segment/analytics-react-native'
import {ThemeProvider} from './view/lib/ThemeContext'
import * as view from './view/index'
import {RootStoreModel, setupState, RootStoreProvider} from './state'
import {MobileShell} from './view/shell/mobile'
import {s} from './view/lib/styles'
import notifee, {EventType} from '@notifee/react-native'
const App = observer(() => {
function App() {
const [rootStore, setRootStore] = useState<RootStoreModel | undefined>(
undefined,
)
const [segment, setSegment] = useState<SegmentClient | undefined>(undefined)
// init
useEffect(() => {
view.setup()
setSegment(
createClient({
writeKey: '8I6DsgfiSLuoONyaunGoiQM7A6y2ybdI',
trackAppLifecycleEvents: true,
}),
)
setupState().then(store => {
setRootStore(store)
SplashScreen.hide()
@@ -44,13 +28,6 @@ const App = observer(() => {
Linking.addEventListener('url', ({url}) => {
store.nav.handleLink(url)
})
notifee.onForegroundEvent(async ({type}: {type: EventType}) => {
store.log.debug('Notifee foreground event', {type})
if (type === EventType.PRESS) {
store.log.debug('User pressed a notifee, opening notifications')
store.nav.switchTo(1, true)
}
})
})
}, [])
@@ -60,20 +37,16 @@ const App = observer(() => {
}
return (
<GestureHandlerRootView style={s.h100pct}>
<GestureHandlerRootView style={{flex: 1}}>
<RootSiblingParent>
<AnalyticsProvider client={segment}>
<RootStoreProvider value={rootStore}>
<ThemeProvider theme={rootStore.shell.darkMode ? 'dark' : 'light'}>
<SafeAreaProvider>
<MobileShell />
</SafeAreaProvider>
</ThemeProvider>
</RootStoreProvider>
</AnalyticsProvider>
<RootStoreProvider value={rootStore}>
<SafeAreaProvider>
<MobileShell />
</SafeAreaProvider>
</RootStoreProvider>
</RootSiblingParent>
</GestureHandlerRootView>
)
})
}
export default App
+1
View File
@@ -1,2 +1,3 @@
export const LOGIN_INCLUDE_DEV_SERVERS = true
export const TABS_ENABLED = false
export const IMAGES_ENABLED = false
+1 -1
View File
@@ -1,4 +1,4 @@
export function isNetworkError(e: unknown) {
const str = String(e)
return str.includes('Abort') || str.includes('Network request failed')
return str.includes('Aborted') || str.includes('Network request failed')
}
-99
View File
@@ -1,99 +0,0 @@
import {LikelyType, LinkMeta} from './link-meta'
import {match as matchRoute} from '../view/routes'
import {convertBskyAppUrlIfNeeded, makeRecordUri} from './strings'
import {RootStoreModel} from '../state'
import {PostThreadViewModel} from '../state/models/post-thread-view'
import {Home} from '../view/screens/Home'
import {Search} from '../view/screens/Search'
import {Notifications} from '../view/screens/Notifications'
import {PostThread} from '../view/screens/PostThread'
import {PostUpvotedBy} from '../view/screens/PostUpvotedBy'
import {PostRepostedBy} from '../view/screens/PostRepostedBy'
import {Profile} from '../view/screens/Profile'
import {ProfileFollowers} from '../view/screens/ProfileFollowers'
import {ProfileFollows} from '../view/screens/ProfileFollows'
// NOTE
// this is a hack around the lack of hosted social metadata
// remove once that's implemented
// -prf
export async function extractBskyMeta(
store: RootStoreModel,
url: string,
): Promise<LinkMeta> {
url = convertBskyAppUrlIfNeeded(url)
const route = matchRoute(url)
let meta: LinkMeta = {
likelyType: LikelyType.AtpData,
url,
title: route.defaultTitle,
}
if (route.Com === Home) {
meta = {
...meta,
title: 'Bluesky',
description: 'A new kind of social network',
}
} else if (route.Com === Search) {
meta = {
...meta,
title: 'Search - Bluesky',
description: 'A new kind of social network',
}
} else if (route.Com === Notifications) {
meta = {
...meta,
title: 'Notifications - Bluesky',
description: 'A new kind of social network',
}
} else if (
route.Com === PostThread ||
route.Com === PostUpvotedBy ||
route.Com === PostRepostedBy
) {
// post and post-related screens
const threadUri = makeRecordUri(
route.params.name,
'app.bsky.feed.post',
route.params.rkey,
)
const threadView = new PostThreadViewModel(store, {
uri: threadUri,
depth: 0,
})
await threadView.setup().catch(_err => undefined)
const title = [
route.Com === PostUpvotedBy
? 'Likes on a post by'
: route.Com === PostRepostedBy
? 'Reposts of a post by'
: 'Post by',
threadView.thread?.post.author.displayName ||
threadView.thread?.post.author.handle ||
'a bluesky user',
].join(' ')
meta = {
...meta,
title,
description: threadView.thread?.postRecord?.text,
}
} else if (
route.Com === Profile ||
route.Com === ProfileFollowers ||
route.Com === ProfileFollows
) {
// profile and profile-related screens
const profile = await store.profiles.getProfile(route.params.name)
if (profile?.data) {
meta = {
...meta,
title: profile.data.displayName || profile.data.handle,
description: profile.data.description,
}
}
}
return meta
}
-71
View File
@@ -1,71 +0,0 @@
import {extractTwitterMeta} from './extractTwitterMeta'
import {extractYoutubeMeta} from './extractYoutubeMeta'
interface ExtractHtmlMetaInput {
html: string
hostname?: string
pathname?: string
}
export const extractHtmlMeta = ({
html,
hostname,
pathname,
}: ExtractHtmlMetaInput): Record<string, string> => {
const htmlTitleRegex = /<title.*>([^<]+)<\/title>/i
let res: Record<string, string> = {}
const match = htmlTitleRegex.exec(html)
if (match) {
res.title = match[1].trim()
}
let metaMatch
let propMatch
const metaRe = /<meta[\s]([^>]+)>/gis
while ((metaMatch = metaRe.exec(html))) {
let propName
let propValue
const propRe = /(name|property|content)="([^"]+)"/gis
while ((propMatch = propRe.exec(metaMatch[1]))) {
if (propMatch[1] === 'content') {
propValue = propMatch[2]
} else {
propName = propMatch[2]
}
}
if (!propName || !propValue) {
continue
}
switch (propName?.trim()) {
case 'title':
case 'og:title':
case 'twitter:title':
res.title = propValue?.trim()
break
case 'description':
case 'og:description':
case 'twitter:description':
res.description = propValue?.trim()
break
case 'og:image':
case 'twitter:image':
res.image = propValue?.trim()
break
}
}
const isYoutubeUrl =
hostname?.includes('youtube.') || hostname?.includes('youtu.be')
const isTwitterUrl = hostname?.includes('twitter.')
// Workaround for some websites not having a title or description in the meta tags in the initial serve
if (isYoutubeUrl) {
res = {...res, ...extractYoutubeMeta(html)}
} else if (isTwitterUrl) {
res = {...extractTwitterMeta({pathname})}
}
return res
}
-20
View File
@@ -1,20 +0,0 @@
export const extractTwitterMeta = ({
pathname,
}: {
pathname: string
}): Record<string, string> => {
const res = {title: 'Twitter'}
const parsedPathname = pathname.split('/')
if (parsedPathname.length <= 1 || parsedPathname[1].length <= 1) {
// Excluding one letter usernames as they're reserved by twitter for things like cases like twitter.com/i/articles/follows/-1675653703
return res
}
const username = parsedPathname?.[1]
const isUserProfile = parsedPathname?.length === 2
res.title = isUserProfile
? `@${username} on Twitter`
: `Tweet by @${username}`
return res
}
-26
View File
@@ -1,26 +0,0 @@
export const extractYoutubeMeta = (html: string): Record<string, string> => {
const res: Record<string, string> = {}
const youtubeTitleRegex = /"videoDetails":.*"title":"([^"]*)"/i
const youtubeDescriptionRegex =
/"videoDetails":.*"shortDescription":"([^"]*)"/i
const youtubeThumbnailRegex = /"videoDetails":.*"url":"(.*)(default\.jpg)/i
const youtubeTitleMatch = youtubeTitleRegex.exec(html)
const youtubeDescriptionMatch = youtubeDescriptionRegex.exec(html)
const youtubeThumbnailMatch = youtubeThumbnailRegex.exec(html)
if (youtubeTitleMatch && youtubeTitleMatch.length >= 1) {
res.title = decodeURI(youtubeTitleMatch[1])
}
if (youtubeDescriptionMatch && youtubeDescriptionMatch.length >= 1) {
res.description = decodeURI(youtubeDescriptionMatch[1]).replace(
/\\n/g,
'\n',
)
}
if (youtubeThumbnailMatch && youtubeThumbnailMatch.length >= 2) {
res.image = youtubeThumbnailMatch[1] + 'default.jpg'
}
return res
}
-6
View File
@@ -1,6 +0,0 @@
export function choose<U, T extends Record<string, U>>(
value: keyof T,
choices: T,
): U {
return choices[value]
}
-130
View File
@@ -1,130 +0,0 @@
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 = 100 - 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,
})
}
export interface Dim {
width: number
height: number
}
export function scaleDownDimensions(dim: Dim, max: Dim): Dim {
if (dim.width < max.width && dim.height < max.height) {
return dim
}
let wScale = dim.width > max.width ? max.width / dim.width : 1
let hScale = dim.height > max.height ? max.height / dim.height : 1
if (wScale < hScale) {
return {width: dim.width * wScale, height: dim.height * wScale}
}
return {width: dim.width * hScale, height: dim.height * hScale}
}
+20 -25
View File
@@ -1,8 +1,10 @@
import he from 'he'
import {isBskyAppUrl} from './strings'
import {RootStoreModel} from '../state'
import {extractBskyMeta} from './extractBskyMeta'
import {extractHtmlMeta} from './extractHtmlMeta'
import {
extractHtmlMeta,
isBskyAppUrl,
convertBskyAppUrlIfNeeded,
} from './strings'
import {match as matchRoute} from '../view/routes'
export enum LikelyType {
HTML,
@@ -20,16 +22,19 @@ export interface LinkMeta {
url: string
title?: string
description?: string
image?: string
}
export async function getLinkMeta(
store: RootStoreModel,
url: string,
timeout = 5e3,
): Promise<LinkMeta> {
export async function getLinkMeta(url: string): Promise<LinkMeta> {
if (isBskyAppUrl(url)) {
return extractBskyMeta(store, url)
// TODO this could be better
url = convertBskyAppUrlIfNeeded(url)
const route = matchRoute(url)
return {
likelyType: LikelyType.AtpData,
url,
title: route.defaultTitle,
// description: ''
}
}
let urlp
@@ -52,27 +57,17 @@ export async function getLinkMeta(
}
try {
const controller = new AbortController()
const to = setTimeout(() => controller.abort(), timeout || 5e3)
const httpRes = await fetch(url, {
headers: {accept: 'text/html'},
signal: controller.signal,
})
const httpRes = await fetch(url)
const httpResBody = await httpRes.text()
clearTimeout(to)
const httpResMeta = extractHtmlMeta({
html: httpResBody,
hostname: urlp?.hostname,
pathname: urlp?.pathname,
})
const httpResMeta = extractHtmlMeta(httpResBody)
meta.title = httpResMeta.title ? he.decode(httpResMeta.title) : undefined
meta.description = httpResMeta.description
? he.decode(httpResMeta.description)
: undefined
meta.image = httpResMeta.image
// TODO meta.image = httpResMeta.image
} catch (e) {
// failed
console.error(e)
console.log(e)
meta.error = 'Failed to fetch link'
}
+86 -36
View File
@@ -1,6 +1,5 @@
import {AtUri} from '../third-party/uri'
import {AppBskyFeedPost} from '@atproto/api'
type Entity = AppBskyFeedPost.Entity
import {Entity} from '../third-party/api/src/client/types/app/bsky/feed/post'
import {PROD_SERVICE} from '../state'
import {isNetworkError} from './errors'
import TLDs from 'tlds'
@@ -23,7 +22,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
@@ -63,9 +62,7 @@ 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
})
}
@@ -78,25 +75,27 @@ export function extractEntities(
let ents: Entity[] = []
{
// mentions
const re = /(^|\s|\()(@)([a-zA-Z0-9.-]+)(\b)/g
const re = /(^|\s|\()(@)([a-zA-Z0-9\.-]+)(\b)/dg
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, end: start + match[3].length + 1},
index: {
start: match.indices[2][0], // skip the (^|\s) but include the '@'
end: match.indices[3][1],
},
})
}
}
{
// links
const re =
/(^|\s|\()((https?:\/\/[\S]+)|((?<domain>[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*))/gim
/(^|\s|\()((https?:\/\/[\S]+)|((?<domain>[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*))/dgm
while ((match = re.exec(text))) {
let value = match[2]
if (!value.startsWith('http')) {
@@ -106,16 +105,20 @@ export function extractEntities(
}
value = `https://${value}`
}
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--
const index = {
start: match.indices[2][0], // skip the (^|\s)
end: match.indices[2][1],
}
if (/[)]$/.test(value) && !value.includes('(')) {
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--
}
}
ents.push({
type: 'link',
@@ -133,7 +136,7 @@ interface DetectedLink {
type DetectedLinkable = string | DetectedLink
export function detectLinkables(text: string): DetectedLinkable[] {
const re =
/((^|\s|\()@[a-z0-9.-]*)|((^|\s|\()https?:\/\/[\S]+)|((^|\s|\()(?<domain>[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*)/gi
/((^|\s|\()@[a-z0-9\.-]*)|((^|\s|\()https?:\/\/[\S]+)|((^|\s|\()(?<domain>[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*)/gi
const segments = []
let match
let start = 0
@@ -154,12 +157,14 @@ export function detectLinkables(text: string): DetectedLinkable[] {
matchValue = matchValue.slice(1)
}
// strip ending puncuation
if (/[.,;!?]$/.test(matchValue)) {
matchValue = matchValue.slice(0, -1)
}
if (/[)]$/.test(matchValue) && !matchValue.includes('(')) {
matchValue = matchValue.slice(0, -1)
{
// strip ending puncuation
if (/[.,;!?]$/.test(matchValue)) {
matchValue = matchValue.slice(0, -1)
}
if (/[)]$/.test(matchValue) && !matchValue.includes('(')) {
matchValue = matchValue.slice(0, -1)
}
}
if (start !== matchIndex) {
@@ -183,8 +188,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}`
}
@@ -196,13 +201,7 @@ export function enforceLen(str: string, len: number): string {
return str
}
export function cleanError(str: any): string {
if (!str) {
return str
}
if (typeof str !== 'string') {
str = str.toString()
}
export function cleanError(str: string): string {
if (isNetworkError(str)) {
return 'Unable to connect. Please check your internet connection and try again.'
}
@@ -260,8 +259,59 @@ export function convertBskyAppUrlIfNeeded(url: string): string {
const urlp = new URL(url)
return urlp.pathname
} catch (e) {
console.error('Unexpected error in convertBskyAppUrlIfNeeded()', e)
console.log('Unexpected error in convertBskyAppUrlIfNeeded()', e)
}
}
return url
}
const htmlTitleRegex = /<title>([^<]+)<\/title>/i
export function extractHtmlMeta(html: string): Record<string, string> {
const res: Record<string, string> = {}
{
const match = htmlTitleRegex.exec(html)
if (match) {
res.title = match[1].trim()
}
}
{
let metaMatch
let propMatch
const metaRe = /<meta[\s]([^>]+)>/gis
while ((metaMatch = metaRe.exec(html))) {
let propName
let propValue
const propRe = /(name|property|content)="([^"]+)"/gis
while ((propMatch = propRe.exec(metaMatch[1]))) {
if (propMatch[1] === 'content') {
propValue = propMatch[2]
} else {
propName = propMatch[2]
}
}
if (!propName || !propValue) {
continue
}
switch (propName?.trim()) {
case 'title':
case 'og:title':
case 'twitter:title':
res.title = propValue?.trim()
break
case 'description':
case 'og:description':
case 'twitter:description':
res.description = propValue?.trim()
break
case 'og:image':
case 'twitter:image':
res.image = propValue?.trim()
break
}
}
}
return res
}
+11 -12
View File
@@ -1,42 +1,41 @@
import {autorun} from 'mobx'
import {Platform} from 'react-native'
import {sessionClient as AtpApi, SessionServiceClient} from '@atproto/api'
import {sessionClient as AtpApi} from '../third-party/api'
import type {SessionServiceClient} from '../third-party/api/src/index'
import {RootStoreModel} from './models/root-store'
import * as libapi from './lib/api'
import * as storage from './lib/storage'
export const LOCAL_DEV_SERVICE =
Platform.OS === 'ios' ? 'http://localhost:2583' : 'http://10.0.2.2:2583'
export const LOCAL_DEV_SERVICE = 'http://localhost:2583'
export const STAGING_SERVICE = 'https://pds.staging.bsky.dev'
export const PROD_SERVICE = 'https://bsky.social'
export const DEFAULT_SERVICE = PROD_SERVICE
const ROOT_STATE_STORAGE_KEY = 'root'
const STATE_FETCH_INTERVAL = 15e3
export async function setupState(serviceUri = DEFAULT_SERVICE) {
export async function setupState() {
let rootStore: RootStoreModel
let data: any
libapi.doPolyfill()
const api = AtpApi.service(serviceUri) as SessionServiceClient
const api = AtpApi.service(DEFAULT_SERVICE) as SessionServiceClient
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: any) {
rootStore.log.error('Failed to load state from storage', e)
} catch (e) {
console.error('Failed to load state from storage', e)
}
console.log('Initial hydrate', rootStore.me)
rootStore.session
.connect()
.then(() => {
rootStore.log.debug('Session connected')
console.log('Session connected', rootStore.me)
return rootStore.fetchStateUpdate()
})
.catch((e: any) => {
rootStore.log.warn('Failed initial connect', e)
.catch(e => {
console.log('Failed initial connect', e)
})
// @ts-ignore .on() is correct -prf
api.sessionManager.on('session', () => {
+70 -127
View File
@@ -4,18 +4,13 @@
*/
// import {ReactNativeStore} from './auth'
import {
sessionClient as AtpApi,
AppBskyEmbedImages,
AppBskyEmbedExternal,
} from '@atproto/api'
import RNFS from 'react-native-fs'
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 {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 {LinkMeta} from '../../lib/link-meta'
import {Image} from '../../lib/images'
const TIMEOUT = 10e3 // 10s
@@ -23,100 +18,15 @@ export function doPolyfill() {
AtpApi.xrpc.fetch = fetchHandler
}
export interface ExternalEmbedDraft {
uri: string
isLoading: boolean
meta?: LinkMeta
localThumb?: Image
}
export async function post(
store: RootStoreModel,
text: string,
replyTo?: string,
extLink?: ExternalEmbedDraft,
images?: string[],
replyTo?: Post.ReplyRef,
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 && extLink) {
let thumb
if (extLink.localThumb) {
onStateChange?.('Uploading link thumbnail...')
let encoding
if (extLink.localThumb.path.endsWith('.png')) {
encoding = 'image/png'
} else if (
extLink.localThumb.path.endsWith('.jpeg') ||
extLink.localThumb.path.endsWith('.jpg')
) {
encoding = 'image/jpeg'
} else {
store.log.warn(
'Unexpected image format for thumbnail, skipping',
extLink.localThumb.path,
)
}
if (encoding) {
const thumbUploadRes = await store.api.com.atproto.blob.upload(
extLink.localThumb.path, // 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: extLink.uri,
title: extLink.meta?.title || '',
description: extLink.meta?.description || '',
thumb,
},
} as AppBskyEmbedExternal.Main
}
if (replyTo) {
const replyToUrip = new AtUri(replyTo)
const replyToUrip = new AtUri(replyTo.uri)
const parentPost = await store.api.app.bsky.feed.post.get({
user: replyToUrip.host,
rkey: replyToUrip.rkey,
@@ -132,29 +42,24 @@ export async function post(
}
}
}
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
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
}
}
}
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) {
@@ -200,6 +105,54 @@ 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>
@@ -219,16 +172,6 @@ async function fetchHandler(
typeof reqBody === 'string' &&
(reqBody.startsWith('/') || reqBody.startsWith('file:'))
) {
if (reqBody.endsWith('.jpeg') || reqBody.endsWith('.jpg')) {
// HACK
// React native has a bug that inflates the size of jpegs on upload
// we get around that by renaming the file ext to .bin
// see https://github.com/facebook/react-native/issues/27099
// -prf
const newPath = reqBody.replace(/\.jpe?g$/, '.bin')
await RNFS.moveFile(reqBody, newPath)
reqBody = newPath
}
// NOTE
// React native treats bodies with {uri: string} as file uploads to pull from cache
// -prf
+196 -271
View File
@@ -1,172 +1,185 @@
import {makeAutoObservable, runInAction} from 'mobx'
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 {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 {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
let _idCounter = 0
type FeedViewPostWithThreadMeta = FeedViewPost & {
type FeedItem = GetTimeline.FeedItem | GetAuthorFeed.FeedItem
type FeedItemWithThreadMeta = FeedItem & {
_isThreadParent?: boolean
_isThreadChildElided?: boolean
_isThreadChild?: boolean
}
export class FeedItemModel {
export class FeedItemMyStateModel {
repost?: string
upvote?: string
downvote?: string
constructor() {
makeAutoObservable(this)
}
}
export class FeedItemModel implements GetTimeline.FeedItem {
// 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
post: PostView
postRecord?: AppBskyFeedPost.Record
reply?: FeedViewPost['reply']
replyParent?: FeedItemModel
reason?: FeedViewPost['reason']
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
constructor(
public rootStore: RootStoreModel,
reactKey: string,
v: FeedViewPostWithThreadMeta,
v: FeedItemWithThreadMeta,
) {
makeAutoObservable(this, {rootStore: false})
this._reactKey = reactKey
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.copy(v)
this._isThreadParent = v._isThreadParent || false
this._isThreadChild = v._isThreadChild || false
this._isThreadChildElided = v._isThreadChildElided || false
makeAutoObservable(this, {rootStore: false})
}
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
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
}
}
async toggleUpvote() {
const wasUpvoted = !!this.post.viewer.upvote
const wasDownvoted = !!this.post.viewer.downvote
const wasUpvoted = !!this.myState.upvote
const wasDownvoted = !!this.myState.downvote
const res = await this.rootStore.api.app.bsky.feed.setVote({
subject: {
uri: this.post.uri,
cid: this.post.cid,
uri: this.uri,
cid: this.cid,
},
direction: wasUpvoted ? 'none' : 'up',
})
runInAction(() => {
if (wasDownvoted) {
this.post.downvoteCount--
this.downvoteCount--
}
if (wasUpvoted) {
this.post.upvoteCount--
this.upvoteCount--
} else {
this.post.upvoteCount++
this.upvoteCount++
}
this.post.viewer.upvote = res.data.upvote
this.post.viewer.downvote = res.data.downvote
this.myState.upvote = res.data.upvote
this.myState.downvote = res.data.downvote
})
}
async toggleDownvote() {
const wasUpvoted = !!this.post.viewer.upvote
const wasDownvoted = !!this.post.viewer.downvote
const wasUpvoted = !!this.myState.upvote
const wasDownvoted = !!this.myState.downvote
const res = await this.rootStore.api.app.bsky.feed.setVote({
subject: {
uri: this.post.uri,
cid: this.post.cid,
uri: this.uri,
cid: this.cid,
},
direction: wasDownvoted ? 'none' : 'down',
})
runInAction(() => {
if (wasUpvoted) {
this.post.upvoteCount--
this.upvoteCount--
}
if (wasDownvoted) {
this.post.downvoteCount--
this.downvoteCount--
} else {
this.post.downvoteCount++
this.downvoteCount++
}
this.post.viewer.upvote = res.data.upvote
this.post.viewer.downvote = res.data.downvote
this.myState.upvote = res.data.upvote
this.myState.downvote = res.data.downvote
})
}
async toggleRepost() {
if (this.post.viewer.repost) {
await apilib.unrepost(this.rootStore, this.post.viewer.repost)
if (this.myState.repost) {
await apilib.unrepost(this.rootStore, this.myState.repost)
runInAction(() => {
this.post.repostCount--
this.post.viewer.repost = undefined
this.repostCount--
this.myState.repost = undefined
})
} else {
const res = await apilib.repost(
this.rootStore,
this.post.uri,
this.post.cid,
)
const res = await apilib.repost(this.rootStore, this.uri, this.cid)
runInAction(() => {
this.post.repostCount++
this.post.viewer.repost = res.uri
this.repostCount++
this.myState.repost = res.uri
})
}
}
async delete() {
await this.rootStore.api.app.bsky.feed.post.delete({
did: this.post.author.did,
rkey: new AtUri(this.post.uri).rkey,
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)
})
this.rootStore.emitPostDeleted(this.post.uri)
}
}
@@ -224,11 +237,12 @@ export class FeedModel {
get nonReplyFeed() {
return this.feed.filter(
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),
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,
)
}
@@ -256,19 +270,11 @@ export class FeedModel {
this._loadPromise = undefined
}
/**
* Register any event listeners. Returns a cleanup function.
*/
registerListeners() {
const sub = this.rootStore.onPostDeleted(this.onPostDeleted.bind(this))
return () => sub.remove()
}
/**
* Reset and load
*/
async refresh() {
await this.setup(true)
return this.setup(true)
}
/**
@@ -312,7 +318,7 @@ export class FeedModel {
}
/**
* Check if new posts are available
* Check if new postrs are available
*/
async checkForLatest() {
if (this.hasNewLatest) {
@@ -320,28 +326,10 @@ export class FeedModel {
}
await this._pendingWork()
const res = await this._getFeed({limit: 1})
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(
res.data.feed[0] &&
(this.feed.length === 0 || res.data.feed[0].uri !== this.pollCursor),
)
this.setHasNewLatest(hasNewLatest)
}
/**
* Removes posts from the feed upon deletion.
*/
onPostDeleted(uri: string) {
let i
do {
i = this.feed.findIndex(item => item.post.uri === uri)
if (i !== -1) {
this.feed.splice(i, 1)
}
} while (i !== -1)
}
// state transitions
@@ -353,14 +341,11 @@ export class FeedModel {
this.error = ''
}
private _xIdle(err?: any) {
private _xIdle(err: string = '') {
this.isLoading = false
this.isRefreshing = false
this.hasLoaded = true
this.error = err ? cleanError(err.toString()) : ''
if (err) {
this.rootStore.log.error('Posts feed request failed', err)
}
this.error = cleanError(err)
}
// loader functions
@@ -388,7 +373,7 @@ export class FeedModel {
await this._replaceAll(res)
this._xIdle()
} catch (e: any) {
this._xIdle(e)
this._xIdle(e.toString())
}
}
@@ -399,12 +384,12 @@ export class FeedModel {
await this._prependAll(res)
this._xIdle()
} catch (e: any) {
this._xIdle(e)
this._xIdle(e.toString())
}
}
private async _loadMore() {
if (!this.hasMore || this.hasError) {
if (!this.hasMore) {
return
}
this._xLoading()
@@ -416,7 +401,7 @@ export class FeedModel {
await this._appendAll(res)
this._xIdle()
} catch (e: any) {
this._xIdle(e)
this._xIdle(`Failed to load feed: ${e.toString()}`)
}
}
@@ -426,7 +411,7 @@ export class FeedModel {
}
this._xLoading()
let numToFetch = this.feed.length
let cursor
let cursor = undefined
try {
do {
const res: GetTimeline.Response = await this._getFeed({
@@ -438,18 +423,19 @@ export class FeedModel {
}
this._updateAll(res)
numToFetch -= res.data.feed.length
cursor = res.data.cursor
} while (cursor && numToFetch > 0)
cursor = this.feed[res.data.feed.length - 1].indexedAt
console.log(numToFetch, cursor, res.data.feed.length)
} while (numToFetch > 0)
this._xIdle()
} catch (e: any) {
this._xIdle(e)
this._xIdle(`Failed to update feed: ${e.toString()}`)
}
}
private async _replaceAll(
res: GetTimeline.Response | GetAuthorFeed.Response,
) {
this.pollCursor = res.data.feed[0]?.post.uri
this.pollCursor = res.data.feed[0]?.uri
return this._appendAll(res, true)
}
@@ -459,48 +445,73 @@ export class FeedModel {
) {
this.loadMoreCursor = res.data.cursor
this.hasMore = !!this.loadMoreCursor
const orgLen = this.feed.length
let counter = this.feed.length
const reorgedFeed = preprocessFeed(res.data.feed)
// 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 promises = []
const toAppend: FeedItemModel[] = []
for (const item of reorgedFeed) {
const itemModel = new FeedItemModel(
this.rootStore,
`item-${_idCounter++}`,
`item-${counter++}`,
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)
}
dedupReposts(this.feed)
dedupParents(this.feed.slice(orgLen)) // we slice to avoid modifying rendering of already-shown posts
})
}
private async _prependAll(
res: GetTimeline.Response | GetAuthorFeed.Response,
) {
this.pollCursor = res.data.feed[0]?.post.uri
this.pollCursor = res.data.feed[0]?.uri
let counter = this.feed.length
const promises = []
const toPrepend: FeedItemModel[] = []
for (const item of res.data.feed) {
if (this.feed.find(item2 => item2.post.uri === item.post.uri)) {
if (this.feed.find(item2 => item2.uri === item.uri)) {
break // stop here - we've hit a post we already have
}
const itemModel = new FeedItemModel(
this.rootStore,
`item-${_idCounter++}`,
`item-${counter++}`,
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)
})
@@ -509,11 +520,10 @@ export class FeedModel {
private _updateAll(res: GetTimeline.Response | GetAuthorFeed.Response) {
for (const item of res.data.feed) {
const existingItem = this.feed.find(
// HACK: need to find the reposts' 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,
// 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,
)
if (existingItem) {
existingItem.copy(item)
@@ -537,23 +547,23 @@ export class FeedModel {
}
}
interface Slice {
index: number
length: number
}
function preprocessFeed(feed: FeedViewPost[]): FeedViewPostWithThreadMeta[] {
const reorg: FeedViewPostWithThreadMeta[] = []
// phase one: identify threads and reorganize them into the feed so
// that they are in order and marked as part of a thread
function preprocessFeed(
feed: FeedItem[],
dedup: boolean,
): FeedItemWithThreadMeta[] {
const reorg: FeedItemWithThreadMeta[] = []
for (let i = feed.length - 1; i >= 0; i--) {
const item = feed[i] as FeedViewPostWithThreadMeta
const item = feed[i] as FeedItemWithThreadMeta
if (dedup) {
if (reorg.find(item2 => item2.uri === item.uri)) {
continue
}
}
const selfReplyUri = getSelfReplyUri(item)
if (selfReplyUri) {
const parentIndex = reorg.findIndex(
item2 => item2.post.uri === selfReplyUri,
)
const parentIndex = reorg.findIndex(item2 => item2.uri === selfReplyUri)
if (parentIndex !== -1 && !reorg[parentIndex]._isThreadParent) {
reorg[parentIndex]._isThreadParent = true
item._isThreadChild = true
@@ -563,108 +573,23 @@ function preprocessFeed(feed: FeedViewPost[]): FeedViewPostWithThreadMeta[] {
}
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
}
// WARNING: mutates `feed`
function dedupReposts(feed: FeedItemModel[]) {
// remove duplicates caused by reposts
for (let i = 0; i < feed.length; i++) {
const item1 = feed[i]
for (let j = i + 1; j < feed.length; j++) {
const item2 = feed[j]
if (item1.post.uri === item2.post.uri) {
feed.splice(j, 1)
j--
}
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
}
}
}
// WARNING: mutates `feed`
function dedupParents(feed: FeedItemModel[]) {
// only show parents that aren't already in the feed
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
}
+6 -8
View File
@@ -1,8 +1,9 @@
import {makeAutoObservable} from 'mobx'
import {AppBskyGraphGetAssertions as GetAssertions} from '@atproto/api'
import * as GetAssertions from '../../third-party/api/src/client/types/app/bsky/graph/getAssertions'
import {RootStoreModel} from './root-store'
export type Assertion = GetAssertions.Assertion & {
type ResponseAssertion = GetAssertions.OutputSchema['assertions'][number]
export type Assertion = ResponseAssertion & {
_reactKey: string
}
@@ -80,14 +81,11 @@ export class GetAssertionsView {
this.error = ''
}
private _xIdle(err?: any) {
private _xIdle(err: string = '') {
this.isLoading = false
this.isRefreshing = false
this.hasLoaded = true
this.error = err ? err.toString() : ''
if (err) {
this.rootStore.log.error('Failed to fetch assertions', err)
}
this.error = err
}
// loader functions
@@ -102,7 +100,7 @@ export class GetAssertionsView {
this._replaceAll(res)
this._xIdle()
} catch (e: any) {
this._xIdle(e)
this._xIdle(e.toString())
}
}
+1 -1
View File
@@ -31,7 +31,7 @@ export class LinkMetasViewModel {
}
}
try {
const promise = getLinkMeta(this.rootStore, url)
const promise = getLinkMeta(url)
this.cache.set(url, promise)
const res = await promise
this.cache.set(url, res)
-111
View File
@@ -1,111 +0,0 @@
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
}
+17 -34
View File
@@ -1,7 +1,6 @@
import {makeAutoObservable, runInAction} from 'mobx'
import notifee from '@notifee/react-native'
import {RootStoreModel} from './root-store'
import {FeedModel} from './feed-view'
import {MembershipsViewModel} from './memberships-view'
import {NotificationsViewModel} from './notifications-view'
import {isObj, hasProp} from '../lib/type-guards'
@@ -12,7 +11,7 @@ export class MeModel {
description: string = ''
avatar: string = ''
notificationCount: number = 0
mainFeed: FeedModel
memberships?: MembershipsViewModel
notifications: NotificationsViewModel
constructor(public rootStore: RootStoreModel) {
@@ -21,9 +20,6 @@ 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, {})
}
@@ -34,6 +30,7 @@ export class MeModel {
this.description = ''
this.avatar = ''
this.notificationCount = 0
this.memberships = undefined
}
serialize(): unknown {
@@ -93,21 +90,15 @@ export class MeModel {
this.avatar = ''
}
})
this.mainFeed = new FeedModel(this.rootStore, 'home', {
algorithm: 'reverse-chronological',
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.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)
}),
])
// request notifications permission once the user has logged in
notifee.requestPermission()
} else {
this.clear()
}
@@ -115,29 +106,21 @@ export class MeModel {
clearNotificationCount() {
this.notificationCount = 0
notifee.setBadgeCount(0)
}
async fetchNotifications() {
async fetchStateUpdate() {
const res = await this.rootStore.api.app.bsky.notification.getCount()
runInAction(() => {
const newNotifications = this.notificationCount !== res.data.count
this.notificationCount = res.data.count
notifee.setBadgeCount(this.notificationCount)
if (newNotifications) {
// trigger pre-emptive fetch on new notifications
let oldMostRecent = this.notifications.mostRecentNotification
this.notifications.refresh().then(() => {
// if a new most recent notification is found, trigger a notification card
const mostRecent = this.notifications.mostRecentNotification
if (mostRecent && oldMostRecent?.uri !== mostRecent?.uri) {
const notifeeOpts = mostRecent.toNotifeeOpts()
if (notifeeOpts) {
notifee.displayNotification(notifeeOpts)
}
}
})
this.notifications.refresh()
}
})
}
async refreshMemberships() {
return this.memberships?.refresh()
}
}
+144
View File
@@ -0,0 +1,144 @@
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)
}
}

Some files were not shown because too many files have changed in this diff Show More