diff options
133 files changed, 3068 insertions, 2804 deletions
diff --git a/.detoxrc.js b/.detoxrc.js index fc9cf042b..1fe578e76 100644 --- a/.detoxrc.js +++ b/.detoxrc.js @@ -3,7 +3,7 @@ module.exports = { testRunner: { args: { $0: 'jest', - config: 'e2e/jest.config.js', + config: '__e2e__/jest.config.js', }, jest: { setupTimeout: 120000, @@ -12,15 +12,16 @@ module.exports = { apps: { 'ios.debug': { type: 'ios.app', - binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/app.app', + binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/bluesky.app', build: - 'xcodebuild -workspace ios/app.xcworkspace -scheme app -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build', + 'xcodebuild -workspace ios/bluesky.xcworkspace -scheme bluesky -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build', }, 'ios.release': { type: 'ios.app', - binaryPath: 'ios/build/Build/Products/Release-iphonesimulator/app.app', + binaryPath: + 'ios/build/Build/Products/Release-iphonesimulator/bluesky.app', build: - 'xcodebuild -workspace ios/app.xcworkspace -scheme app -configuration Release -sdk iphonesimulator -derivedDataPath ios/build', + 'xcodebuild -workspace ios/bluesky.xcworkspace -scheme bluesky -configuration Release -sdk iphonesimulator -derivedDataPath ios/build', }, 'android.debug': { type: 'android.apk', diff --git a/README.md b/README.md index 8ae03e70c..c6c72c03d 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,12 @@ - iOS: `yarn ios` - Android: `yarn android` - Web: `yarn web` +- Run e2e tests + - Start in various console tabs: + - `yarn e2e:server` + - `yarn e2e:metro` + - Run once: `yarn e2e:build` + - Each test run: `yarn e2e:run` - Tips - `npx react-native info` Checks what has been installed. - On M1 macs, [you need to exclude "arm64" from the target architectures](https://stackoverflow.com/a/65399525) diff --git a/e2e/jest.config.js b/__e2e__/jest.config.js index 3472f7161..80c2ad5b3 100644 --- a/e2e/jest.config.js +++ b/__e2e__/jest.config.js @@ -1,7 +1,7 @@ /** @type {import('@jest/types').Config.InitialOptions} */ module.exports = { rootDir: '..', - testMatch: ['<rootDir>/e2e/**/*.test.js'], + testMatch: ['<rootDir>/__e2e__/**/*.test.ts'], testTimeout: 120000, maxWorkers: 1, globalSetup: 'detox/runners/jest/globalSetup', diff --git a/__e2e__/mock-server.ts b/__e2e__/mock-server.ts new file mode 100644 index 000000000..7a2be6060 --- /dev/null +++ b/__e2e__/mock-server.ts @@ -0,0 +1,75 @@ +import {createServer as createHTTPServer} from 'node:http' +import {parse} from 'node:url' +import {createServer, TestPDS} from '../jest/test-pds' + +async function main() { + let server: TestPDS + createHTTPServer(async (req, res) => { + const url = parse(req.url || '/', true) + if (req.method !== 'POST') { + return res.writeHead(200).end() + } + try { + console.log('Closing old server') + await server?.close() + console.log('Starting new server') + server = await createServer() + console.log('Listening at', server.pdsUrl) + if (url?.query) { + if ('users' in url.query) { + console.log('Generating mock users') + await server.mocker.createUser('alice') + await server.mocker.createUser('bob') + await server.mocker.createUser('carla') + await server.mocker.users.alice.agent.upsertProfile(() => ({ + displayName: 'Alice', + description: 'Test user 1', + })) + await server.mocker.users.bob.agent.upsertProfile(() => ({ + displayName: 'Bob', + description: 'Test user 2', + })) + await server.mocker.users.carla.agent.upsertProfile(() => ({ + displayName: 'Carla', + description: 'Test user 3', + })) + } + if ('follows' in url.query) { + console.log('Generating mock follows') + await server.mocker.follow('alice', 'bob') + await server.mocker.follow('alice', 'carla') + await server.mocker.follow('bob', 'alice') + await server.mocker.follow('bob', 'carla') + await server.mocker.follow('carla', 'alice') + await server.mocker.follow('carla', 'bob') + } + if ('posts' in url.query) { + console.log('Generating mock posts') + for (let user in server.mocker.users) { + await server.mocker.users[user].agent.post({text: 'Post'}) + } + } + if ('thread' in url.query) { + console.log('Generating mock posts') + const res = await server.mocker.users.bob.agent.post({ + text: 'Thread root', + }) + await server.mocker.users.carla.agent.post({ + text: 'Thread reply', + reply: { + parent: {cid: res.cid, uri: res.uri}, + root: {cid: res.cid, uri: res.uri}, + }, + }) + } + } + console.log('Ready') + return res.writeHead(200).end(server.pdsUrl) + } catch (e) { + console.error('Error!', e) + return res.writeHead(500).end() + } + }).listen(1986) + console.log('Mock server manager listening on 1986') +} +main() diff --git a/__e2e__/tests/composer.test.ts b/__e2e__/tests/composer.test.ts new file mode 100644 index 000000000..afc23cc13 --- /dev/null +++ b/__e2e__/tests/composer.test.ts @@ -0,0 +1,108 @@ +/* eslint-env detox/detox */ + +import {openApp, login, createServer, sleep} from '../util' + +describe('Composer', () => { + let service: string + beforeAll(async () => { + service = await createServer('?users') + await openApp({ + permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'}, + }) + }) + + it('Login', async () => { + await login(service, 'alice', 'hunter2') + await element(by.id('homeScreenFeedTabs-Following')).tap() + }) + + it('Post text only', async () => { + await element(by.id('composeFAB')).tap() + await device.takeScreenshot('1- opened composer') + await element(by.id('composerTextInput')).typeText('Post text only') + await device.takeScreenshot('2- entered text') + await element(by.id('composerPublishBtn')).tap() + await device.takeScreenshot('3- opened general section') + await expect(element(by.id('composeFAB'))).toBeVisible() + }) + + it('Post with an image', async () => { + await element(by.id('composeFAB')).tap() + await element(by.id('composerTextInput')).typeText('Post with an image') + await element(by.id('openGalleryBtn')).tap() + await sleep(1e3) + await element(by.id('composerPublishBtn')).tap() + await expect(element(by.id('composeFAB'))).toBeVisible() + }) + + it('Post with a link card', async () => { + await element(by.id('composeFAB')).tap() + await element(by.id('composerTextInput')).typeText( + 'Post with a https://example.com link card', + ) + await element(by.id('addLinkCardBtn')).tap() + await element(by.id('composerPublishBtn')).tap() + await expect(element(by.id('composeFAB'))).toBeVisible() + }) + + it('Reply text only', async () => { + const post = by.id('feedItem-by-alice.test') + await element(by.id('replyBtn').withAncestor(post)).atIndex(0).tap() + await element(by.id('composerTextInput')).typeText('Reply text only') + await element(by.id('composerPublishBtn')).tap() + await expect(element(by.id('composeFAB'))).toBeVisible() + }) + + it('Reply with an image', async () => { + const post = by.id('feedItem-by-alice.test') + await element(by.id('replyBtn').withAncestor(post)).atIndex(0).tap() + await element(by.id('composerTextInput')).typeText('Reply with an image') + await element(by.id('openGalleryBtn')).tap() + await sleep(1e3) + await element(by.id('composerPublishBtn')).tap() + await expect(element(by.id('composeFAB'))).toBeVisible() + }) + + it('Reply with a link card', async () => { + const post = by.id('feedItem-by-alice.test') + await element(by.id('replyBtn').withAncestor(post)).atIndex(0).tap() + await element(by.id('composerTextInput')).typeText( + 'Reply with a https://example.com link card', + ) + await element(by.id('addLinkCardBtn')).tap() + await element(by.id('composerPublishBtn')).tap() + await expect(element(by.id('composeFAB'))).toBeVisible() + }) + + it('QP text only', async () => { + const post = by.id('feedItem-by-alice.test') + await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap() + await element(by.id('quoteBtn').withAncestor(by.id('repostModal'))).tap() + await element(by.id('composerTextInput')).typeText('QP text only') + await element(by.id('composerPublishBtn')).tap() + await expect(element(by.id('composeFAB'))).toBeVisible() + }) + + it('QP with an image', async () => { + const post = by.id('feedItem-by-alice.test') + await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap() + await element(by.id('quoteBtn').withAncestor(by.id('repostModal'))).tap() + await element(by.id('composerTextInput')).typeText('QP with an image') + await element(by.id('openGalleryBtn')).tap() + await sleep(1e3) + await element(by.id('composerPublishBtn')).tap() + await expect(element(by.id('composeFAB'))).toBeVisible() + }) + + it('QP with a link card', async () => { + const post = by.id('feedItem-by-alice.test') + await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap() + await element(by.id('quoteBtn').withAncestor(by.id('repostModal'))).tap() + await element(by.id('composerTextInput')).typeText( + 'QP with a https://example.com link card', + ) + await element(by.id('addLinkCardBtn')).tap() + await element(by.id('composerPublishBtn')).tap() + await expect(element(by.id('composeFAB'))).toBeVisible() + }) +}) diff --git a/__e2e__/tests/create-account.test.ts b/__e2e__/tests/create-account.test.ts new file mode 100644 index 000000000..7b2e00fb5 --- /dev/null +++ b/__e2e__/tests/create-account.test.ts @@ -0,0 +1,31 @@ +/* eslint-env detox/detox */ + +import {openApp, createServer} from '../util' + +describe('Create account', () => { + let service: string + beforeAll(async () => { + service = await createServer('mock0') + await openApp({permissions: {notifications: 'YES'}}) + }) + + it('I can create a new account', async () => { + await element(by.id('createAccountButton')).tap() + await device.takeScreenshot('1- opened create account screen') + await element(by.id('otherServerBtn')).tap() + await device.takeScreenshot('2- selected other server') + await element(by.id('customServerInput')).clearText() + await element(by.id('customServerInput')).typeText(service) + await device.takeScreenshot('3- input test server URL') + await element(by.id('nextBtn')).tap() + await element(by.id('emailInput')).typeText('example@test.com') + await element(by.id('passwordInput')).typeText('hunter2') + await element(by.id('is13Input')).tap() + await device.takeScreenshot('4- entered account details') + await element(by.id('nextBtn')).tap() + await element(by.id('handleInput')).typeText('e2e-test') + await device.takeScreenshot('4- entered handle') + await element(by.id('nextBtn')).tap() + await expect(element(by.id('homeScreen'))).toBeVisible() + }) +}) diff --git a/__e2e__/tests/home-screen.test.ts b/__e2e__/tests/home-screen.test.ts new file mode 100644 index 000000000..1ec1774f3 --- /dev/null +++ b/__e2e__/tests/home-screen.test.ts @@ -0,0 +1,92 @@ +/* eslint-env detox/detox */ + +import {openApp, login, createServer} from '../util' + +describe('Home screen', () => { + let service: string + beforeAll(async () => { + service = await createServer('?users&follows&posts') + await openApp({permissions: {notifications: 'YES'}}) + }) + + it('Login', async () => { + await login(service, 'alice', 'hunter2') + await element(by.id('homeScreenFeedTabs-Following')).tap() + }) + + it('Can like posts', async () => { + const carlaPosts = by.id('feedItem-by-carla.test') + await expect( + element(by.id('likeCount').withAncestor(carlaPosts)).atIndex(0), + ).toHaveText('0') + await element(by.id('likeBtn').withAncestor(carlaPosts)).atIndex(0).tap() + await expect( + element(by.id('likeCount').withAncestor(carlaPosts)).atIndex(0), + ).toHaveText('1') + await element(by.id('likeBtn').withAncestor(carlaPosts)).atIndex(0).tap() + await expect( + element(by.id('likeCount').withAncestor(carlaPosts)).atIndex(0), + ).toHaveText('0') + }) + + it('Can repost posts', async () => { + const carlaPosts = by.id('feedItem-by-carla.test') + await expect( + element(by.id('repostCount').withAncestor(carlaPosts)).atIndex(0), + ).toHaveText('0') + await element(by.id('repostBtn').withAncestor(carlaPosts)).atIndex(0).tap() + await expect(element(by.id('repostModal'))).toBeVisible() + await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap() + await expect(element(by.id('repostModal'))).not.toBeVisible() + await expect( + element(by.id('repostCount').withAncestor(carlaPosts)).atIndex(0), + ).toHaveText('1') + await element(by.id('repostBtn').withAncestor(carlaPosts)).atIndex(0).tap() + await expect(element(by.id('repostModal'))).toBeVisible() + await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap() + await expect(element(by.id('repostModal'))).not.toBeVisible() + await expect( + element(by.id('repostCount').withAncestor(carlaPosts)).atIndex(0), + ).toHaveText('0') + }) + + it('Can report posts', async () => { + const carlaPosts = by.id('feedItem-by-carla.test') + await element(by.id('postDropdownBtn').withAncestor(carlaPosts)) + .atIndex(0) + .tap() + await element(by.id('postDropdownReportBtn')).tap() + await expect(element(by.id('reportPostModal'))).toBeVisible() + await element(by.id('reportPostRadios-spam')).tap() + await element(by.id('sendReportBtn')).tap() + await expect(element(by.id('reportPostModal'))).not.toBeVisible() + }) + + it('Can swipe between feeds', async () => { + await element(by.id('homeScreen')).swipe('left', 'fast', 0.75) + await expect(element(by.id('whatshotFeedPage'))).toBeVisible() + await element(by.id('homeScreen')).swipe('right', 'fast', 0.75) + await expect(element(by.id('followingFeedPage'))).toBeVisible() + }) + + it('Can tap between feeds', async () => { + await element(by.id("homeScreenFeedTabs-What's hot")).tap() + await expect(element(by.id('whatshotFeedPage'))).toBeVisible() + await element(by.id('homeScreenFeedTabs-Following')).tap() + await expect(element(by.id('followingFeedPage'))).toBeVisible() + }) + + it('Can delete posts', async () => { + const alicePosts = by.id('feedItem-by-alice.test') + await expect(element(alicePosts.withDescendant(by.text('Post')))).toExist() + await element(by.id('postDropdownBtn').withAncestor(alicePosts)) + .atIndex(0) + .tap() + await element(by.id('postDropdownDeleteBtn')).tap() + await expect(element(by.id('confirmModal'))).toBeVisible() + await element(by.id('confirmBtn')).tap() + await expect( + element(alicePosts.withDescendant(by.text('Post'))), + ).not.toExist() + }) +}) diff --git a/__e2e__/tests/login.test.ts b/__e2e__/tests/login.test.ts new file mode 100644 index 000000000..788016db6 --- /dev/null +++ b/__e2e__/tests/login.test.ts @@ -0,0 +1,19 @@ +/* eslint-env detox/detox */ + +import {openApp, login, createServer} from '../util' + +describe('Login', () => { + let service: string + beforeAll(async () => { + service = await createServer('?users') + await openApp({permissions: {notifications: 'YES'}}) + }) + + it('As Alice, I can login', async () => { + await expect(element(by.id('signInButton'))).toBeVisible() + await login(service, 'alice', 'hunter2', { + takeScreenshots: true, + }) + await device.takeScreenshot('5- opened home screen') + }) +}) diff --git a/__e2e__/tests/profile-screen.test.ts b/__e2e__/tests/profile-screen.test.ts new file mode 100644 index 000000000..e1b6dcaf4 --- /dev/null +++ b/__e2e__/tests/profile-screen.test.ts @@ -0,0 +1,173 @@ +/* eslint-env detox/detox */ + +import {openApp, login, createServer, sleep} from '../util' + +describe('Profile screen', () => { + let service: string + beforeAll(async () => { + service = await createServer('?users&posts') + await openApp({ + permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'}, + }) + }) + + it('Login and navigate to my profile', async () => { + await expect(element(by.id('signInButton'))).toBeVisible() + await login(service, 'alice', 'hunter2') + await element(by.id('bottomBarProfileBtn')).tap() + }) + + it('Open and close edit profile modal', async () => { + await element(by.id('profileHeaderEditProfileButton')).tap() + await expect(element(by.id('editProfileModal'))).toBeVisible() + await element(by.id('editProfileCancelBtn')).tap() + await expect(element(by.id('editProfileModal'))).not.toBeVisible() + }) + + it('Edit display name and description via the edit profile modal', async () => { + await element(by.id('profileHeaderEditProfileButton')).tap() + await expect(element(by.id('editProfileModal'))).toBeVisible() + await element(by.id('editProfileDisplayNameInput')).clearText() + await element(by.id('editProfileDisplayNameInput')).typeText('Alicia') + await element(by.id('editProfileDescriptionInput')).clearText() + await element(by.id('editProfileDescriptionInput')).typeText( + 'One cool hacker', + ) + await element(by.id('editProfileSaveBtn')).tap() + await expect(element(by.id('editProfileModal'))).not.toBeVisible() + await expect(element(by.id('profileHeaderDisplayName'))).toHaveText( + 'Alicia', + ) + await expect(element(by.id('profileHeaderDescription'))).toHaveText( + 'One cool hacker', + ) + }) + + it('Remove display name and description via the edit profile modal', async () => { + await element(by.id('profileHeaderEditProfileButton')).tap() + await expect(element(by.id('editProfileModal'))).toBeVisible() + await element(by.id('editProfileDisplayNameInput')).clearText() + await element(by.id('editProfileDescriptionInput')).clearText() + await element(by.id('editProfileSaveBtn')).tap() + await expect(element(by.id('editProfileModal'))).not.toBeVisible() + await expect(element(by.id('profileHeaderDisplayName'))).toHaveText( + 'alice.test', + ) + await expect(element(by.id('profileHeaderDescription'))).toHaveText('') + }) + + it('Set avi and banner via the edit profile modal', async () => { + await expect(element(by.id('userBannerFallback'))).toExist() + await expect(element(by.id('userAvatarFallback'))).toExist() + await element(by.id('profileHeaderEditProfileButton')).tap() + await expect(element(by.id('editProfileModal'))).toBeVisible() + await element(by.id('changeBannerBtn')).tap() + await element(by.id('changeBannerLibraryBtn')).tap() + await sleep(3e3) + await element(by.id('changeAvatarBtn')).tap() + await element(by.id('changeAvatarLibraryBtn')).tap() + await sleep(3e3) + await element(by.id('editProfileSaveBtn')).tap() + await expect(element(by.id('editProfileModal'))).not.toBeVisible() + await expect(element(by.id('userBannerImage'))).toExist() + await expect(element(by.id('userAvatarImage'))).toExist() + }) + + it('Remove avi and banner via the edit profile modal', async () => { + await expect(element(by.id('userBannerImage'))).toExist() + await expect(element(by.id('userAvatarImage'))).toExist() + await element(by.id('profileHeaderEditProfileButton')).tap() + await expect(element(by.id('editProfileModal'))).toBeVisible() + await element(by.id('changeBannerBtn')).tap() + await element(by.id('changeBannerRemoveBtn')).tap() + await element(by.id('changeAvatarBtn')).tap() + await element(by.id('changeAvatarRemoveBtn')).tap() + await element(by.id('editProfileSaveBtn')).tap() + await expect(element(by.id('editProfileModal'))).not.toBeVisible() + await expect(element(by.id('userBannerFallback'))).toExist() + await expect(element(by.id('userAvatarFallback'))).toExist() + }) + + it('Navigate to another user profile', async () => { + await element(by.id('bottomBarSearchBtn')).tap() + // have to wait for the toast to clear + await waitFor(element(by.id('searchTextInput'))) + .toBeVisible() + .withTimeout(2000) + await element(by.id('searchTextInput')).typeText('bob') + await element(by.id('searchAutoCompleteResult-bob.test')).tap() + await expect(element(by.id('profileView'))).toBeVisible() + }) + + it('Can follow/unfollow another user', async () => { + await element(by.id('followBtn')).tap() + await expect(element(by.id('unfollowBtn'))).toBeVisible() + await element(by.id('unfollowBtn')).tap() + await expect(element(by.id('followBtn'))).toBeVisible() + }) + + it('Can mute/unmute another user', async () => { + await expect(element(by.id('profileHeaderMutedNotice'))).not.toExist() + await element(by.id('profileHeaderDropdownBtn')).tap() + await element(by.id('profileHeaderDropdownMuteBtn')).tap() + await expect(element(by.id('profileHeaderMutedNotice'))).toBeVisible() + await element(by.id('profileHeaderDropdownBtn')).tap() + await element(by.id('profileHeaderDropdownMuteBtn')).tap() + await expect(element(by.id('profileHeaderMutedNotice'))).not.toExist() + }) + + it('Can report another user', async () => { + await element(by.id('profileHeaderDropdownBtn')).tap() + await element(by.id('profileHeaderDropdownReportBtn')).tap() + await expect(element(by.id('reportAccountModal'))).toBeVisible() + await element(by.id('reportAccountRadios-spam')).tap() + await element(by.id('sendReportBtn')).tap() + await expect(element(by.id('reportAccountModal'))).not.toBeVisible() + }) + + it('Can like posts', async () => { + const posts = by.id('feedItem-by-bob.test') + await expect( + element(by.id('likeCount').withAncestor(posts)).atIndex(0), + ).toHaveText('0') + await element(by.id('likeBtn').withAncestor(posts)).atIndex(0).tap() + await expect( + element(by.id('likeCount').withAncestor(posts)).atIndex(0), + ).toHaveText('1') + await element(by.id('likeBtn').withAncestor(posts)).atIndex(0).tap() + await expect( + element(by.id('likeCount').withAncestor(posts)).atIndex(0), + ).toHaveText('0') + }) + + it('Can repost posts', async () => { + const posts = by.id('feedItem-by-bob.test') + await expect( + element(by.id('repostCount').withAncestor(posts)).atIndex(0), + ).toHaveText('0') + await element(by.id('repostBtn').withAncestor(posts)).atIndex(0).tap() + await expect(element(by.id('repostModal'))).toBeVisible() + await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap() + await expect(element(by.id('repostModal'))).not.toBeVisible() + await expect( + element(by.id('repostCount').withAncestor(posts)).atIndex(0), + ).toHaveText('1') + await element(by.id('repostBtn').withAncestor(posts)).atIndex(0).tap() + await expect(element(by.id('repostModal'))).toBeVisible() + await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap() + await expect(element(by.id('repostModal'))).not.toBeVisible() + await expect( + element(by.id('repostCount').withAncestor(posts)).atIndex(0), + ).toHaveText('0') + }) + + it('Can report posts', async () => { + const posts = by.id('feedItem-by-bob.test') + await element(by.id('postDropdownBtn').withAncestor(posts)).atIndex(0).tap() + await element(by.id('postDropdownReportBtn')).tap() + await expect(element(by.id('reportPostModal'))).toBeVisible() + await element(by.id('reportPostRadios-spam')).tap() + await element(by.id('sendReportBtn')).tap() + await expect(element(by.id('reportPostModal'))).not.toBeVisible() + }) +}) diff --git a/__e2e__/tests/search-screen.test.ts b/__e2e__/tests/search-screen.test.ts new file mode 100644 index 000000000..093d97c89 --- /dev/null +++ b/__e2e__/tests/search-screen.test.ts @@ -0,0 +1,24 @@ +/* eslint-env detox/detox */ + +import {openApp, login, createServer} from '../util' + +describe('Search screen', () => { + let service: string + beforeAll(async () => { + service = await createServer('?users') + await openApp({ + permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'}, + }) + }) + + it('Login', async () => { + await login(service, 'alice', 'hunter2') + }) + + it('Navigate to another user profile via autocomplete', async () => { + await element(by.id('bottomBarSearchBtn')).tap() + await element(by.id('searchTextInput')).typeText('bob') + await element(by.id('searchAutoCompleteResult-bob.test')).tap() + await expect(element(by.id('profileView'))).toBeVisible() + }) +}) diff --git a/__e2e__/tests/shell.test.ts b/__e2e__/tests/shell.test.ts new file mode 100644 index 000000000..5cfd4277f --- /dev/null +++ b/__e2e__/tests/shell.test.ts @@ -0,0 +1,34 @@ +/* eslint-env detox/detox */ + +import {openApp, login, createServer} from '../util' + +describe('Shell', () => { + let service: string + beforeAll(async () => { + service = await createServer('?users') + await openApp({permissions: {notifications: 'YES'}}) + }) + + it('Login', async () => { + await login(service, 'alice', 'hunter2') + await element(by.id('homeScreenFeedTabs-Following')).tap() + }) + + it('Can swipe the shelf open', async () => { + await element(by.id('homeScreen')).swipe('right', 'fast', 0.75) + await expect(element(by.id('drawer'))).toBeVisible() + await element(by.id('drawer')).swipe('left', 'fast', 0.75) + await expect(element(by.id('drawer'))).not.toBeVisible() + }) + + it('Can open the shelf by pressing the header avi', async () => { + await element(by.id('viewHeaderDrawerBtn')).tap() + await expect(element(by.id('drawer'))).toBeVisible() + }) + + it('Can navigate using the shelf', async () => { + await element(by.id('menuItemButton-Notifications')).tap() + await expect(element(by.id('drawer'))).not.toBeVisible() + await expect(element(by.id('notificationsScreen'))).toBeVisible() + }) +}) diff --git a/__e2e__/tests/thread-screen.test.ts b/__e2e__/tests/thread-screen.test.ts new file mode 100644 index 000000000..f84c339ce --- /dev/null +++ b/__e2e__/tests/thread-screen.test.ts @@ -0,0 +1,123 @@ +/* eslint-env detox/detox */ + +import {openApp, login, createServer} from '../util' + +describe('Thread screen', () => { + let service: string + beforeAll(async () => { + service = await createServer('?users&follows&thread') + await openApp({permissions: {notifications: 'YES'}}) + }) + + it('Login & navigate to thread', async () => { + await login(service, 'alice', 'hunter2') + await element(by.id('homeScreenFeedTabs-Following')).tap() + await element(by.id('feedItem-by-bob.test')).atIndex(0).tap() + await expect( + element( + by + .id('postThreadItem-by-bob.test') + .withDescendant(by.text('Thread root')), + ), + ).toBeVisible() + await expect( + element( + by + .id('postThreadItem-by-carla.test') + .withDescendant(by.text('Thread reply')), + ), + ).toBeVisible() + }) + + it('Can like the root post', async () => { + const post = by.id('postThreadItem-by-bob.test') + await expect( + element(by.id('likeCount').withAncestor(post)).atIndex(0), + ).not.toExist() + await element(by.id('likeBtn').withAncestor(post)).atIndex(0).tap() + await expect( + element(by.id('likeCount').withAncestor(post)).atIndex(0), + ).toHaveText('1 like') + await element(by.id('likeBtn').withAncestor(post)).atIndex(0).tap() + await expect( + element(by.id('likeCount').withAncestor(post)).atIndex(0), + ).not.toExist() + }) + + it('Can like a reply post', async () => { + const post = by.id('postThreadItem-by-carla.test') + await expect( + element(by.id('likeCount').withAncestor(post)).atIndex(0), + ).toHaveText('0') + await element(by.id('likeBtn').withAncestor(post)).atIndex(0).tap() + await expect( + element(by.id('likeCount').withAncestor(post)).atIndex(0), + ).toHaveText('1') + await element(by.id('likeBtn').withAncestor(post)).atIndex(0).tap() + await expect( + element(by.id('likeCount').withAncestor(post)).atIndex(0), + ).toHaveText('0') + }) + + it('Can repost the root post', async () => { + const post = by.id('postThreadItem-by-bob.test') + await expect( + element(by.id('repostCount').withAncestor(post)).atIndex(0), + ).not.toExist() + await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap() + await expect(element(by.id('repostModal'))).toBeVisible() + await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap() + await expect(element(by.id('repostModal'))).not.toBeVisible() + await expect( + element(by.id('repostCount').withAncestor(post)).atIndex(0), + ).toHaveText('1 repost') + await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap() + await expect(element(by.id('repostModal'))).toBeVisible() + await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap() + await expect(element(by.id('repostModal'))).not.toBeVisible() + await expect( + element(by.id('repostCount').withAncestor(post)).atIndex(0), + ).not.toExist() + }) + + it('Can repost a reply post', async () => { + const post = by.id('postThreadItem-by-carla.test') + await expect( + element(by.id('repostCount').withAncestor(post)).atIndex(0), + ).toHaveText('0') + await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap() + await expect(element(by.id('repostModal'))).toBeVisible() + await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap() + await expect(element(by.id('repostModal'))).not.toBeVisible() + await expect( + element(by.id('repostCount').withAncestor(post)).atIndex(0), + ).toHaveText('1') + await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap() + await expect(element(by.id('repostModal'))).toBeVisible() + await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap() + await expect(element(by.id('repostModal'))).not.toBeVisible() + await expect( + element(by.id('repostCount').withAncestor(post)).atIndex(0), + ).toHaveText('0') + }) + + it('Can report the root post', async () => { + const post = by.id('postThreadItem-by-bob.test') + await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap() + await element(by.id('postDropdownReportBtn')).tap() + await expect(element(by.id('reportPostModal'))).toBeVisible() + await element(by.id('reportPostRadios-spam')).tap() + await element(by.id('sendReportBtn')).tap() + await expect(element(by.id('reportPostModal'))).not.toBeVisible() + }) + + it('Can report a reply post', async () => { + const post = by.id('postThreadItem-by-carla.test') + await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap() + await element(by.id('postDropdownReportBtn')).tap() + await expect(element(by.id('reportPostModal'))).toBeVisible() + await element(by.id('reportPostRadios-spam')).tap() + await element(by.id('sendReportBtn')).tap() + await expect(element(by.id('reportPostModal'))).not.toBeVisible() + }) +}) diff --git a/__e2e__/util.ts b/__e2e__/util.ts new file mode 100644 index 000000000..78d9f9f5d --- /dev/null +++ b/__e2e__/util.ts @@ -0,0 +1,96 @@ +import {resolveConfig} from 'detox/internals' + +const platform = device.getPlatform() + +export async function openApp(opts: any) { + opts = opts || {} + const config = await resolveConfig() + if (config.configurationName.split('.').includes('debug')) { + return await openAppForDebugBuild(platform, opts) + } else { + return await device.launchApp({ + ...opts, + newInstance: true, + }) + } +} + +export async function isVisible(id: string) { + try { + await expect(element(by.id(id))).toBeVisible() + return true + } catch (e) { + return false + } +} + +export async function login( + service: string, + username: string, + password: string, + {takeScreenshots} = {takeScreenshots: false}, +) { + await element(by.id('signInButton')).tap() + if (takeScreenshots) { + await device.takeScreenshot('1- opened sign-in screen') + } + if (await isVisible('chooseAccountForm')) { + await element(by.id('chooseNewAccountBtn')).tap() + } + await element(by.id('loginSelectServiceButton')).tap() + if (takeScreenshots) { + await device.takeScreenshot('2- opened service selector') + } + await element(by.id('customServerTextInput')).typeText(service) + await element(by.id('customServerSelectBtn')).tap() + if (takeScreenshots) { + await device.takeScreenshot('3- input custom service') + } + await element(by.id('loginUsernameInput')).typeText(username) + await element(by.id('loginPasswordInput')).typeText(password) + if (takeScreenshots) { + await device.takeScreenshot('4- entered username and password') + } + await element(by.id('loginNextButton')).tap() +} + +async function openAppForDebugBuild(platform: string, opts: any) { + const deepLinkUrl = // Local testing with packager + /*process.env.EXPO_USE_UPDATES + ? // Testing latest published EAS update for the test_debug channel + getDeepLinkUrl(getLatestUpdateUrl()) + : */ getDeepLinkUrl(getDevLauncherPackagerUrl(platform)) + + if (platform === 'ios') { + await device.launchApp({ + ...opts, + newInstance: true, + }) + sleep(3000) + await device.openURL({ + url: deepLinkUrl, + }) + } else { + await device.launchApp({ + ...opts, + newInstance: true, + url: deepLinkUrl, + }) + } + + await sleep(3000) +} + +export async function createServer(path = '') { + const res = await fetch(`http://localhost:1986/${path}`, {method: 'POST'}) + const resBody = await res.text() + return resBody +} + +const getDeepLinkUrl = (url: string) => + `expo+bluesky://expo-development-client/?url=${encodeURIComponent(url)}` + +const getDevLauncherPackagerUrl = (platform: string) => + `http://localhost:8081/index.bundle?platform=${platform}&dev=true&minify=false&disableOnboarding=1` + +export const sleep = (t: number) => new Promise(res => setTimeout(res, t)) diff --git a/__tests__/lib/link-meta.test.ts b/__tests__/lib/link-meta.test.ts index ce7da4152..8af14628e 100644 --- a/__tests__/lib/link-meta.test.ts +++ b/__tests__/lib/link-meta.test.ts @@ -4,14 +4,14 @@ import { getLikelyType, } from '../../src/lib/link-meta/link-meta' import {exampleComHtml} from './__mocks__/exampleComHtml' -import AtpAgent from '@atproto/api' +import {BskyAgent} from '@atproto/api' import {DEFAULT_SERVICE, RootStoreModel} from '../../src/state' describe('getLinkMeta', () => { let rootStore: RootStoreModel beforeEach(() => { - rootStore = new RootStoreModel(new AtpAgent({service: DEFAULT_SERVICE})) + rootStore = new RootStoreModel(new BskyAgent({service: DEFAULT_SERVICE})) }) const inputs = [ diff --git a/__tests__/lib/string.test.ts b/__tests__/lib/string.test.ts index 4f6fd62d6..f25bd02a7 100644 --- a/__tests__/lib/string.test.ts +++ b/__tests__/lib/string.test.ts @@ -7,172 +7,10 @@ import { } from '../../src/lib/strings/url-helpers' import {pluralize, enforceLen} from '../../src/lib/strings/helpers' import {ago} from '../../src/lib/strings/time' -import { - extractEntities, - detectLinkables, -} from '../../src/lib/strings/rich-text-detection' +import {detectLinkables} from '../../src/lib/strings/rich-text-detection' import {makeValidHandle, createFullHandle} from '../../src/lib/strings/handles' import {cleanError} from '../../src/lib/strings/errors' -describe('extractEntities', () => { - const knownHandles = new Set(['handle.com', 'full123.test-of-chars']) - const inputs = [ - 'no mention', - '@handle.com middle end', - 'start @handle.com end', - 'start middle @handle.com', - '@handle.com @handle.com @handle.com', - '@full123.test-of-chars', - 'not@right', - '@handle.com!@#$chars', - '@handle.com\n@handle.com', - 'parenthetical (@handle.com)', - 'start https://middle.com end', - 'start https://middle.com/foo/bar end', - 'start https://middle.com/foo/bar?baz=bux end', - 'start https://middle.com/foo/bar?baz=bux#hash end', - 'https://start.com/foo/bar?baz=bux#hash middle end', - 'start middle https://end.com/foo/bar?baz=bux#hash', - 'https://newline1.com\nhttps://newline2.com', - 'start middle.com end', - 'start middle.com/foo/bar end', - 'start middle.com/foo/bar?baz=bux end', - 'start middle.com/foo/bar?baz=bux#hash end', - 'start.com/foo/bar?baz=bux#hash middle end', - 'start middle end.com/foo/bar?baz=bux#hash', - 'newline1.com\nnewline2.com', - 'not.. a..url ..here', - 'e.g.', - 'something-cool.jpg', - 'website.com.jpg', - 'e.g./foo', - 'website.com.jpg/foo', - 'Classic article https://socket3.wordpress.com/2018/02/03/designing-windows-95s-user-interface/', - 'Classic article https://socket3.wordpress.com/2018/02/03/designing-windows-95s-user-interface/ ', - 'https://foo.com https://bar.com/whatever https://baz.com', - 'punctuation https://foo.com, https://bar.com/whatever; https://baz.com.', - 'parenthentical (https://foo.com)', - 'except for https://foo.com/thing_(cool)', - ] - interface Output { - type: string - value: string - noScheme?: boolean - } - const outputs: Output[][] = [ - [], - [{type: 'mention', value: 'handle.com'}], - [{type: 'mention', value: 'handle.com'}], - [{type: 'mention', value: 'handle.com'}], - [ - {type: 'mention', value: 'handle.com'}, - {type: 'mention', value: 'handle.com'}, - {type: 'mention', value: 'handle.com'}, - ], - [ - { - type: 'mention', - value: 'full123.test-of-chars', - }, - ], - [], - [{type: 'mention', value: 'handle.com'}], - [ - {type: 'mention', value: 'handle.com'}, - {type: 'mention', value: 'handle.com'}, - ], - [{type: 'mention', value: 'handle.com'}], - [{type: 'link', value: 'https://middle.com'}], - [{type: 'link', value: 'https://middle.com/foo/bar'}], - [{type: 'link', value: 'https://middle.com/foo/bar?baz=bux'}], - [{type: 'link', value: 'https://middle.com/foo/bar?baz=bux#hash'}], - [{type: 'link', value: 'https://start.com/foo/bar?baz=bux#hash'}], - [{type: 'link', value: 'https://end.com/foo/bar?baz=bux#hash'}], - [ - {type: 'link', value: 'https://newline1.com'}, - {type: 'link', value: 'https://newline2.com'}, - ], - [{type: 'link', value: 'middle.com', noScheme: true}], - [{type: 'link', value: 'middle.com/foo/bar', noScheme: true}], - [{type: 'link', value: 'middle.com/foo/bar?baz=bux', noScheme: true}], - [{type: 'link', value: 'middle.com/foo/bar?baz=bux#hash', noScheme: true}], - [{type: 'link', value: 'start.com/foo/bar?baz=bux#hash', noScheme: true}], - [{type: 'link', value: 'end.com/foo/bar?baz=bux#hash', noScheme: true}], - [ - {type: 'link', value: 'newline1.com', noScheme: true}, - {type: 'link', value: 'newline2.com', noScheme: true}, - ], - [], - [], - [], - [], - [], - [], - [ - { - type: 'link', - value: - 'https://socket3.wordpress.com/2018/02/03/designing-windows-95s-user-interface/', - }, - ], - [ - { - type: 'link', - value: - 'https://socket3.wordpress.com/2018/02/03/designing-windows-95s-user-interface/', - }, - ], - [ - {type: 'link', value: 'https://foo.com'}, - {type: 'link', value: 'https://bar.com/whatever'}, - {type: 'link', value: 'https://baz.com'}, - ], - [ - {type: 'link', value: 'https://foo.com'}, - {type: 'link', value: 'https://bar.com/whatever'}, - {type: 'link', value: 'https://baz.com'}, - ], - [{type: 'link', value: 'https://foo.com'}], - [{type: 'link', value: 'https://foo.com/thing_(cool)'}], - ] - it('correctly handles a set of text inputs', () => { - for (let i = 0; i < inputs.length; i++) { - const input = inputs[i] - const result = extractEntities(input, knownHandles) - if (!outputs[i].length) { - expect(result).toBeFalsy() - } else if (outputs[i].length && !result) { - expect(result).toBeTruthy() - } else if (result) { - expect(result.length).toBe(outputs[i].length) - for (let j = 0; j < outputs[i].length; j++) { - expect(result[j].type).toEqual(outputs[i][j].type) - if (outputs[i][j].noScheme) { - expect(result[j].value).toEqual(`https://${outputs[i][j].value}`) - } else { - expect(result[j].value).toEqual(outputs[i][j].value) - } - if (outputs[i]?.[j].type === 'mention') { - expect( - input.slice(result[j].index.start, result[j].index.end), - ).toBe(`@${result[j].value}`) - } else { - if (!outputs[i]?.[j].noScheme) { - expect( - input.slice(result[j].index.start, result[j].index.end), - ).toBe(result[j].value) - } else { - expect( - input.slice(result[j].index.start, result[j].index.end), - ).toBe(result[j].value.slice('https://'.length)) - } - } - } - } - } - }) -}) - describe('detectLinkables', () => { const inputs = [ 'no linkable', diff --git a/__tests__/lib/strings/rich-text-sanitize.ts b/__tests__/lib/strings/rich-text-sanitize.ts deleted file mode 100644 index d0bbae5e8..000000000 --- a/__tests__/lib/strings/rich-text-sanitize.ts +++ /dev/null @@ -1,123 +0,0 @@ -import {AppBskyFeedPost} from '@atproto/api' -type Entity = AppBskyFeedPost.Entity -import {RichText} from '../../../src/lib/strings/rich-text' -import {removeExcessNewlines} from '../../../src/lib/strings/rich-text-sanitize' - -describe('removeExcessNewlines', () => { - it('removes more than two consecutive new lines', () => { - const input = new RichText( - 'test\n\n\n\n\ntest\n\n\n\n\n\n\ntest\n\n\n\n\n\n\ntest\n\n\n\n\n\n\ntest', - ) - const output = removeExcessNewlines(input) - expect(output.text).toEqual('test\n\ntest\n\ntest\n\ntest\n\ntest') - }) - - it('removes more than two consecutive new lines with spaces', () => { - const input = new RichText( - 'test\n\n\n\n\ntest\n \n \n \n \n\n\ntest\n\n\n\n\n\n\ntest\n\n\n\n\n \n\ntest', - ) - const output = removeExcessNewlines(input) - expect(output.text).toEqual('test\n\ntest\n\ntest\n\ntest\n\ntest') - }) - - it('returns original string if there are no consecutive new lines', () => { - const input = new RichText('test\n\ntest\n\ntest\n\ntest\n\ntest') - const output = removeExcessNewlines(input) - expect(output.text).toEqual(input.text) - }) - - it('returns original string if there are no new lines', () => { - const input = new RichText('test test test test test') - const output = removeExcessNewlines(input) - expect(output.text).toEqual(input.text) - }) - - it('returns empty string if input is empty', () => { - const input = new RichText('') - const output = removeExcessNewlines(input) - expect(output.text).toEqual('') - }) - - it('works with different types of new line characters', () => { - const input = new RichText( - 'test\r\ntest\n\rtest\rtest\n\n\n\ntest\n\r \n \n \n \n\n\ntest', - ) - const output = removeExcessNewlines(input) - expect(output.text).toEqual('test\r\ntest\n\rtest\rtest\n\ntest\n\ntest') - }) - - it('removes more than two consecutive new lines with zero width space', () => { - const input = new RichText( - 'test\n\n\n\n\ntest\n\u200B\u200B\n\n\n\ntest\n \u200B\u200B \n\n\n\ntest\n\n\n\n\n\n\ntest', - ) - const output = removeExcessNewlines(input) - expect(output.text).toEqual('test\n\ntest\n\ntest\n\ntest\n\ntest') - }) - - it('removes more than two consecutive new lines with zero width non-joiner', () => { - const input = new RichText( - 'test\n\n\n\n\ntest\n\u200C\u200C\n\n\n\ntest\n \u200C\u200C \n\n\n\ntest\n\n\n\n\n\n\ntest', - ) - const output = removeExcessNewlines(input) - expect(output.text).toEqual('test\n\ntest\n\ntest\n\ntest\n\ntest') - }) - - it('removes more than two consecutive new lines with zero width joiner', () => { - const input = new RichText( - 'test\n\n\n\n\ntest\n\u200D\u200D\n\n\n\ntest\n \u200D\u200D \n\n\n\ntest\n\n\n\n\n\n\ntest', - ) - const output = removeExcessNewlines(input) - expect(output.text).toEqual('test\n\ntest\n\ntest\n\ntest\n\ntest') - }) - - it('removes more than two consecutive new lines with soft hyphen', () => { - const input = new RichText( - 'test\n\n\n\n\ntest\n\u00AD\u00AD\n\n\n\ntest\n \u00AD\u00AD \n\n\n\ntest\n\n\n\n\n\n\ntest', - ) - const output = removeExcessNewlines(input) - expect(output.text).toEqual('test\n\ntest\n\ntest\n\ntest\n\ntest') - }) - - it('removes more than two consecutive new lines with word joiner', () => { - const input = new RichText( - 'test\n\n\n\n\ntest\n\u2060\u2060\n\n\n\ntest\n \u2060\u2060 \n\n\n\ntest\n\n\n\n\n\n\ntest', - ) - const output = removeExcessNewlines(input) - expect(output.text).toEqual('test\n\ntest\n\ntest\n\ntest\n\ntest') - }) -}) - -describe('removeExcessNewlines w/entities', () => { - it('preserves entities as expected', () => { - const input = new RichText( - 'test\n\n\n\n\ntest\n\n\n\n\n\n\ntest\n\n\n\n\n\n\ntest\n\n\n\n\n\n\ntest', - [ - {index: {start: 0, end: 13}, type: '', value: ''}, - {index: {start: 13, end: 24}, type: '', value: ''}, - {index: {start: 9, end: 15}, type: '', value: ''}, - {index: {start: 4, end: 9}, type: '', value: ''}, - ], - ) - const output = removeExcessNewlines(input) - expect(entToStr(input.text, input.entities?.[0])).toEqual( - 'test\n\n\n\n\ntest', - ) - expect(entToStr(input.text, input.entities?.[1])).toEqual( - '\n\n\n\n\n\n\ntest', - ) - expect(entToStr(input.text, input.entities?.[2])).toEqual('test\n\n') - expect(entToStr(input.text, input.entities?.[3])).toEqual('\n\n\n\n\n') - expect(output.text).toEqual('test\n\ntest\n\ntest\n\ntest\n\ntest') - expect(entToStr(output.text, output.entities?.[0])).toEqual('test\n\ntest') - expect(entToStr(output.text, output.entities?.[1])).toEqual('test') - expect(entToStr(output.text, output.entities?.[2])).toEqual('test') - expect(output.entities?.[3]).toEqual(undefined) - }) -}) - -function entToStr(str: string, ent?: Entity) { - if (!ent) { - return '' - } - return str.slice(ent.index.start, ent.index.end) -} diff --git a/__tests__/lib/strings/rich-text.ts b/__tests__/lib/strings/rich-text.ts deleted file mode 100644 index e52ac6cec..000000000 --- a/__tests__/lib/strings/rich-text.ts +++ /dev/null @@ -1,123 +0,0 @@ -import {RichText} from '../../../src/lib/strings/rich-text' - -describe('richText.insert', () => { - const input = new RichText('hello world', [ - {index: {start: 2, end: 7}, type: '', value: ''}, - ]) - - it('correctly adjusts entities (scenario A - before)', () => { - const output = input.clone().insert(0, 'test') - expect(output.text).toEqual('testhello world') - expect(output.entities?.[0].index.start).toEqual(6) - expect(output.entities?.[0].index.end).toEqual(11) - expect( - output.text.slice( - output.entities?.[0].index.start, - output.entities?.[0].index.end, - ), - ).toEqual('llo w') - }) - - it('correctly adjusts entities (scenario B - inner)', () => { - const output = input.clone().insert(4, 'test') - expect(output.text).toEqual('helltesto world') - expect(output.entities?.[0].index.start).toEqual(2) - expect(output.entities?.[0].index.end).toEqual(11) - expect( - output.text.slice( - output.entities?.[0].index.start, - output.entities?.[0].index.end, - ), - ).toEqual('lltesto w') - }) - - it('correctly adjusts entities (scenario C - after)', () => { - const output = input.clone().insert(8, 'test') - expect(output.text).toEqual('hello wotestrld') - expect(output.entities?.[0].index.start).toEqual(2) - expect(output.entities?.[0].index.end).toEqual(7) - expect( - output.text.slice( - output.entities?.[0].index.start, - output.entities?.[0].index.end, - ), - ).toEqual('llo w') - }) -}) - -describe('richText.delete', () => { - const input = new RichText('hello world', [ - {index: {start: 2, end: 7}, type: '', value: ''}, - ]) - - it('correctly adjusts entities (scenario A - entirely outer)', () => { - const output = input.clone().delete(0, 9) - expect(output.text).toEqual('ld') - expect(output.entities?.length).toEqual(0) - }) - - it('correctly adjusts entities (scenario B - entirely after)', () => { - const output = input.clone().delete(7, 11) - expect(output.text).toEqual('hello w') - expect(output.entities?.[0].index.start).toEqual(2) - expect(output.entities?.[0].index.end).toEqual(7) - expect( - output.text.slice( - output.entities?.[0].index.start, - output.entities?.[0].index.end, - ), - ).toEqual('llo w') - }) - - it('correctly adjusts entities (scenario C - partially after)', () => { - const output = input.clone().delete(4, 11) - expect(output.text).toEqual('hell') - expect(output.entities?.[0].index.start).toEqual(2) - expect(output.entities?.[0].index.end).toEqual(4) - expect( - output.text.slice( - output.entities?.[0].index.start, - output.entities?.[0].index.end, - ), - ).toEqual('ll') - }) - - it('correctly adjusts entities (scenario D - entirely inner)', () => { - const output = input.clone().delete(3, 5) - expect(output.text).toEqual('hel world') - expect(output.entities?.[0].index.start).toEqual(2) - expect(output.entities?.[0].index.end).toEqual(5) - expect( - output.text.slice( - output.entities?.[0].index.start, - output.entities?.[0].index.end, - ), - ).toEqual('l w') - }) - - it('correctly adjusts entities (scenario E - partially before)', () => { - const output = input.clone().delete(1, 5) - expect(output.text).toEqual('h world') - expect(output.entities?.[0].index.start).toEqual(1) - expect(output.entities?.[0].index.end).toEqual(3) - expect( - output.text.slice( - output.entities?.[0].index.start, - output.entities?.[0].index.end, - ), - ).toEqual(' w') - }) - - it('correctly adjusts entities (scenario F - entirely before)', () => { - const output = input.clone().delete(0, 2) - expect(output.text).toEqual('llo world') - expect(output.entities?.[0].index.start).toEqual(0) - expect(output.entities?.[0].index.end).toEqual(5) - expect( - output.text.slice( - output.entities?.[0].index.start, - output.entities?.[0].index.end, - ), - ).toEqual('llo w') - }) -}) diff --git a/app.json b/app.json index a4749144e..0e5bb2e03 100644 --- a/app.json +++ b/app.json @@ -2,7 +2,7 @@ "expo": { "name": "bluesky", "slug": "bluesky", - "version": "1.10.0", + "version": "1.11.0", "orientation": "portrait", "icon": "./assets/icon.png", "userInterfaceStyle": "light", diff --git a/e2e/tests/happyPath.test.js b/e2e/tests/happyPath.test.js deleted file mode 100644 index 4176cecb9..000000000 --- a/e2e/tests/happyPath.test.js +++ /dev/null @@ -1,70 +0,0 @@ -/* eslint-env detox/detox */ - -describe('Happy paths', () => { - async function grantAccessToUserWithValidCredentials( - username, - {takeScreenshots} = {takeScreenshots: false}, - ) { - await element(by.id('signInButton')).tap() - if (takeScreenshots) { - await device.takeScreenshot('1- opened sign-in screen') - } - await element(by.id('loginSelectServiceButton')).tap() - if (takeScreenshots) { - await device.takeScreenshot('2- opened service selector') - } - await element(by.id('localDevServerButton')).tap() - if (takeScreenshots) { - await device.takeScreenshot('3- selected local dev server') - } - await element(by.id('loginUsernameInput')).typeText(username) - await element(by.id('loginPasswordInput')).typeText('hunter2') - if (takeScreenshots) { - await device.takeScreenshot('4- entered username and password') - } - await element(by.id('loginNextButton')).tap() - } - - beforeEach(async () => { - await device.uninstallApp() - await device.installApp() - await device.launchApp({permissions: {notifications: 'YES'}}) - }) - - it('As Alice, I can login', async () => { - await expect(element(by.id('signInButton'))).toBeVisible() - await grantAccessToUserWithValidCredentials('alice', { - takeScreenshots: true, - }) - await device.takeScreenshot('5- opened home screen') - }) - - it('As Alice, I can login, and post a text', async () => { - await grantAccessToUserWithValidCredentials('alice') - await element(by.id('composeFAB')).tap() - await device.takeScreenshot('1- opened composer') - await element(by.id('composerTextInput')).typeText( - 'Greetings earthlings, I come in peace... and to run some tests.', - ) - await device.takeScreenshot('2- entered text') - await element(by.id('composerPublishButton')).tap() - await device.takeScreenshot('3- opened general section') - await expect(element(by.id('composeFAB'))).toBeVisible() - }) - - it('I can create a new account', async () => { - await element(by.id('createAccountButton')).tap() - await device.takeScreenshot('1- opened create account screen') - await element(by.id('registerSelectServiceButton')).tap() - await device.takeScreenshot('2- opened service selector') - await element(by.id('localDevServerButton')).tap() - await device.takeScreenshot('3- selected local dev server') - await element(by.id('registerEmailInput')).typeText('example@test.com') - await element(by.id('registerPasswordInput')).typeText('hunter2') - await element(by.id('registerHandleInput')).typeText('e2e-test') - await element(by.id('registerIs13Input')).tap() - await device.takeScreenshot('4- entered account details') - await element(by.id('createAccountButton')).tap() - await expect(element(by.id('welcomeBanner'))).toBeVisible() - }) -}) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 761ec7373..8f9e5f903 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -22,7 +22,7 @@ PODS: - EXMediaLibrary (15.2.3): - ExpoModulesCore - React-Core - - Expo (48.0.7): + - Expo (48.0.9): - ExpoModulesCore - expo-dev-client (2.1.5): - EXManifests @@ -102,7 +102,7 @@ PODS: - ExpoModulesCore - ExpoLocalization (14.1.1): - ExpoModulesCore - - ExpoModulesCore (1.2.5): + - ExpoModulesCore (1.2.6): - React-Core - React-RCTAppDelegate - ReactCommon/turbomodule/core @@ -110,19 +110,19 @@ PODS: - ExpoModulesCore - React-Core - EXUpdatesInterface (0.9.1) - - FBLazyVector (0.71.3) - - FBReactNativeSpec (0.71.3): + - FBLazyVector (0.71.4) + - FBReactNativeSpec (0.71.4): - RCT-Folly (= 2021.07.22.00) - - RCTRequired (= 0.71.3) - - RCTTypeSafety (= 0.71.3) - - React-Core (= 0.71.3) - - React-jsi (= 0.71.3) - - ReactCommon/turbomodule/core (= 0.71.3) + - RCTRequired (= 0.71.4) + - RCTTypeSafety (= 0.71.4) + - React-Core (= 0.71.4) + - React-jsi (= 0.71.4) + - ReactCommon/turbomodule/core (= 0.71.4) - fmt (6.2.1) - glog (0.3.5) - - hermes-engine (0.71.3): - - hermes-engine/Pre-built (= 0.71.3) - - hermes-engine/Pre-built (0.71.3) + - hermes-engine (0.71.4): + - hermes-engine/Pre-built (= 0.71.4) + - hermes-engine/Pre-built (0.71.4) - libevent (2.1.12) - libwebp (1.2.4): - libwebp/demux (= 1.2.4) @@ -150,26 +150,26 @@ PODS: - fmt (~> 6.2.1) - glog - libevent - - RCTRequired (0.71.3) - - RCTTypeSafety (0.71.3): - - FBLazyVector (= 0.71.3) - - RCTRequired (= 0.71.3) - - React-Core (= 0.71.3) - - React (0.71.3): - - React-Core (= 0.71.3) - - React-Core/DevSupport (= 0.71.3) - - React-Core/RCTWebSocket (= 0.71.3) - - React-RCTActionSheet (= 0.71.3) - - React-RCTAnimation (= 0.71.3) - - React-RCTBlob (= 0.71.3) - - React-RCTImage (= 0.71.3) - - React-RCTLinking (= 0.71.3) - - React-RCTNetwork (= 0.71.3) - - React-RCTSettings (= 0.71.3) - - React-RCTText (= 0.71.3) - - React-RCTVibration (= 0.71.3) - - React-callinvoker (0.71.3) - - React-Codegen (0.71.3): + - RCTRequired (0.71.4) + - RCTTypeSafety (0.71.4): + - FBLazyVector (= 0.71.4) + - RCTRequired (= 0.71.4) + - React-Core (= 0.71.4) + - React (0.71.4): + - React-Core (= 0.71.4) + - React-Core/DevSupport (= 0.71.4) + - React-Core/RCTWebSocket (= 0.71.4) + - React-RCTActionSheet (= 0.71.4) + - React-RCTAnimation (= 0.71.4) + - React-RCTBlob (= 0.71.4) + - React-RCTImage (= 0.71.4) + - React-RCTLinking (= 0.71.4) + - React-RCTNetwork (= 0.71.4) + - React-RCTSettings (= 0.71.4) + - React-RCTText (= 0.71.4) + - React-RCTVibration (= 0.71.4) + - React-callinvoker (0.71.4) + - React-Codegen (0.71.4): - FBReactNativeSpec - hermes-engine - RCT-Folly @@ -180,209 +180,209 @@ PODS: - React-jsiexecutor - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - React-Core (0.71.3): + - React-Core (0.71.4): - glog - hermes-engine - RCT-Folly (= 2021.07.22.00) - - React-Core/Default (= 0.71.3) - - React-cxxreact (= 0.71.3) + - React-Core/Default (= 0.71.4) + - React-cxxreact (= 0.71.4) - React-hermes - - React-jsi (= 0.71.3) - - React-jsiexecutor (= 0.71.3) - - React-perflogger (= 0.71.3) + - React-jsi (= 0.71.4) + - React-jsiexecutor (= 0.71.4) + - React-perflogger (= 0.71.4) - Yoga - - React-Core/CoreModulesHeaders (0.71.3): + - React-Core/CoreModulesHeaders (0.71.4): - glog - hermes-engine - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.71.3) + - React-cxxreact (= 0.71.4) - React-hermes - - React-jsi (= 0.71.3) - - React-jsiexecutor (= 0.71.3) - - React-perflogger (= 0.71.3) + - React-jsi (= 0.71.4) + - React-jsiexecutor (= 0.71.4) + - React-perflogger (= 0.71.4) - Yoga - - React-Core/Default (0.71.3): + - React-Core/Default (0.71.4): - glog - hermes-engine - RCT-Folly (= 2021.07.22.00) - - React-cxxreact (= 0.71.3) + - React-cxxreact (= 0.71.4) - React-hermes - - React-jsi (= 0.71.3) - - React-jsiexecutor (= 0.71.3) - - React-perflogger (= 0.71.3) + - React-jsi (= 0.71.4) + - React-jsiexecutor (= 0.71.4) + - React-perflogger (= 0.71.4) - Yoga - - React-Core/DevSupport (0.71.3): + - React-Core/DevSupport (0.71.4): - glog - hermes-engine - RCT-Folly (= 2021.07.22.00) - - React-Core/Default (= 0.71.3) - - React-Core/RCTWebSocket (= 0.71.3) - - React-cxxreact (= 0.71.3) + - React-Core/Default (= 0.71.4) + - React-Core/RCTWebSocket (= 0.71.4) + - React-cxxreact (= 0.71.4) - React-hermes - - React-jsi (= 0.71.3) - - React-jsiexecutor (= 0.71.3) - - React-jsinspector (= 0.71.3) - - React-perflogger (= 0.71.3) + - React-jsi (= 0.71.4) + - React-jsiexecutor (= 0.71.4) + - React-jsinspector (= 0.71.4) + - React-perflogger (= 0.71.4) - Yoga - - React-Core/RCTActionSheetHeaders (0.71.3): + - React-Core/RCTActionSheetHeaders (0.71.4): - glog - hermes-engine - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.71.3) + - React-cxxreact (= 0.71.4) - React-hermes - - React-jsi (= 0.71.3) - - React-jsiexecutor (= 0.71.3) - - React-perflogger (= 0.71.3) + - React-jsi (= 0.71.4) + - React-jsiexecutor (= 0.71.4) + - React-perflogger (= 0.71.4) - Yoga - - React-Core/RCTAnimationHeaders (0.71.3): + - React-Core/RCTAnimationHeaders (0.71.4): - glog - hermes-engine - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.71.3) + - React-cxxreact (= 0.71.4) - React-hermes - - React-jsi (= 0.71.3) - - React-jsiexecutor (= 0.71.3) - - React-perflogger (= 0.71.3) + - React-jsi (= 0.71.4) + - React-jsiexecutor (= 0.71.4) + - React-perflogger (= 0.71.4) - Yoga - - React-Core/RCTBlobHeaders (0.71.3): + - React-Core/RCTBlobHeaders (0.71.4): - glog - hermes-engine - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.71.3) + - React-cxxreact (= 0.71.4) - React-hermes - - React-jsi (= 0.71.3) - - React-jsiexecutor (= 0.71.3) - - React-perflogger (= 0.71.3) + - React-jsi (= 0.71.4) + - React-jsiexecutor (= 0.71.4) + - React-perflogger (= 0.71.4) - Yoga - - React-Core/RCTImageHeaders (0.71.3): + - React-Core/RCTImageHeaders (0.71.4): - glog - hermes-engine - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.71.3) + - React-cxxreact (= 0.71.4) - React-hermes - - React-jsi (= 0.71.3) - - React-jsiexecutor (= 0.71.3) - - React-perflogger (= 0.71.3) + - React-jsi (= 0.71.4) + - React-jsiexecutor (= 0.71.4) + - React-perflogger (= 0.71.4) - Yoga - - React-Core/RCTLinkingHeaders (0.71.3): + - React-Core/RCTLinkingHeaders (0.71.4): - glog - hermes-engine - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.71.3) + - React-cxxreact (= 0.71.4) - React-hermes - - React-jsi (= 0.71.3) - - React-jsiexecutor (= 0.71.3) - - React-perflogger (= 0.71.3) + - React-jsi (= 0.71.4) + - React-jsiexecutor (= 0.71.4) + - React-perflogger (= 0.71.4) - Yoga - - React-Core/RCTNetworkHeaders (0.71.3): + - React-Core/RCTNetworkHeaders (0.71.4): - glog - hermes-engine - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.71.3) + - React-cxxreact (= 0.71.4) - React-hermes - - React-jsi (= 0.71.3) - - React-jsiexecutor (= 0.71.3) - - React-perflogger (= 0.71.3) + - React-jsi (= 0.71.4) + - React-jsiexecutor (= 0.71.4) + - React-perflogger (= 0.71.4) - Yoga - - React-Core/RCTSettingsHeaders (0.71.3): + - React-Core/RCTSettingsHeaders (0.71.4): - glog - hermes-engine - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.71.3) + - React-cxxreact (= 0.71.4) - React-hermes - - React-jsi (= 0.71.3) - - React-jsiexecutor (= 0.71.3) - - React-perflogger (= 0.71.3) + - React-jsi (= 0.71.4) + - React-jsiexecutor (= 0.71.4) + - React-perflogger (= 0.71.4) - Yoga - - React-Core/RCTTextHeaders (0.71.3): + - React-Core/RCTTextHeaders (0.71.4): - glog - hermes-engine - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.71.3) + - React-cxxreact (= 0.71.4) - React-hermes - - React-jsi (= 0.71.3) - - React-jsiexecutor (= 0.71.3) - - React-perflogger (= 0.71.3) + - React-jsi (= 0.71.4) + - React-jsiexecutor (= 0.71.4) + - React-perflogger (= 0.71.4) - Yoga - - React-Core/RCTVibrationHeaders (0.71.3): + - React-Core/RCTVibrationHeaders (0.71.4): - glog - hermes-engine - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.71.3) + - React-cxxreact (= 0.71.4) - React-hermes - - React-jsi (= 0.71.3) - - React-jsiexecutor (= 0.71.3) - - React-perflogger (= 0.71.3) + - React-jsi (= 0.71.4) + - React-jsiexecutor (= 0.71.4) + - React-perflogger (= 0.71.4) - Yoga - - React-Core/RCTWebSocket (0.71.3): + - React-Core/RCTWebSocket (0.71.4): - glog - hermes-engine - RCT-Folly (= 2021.07.22.00) - - React-Core/Default (= 0.71.3) - - React-cxxreact (= 0.71.3) + - React-Core/Default (= 0.71.4) + - React-cxxreact (= 0.71.4) - React-hermes - - React-jsi (= 0.71.3) - - React-jsiexecutor (= 0.71.3) - - React-perflogger (= 0.71.3) + - React-jsi (= 0.71.4) + - React-jsiexecutor (= 0.71.4) + - React-perflogger (= 0.71.4) - Yoga - - React-CoreModules (0.71.3): + - React-CoreModules (0.71.4): - RCT-Folly (= 2021.07.22.00) - - RCTTypeSafety (= 0.71.3) - - React-Codegen (= 0.71.3) - - React-Core/CoreModulesHeaders (= 0.71.3) - - React-jsi (= 0.71.3) + - RCTTypeSafety (= 0.71.4) + - React-Codegen (= 0.71.4) + - React-Core/CoreModulesHeaders (= 0.71.4) + - React-jsi (= 0.71.4) - React-RCTBlob - - React-RCTImage (= 0.71.3) - - ReactCommon/turbomodule/core (= 0.71.3) - - React-cxxreact (0.71.3): + - React-RCTImage (= 0.71.4) + - ReactCommon/turbomodule/core (= 0.71.4) + - React-cxxreact (0.71.4): - boost (= 1.76.0) - DoubleConversion - glog - hermes-engine - RCT-Folly (= 2021.07.22.00) - - React-callinvoker (= 0.71.3) - - React-jsi (= 0.71.3) - - React-jsinspector (= 0.71.3) - - React-logger (= 0.71.3) - - React-perflogger (= 0.71.3) - - React-runtimeexecutor (= 0.71.3) - - React-hermes (0.71.3): + - React-callinvoker (= 0.71.4) + - React-jsi (= 0.71.4) + - React-jsinspector (= 0.71.4) + - React-logger (= 0.71.4) + - React-perflogger (= 0.71.4) + - React-runtimeexecutor (= 0.71.4) + - React-hermes (0.71.4): - DoubleConversion - glog - hermes-engine - RCT-Folly (= 2021.07.22.00) - RCT-Folly/Futures (= 2021.07.22.00) - - React-cxxreact (= 0.71.3) + - React-cxxreact (= 0.71.4) - React-jsi - - React-jsiexecutor (= 0.71.3) - - React-jsinspector (= 0.71.3) - - React-perflogger (= 0.71.3) - - React-jsi (0.71.3): + - React-jsiexecutor (= 0.71.4) + - React-jsinspector (= 0.71.4) + - React-perflogger (= 0.71.4) + - React-jsi (0.71.4): - boost (= 1.76.0) - DoubleConversion - glog - hermes-engine - RCT-Folly (= 2021.07.22.00) - - React-jsiexecutor (0.71.3): + - React-jsiexecutor (0.71.4): - DoubleConversion - glog - hermes-engine - RCT-Folly (= 2021.07.22.00) - - React-cxxreact (= 0.71.3) - - React-jsi (= 0.71.3) - - React-perflogger (= 0.71.3) - - React-jsinspector (0.71.3) - - React-logger (0.71.3): + - React-cxxreact (= 0.71.4) + - React-jsi (= 0.71.4) + - React-perflogger (= 0.71.4) + - React-jsinspector (0.71.4) + - React-logger (0.71.4): - glog - react-native-blur (4.3.0): - React-Core @@ -407,92 +407,90 @@ PODS: - React-Core - react-native-version-number (0.3.6): - React - - react-native-webview (11.26.0): - - React-Core - - React-perflogger (0.71.3) - - React-RCTActionSheet (0.71.3): - - React-Core/RCTActionSheetHeaders (= 0.71.3) - - React-RCTAnimation (0.71.3): + - React-perflogger (0.71.4) + - React-RCTActionSheet (0.71.4): + - React-Core/RCTActionSheetHeaders (= 0.71.4) + - React-RCTAnimation (0.71.4): - RCT-Folly (= 2021.07.22.00) - - RCTTypeSafety (= 0.71.3) - - React-Codegen (= 0.71.3) - - React-Core/RCTAnimationHeaders (= 0.71.3) - - React-jsi (= 0.71.3) - - ReactCommon/turbomodule/core (= 0.71.3) - - React-RCTAppDelegate (0.71.3): + - RCTTypeSafety (= 0.71.4) + - React-Codegen (= 0.71.4) + - React-Core/RCTAnimationHeaders (= 0.71.4) + - React-jsi (= 0.71.4) + - ReactCommon/turbomodule/core (= 0.71.4) + - React-RCTAppDelegate (0.71.4): - RCT-Folly - RCTRequired - RCTTypeSafety - React-Core - ReactCommon/turbomodule/core - - React-RCTBlob (0.71.3): + - React-RCTBlob (0.71.4): - hermes-engine - RCT-Folly (= 2021.07.22.00) - - React-Codegen (= 0.71.3) - - React-Core/RCTBlobHeaders (= 0.71.3) - - React-Core/RCTWebSocket (= 0.71.3) - - React-jsi (= 0.71.3) - - React-RCTNetwork (= 0.71.3) - - ReactCommon/turbomodule/core (= 0.71.3) - - React-RCTImage (0.71.3): + - React-Codegen (= 0.71.4) + - React-Core/RCTBlobHeaders (= 0.71.4) + - React-Core/RCTWebSocket (= 0.71.4) + - React-jsi (= 0.71.4) + - React-RCTNetwork (= 0.71.4) + - ReactCommon/turbomodule/core (= 0.71.4) + - React-RCTImage (0.71.4): - RCT-Folly (= 2021.07.22.00) - - RCTTypeSafety (= 0.71.3) - - React-Codegen (= 0.71.3) - - React-Core/RCTImageHeaders (= 0.71.3) - - React-jsi (= 0.71.3) - - React-RCTNetwork (= 0.71.3) - - ReactCommon/turbomodule/core (= 0.71.3) - - React-RCTLinking (0.71.3): - - React-Codegen (= 0.71.3) - - React-Core/RCTLinkingHeaders (= 0.71.3) - - React-jsi (= 0.71.3) - - ReactCommon/turbomodule/core (= 0.71.3) - - React-RCTNetwork (0.71.3): + - RCTTypeSafety (= 0.71.4) + - React-Codegen (= 0.71.4) + - React-Core/RCTImageHeaders (= 0.71.4) + - React-jsi (= 0.71.4) + - React-RCTNetwork (= 0.71.4) + - ReactCommon/turbomodule/core (= 0.71.4) + - React-RCTLinking (0.71.4): + - React-Codegen (= 0.71.4) + - React-Core/RCTLinkingHeaders (= 0.71.4) + - React-jsi (= 0.71.4) + - ReactCommon/turbomodule/core (= 0.71.4) + - React-RCTNetwork (0.71.4): - RCT-Folly (= 2021.07.22.00) - - RCTTypeSafety (= 0.71.3) - - React-Codegen (= 0.71.3) - - React-Core/RCTNetworkHeaders (= 0.71.3) - - React-jsi (= 0.71.3) - - ReactCommon/turbomodule/core (= 0.71.3) - - React-RCTSettings (0.71.3): + - RCTTypeSafety (= 0.71.4) + - React-Codegen (= 0.71.4) + - React-Core/RCTNetworkHeaders (= 0.71.4) + - React-jsi (= 0.71.4) + - ReactCommon/turbomodule/core (= 0.71.4) + - React-RCTSettings (0.71.4): - RCT-Folly (= 2021.07.22.00) - - RCTTypeSafety (= 0.71.3) - - React-Codegen (= 0.71.3) - - React-Core/RCTSettingsHeaders (= 0.71.3) - - React-jsi (= 0.71.3) - - ReactCommon/turbomodule/core (= 0.71.3) - - React-RCTText (0.71.3): - - React-Core/RCTTextHeaders (= 0.71.3) - - React-RCTVibration (0.71.3): + - RCTTypeSafety (= 0.71.4) + - React-Codegen (= 0.71.4) + - React-Core/RCTSettingsHeaders (= 0.71.4) + - React-jsi (= 0.71.4) + - ReactCommon/turbomodule/core (= 0.71.4) + - React-RCTText (0.71.4): + - React-Core/RCTTextHeaders (= 0.71.4) + - React-RCTVibration (0.71.4): - RCT-Folly (= 2021.07.22.00) - - React-Codegen (= 0.71.3) - - React-Core/RCTVibrationHeaders (= 0.71.3) - - React-jsi (= 0.71.3) - - ReactCommon/turbomodule/core (= 0.71.3) - - React-runtimeexecutor (0.71.3): - - React-jsi (= 0.71.3) - - ReactCommon/turbomodule/bridging (0.71.3): + - React-Codegen (= 0.71.4) + - React-Core/RCTVibrationHeaders (= 0.71.4) + - React-jsi (= 0.71.4) + - ReactCommon/turbomodule/core (= 0.71.4) + - React-runtimeexecutor (0.71.4): + - React-jsi (= 0.71.4) + - ReactCommon/turbomodule/bridging (0.71.4): - DoubleConversion - glog - hermes-engine - RCT-Folly (= 2021.07.22.00) - - React-callinvoker (= 0.71.3) - - React-Core (= 0.71.3) - - React-cxxreact (= 0.71.3) - - React-jsi (= 0.71.3) - - React-logger (= 0.71.3) - - React-perflogger (= 0.71.3) - - ReactCommon/turbomodule/core (0.71.3): + - React-callinvoker (= 0.71.4) + - React-Core (= 0.71.4) + - React-cxxreact (= 0.71.4) + - React-jsi (= 0.71.4) + - React-logger (= 0.71.4) + - React-perflogger (= 0.71.4) + - ReactCommon/turbomodule/core (0.71.4): - DoubleConversion - glog - hermes-engine - RCT-Folly (= 2021.07.22.00) - - React-callinvoker (= 0.71.3) - - React-Core (= 0.71.3) - - React-cxxreact (= 0.71.3) - - React-jsi (= 0.71.3) - - React-logger (= 0.71.3) - - React-perflogger (= 0.71.3) + - React-callinvoker (= 0.71.4) + - React-Core (= 0.71.4) + - React-cxxreact (= 0.71.4) + - React-jsi (= 0.71.4) + - React-logger (= 0.71.4) + - React-perflogger (= 0.71.4) - rn-fetch-blob (0.12.0): - React-Core - RNBackgroundFetch (4.1.9): @@ -627,7 +625,6 @@ DEPENDENCIES: - 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-native-webview (from `../node_modules/react-native-webview`) - 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`) @@ -770,8 +767,6 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-splash-screen" react-native-version-number: :path: "../node_modules/react-native-version-number" - react-native-webview: - :path: "../node_modules/react-native-webview" React-perflogger: :path: "../node_modules/react-native/ReactCommon/reactperflogger" React-RCTActionSheet: @@ -846,38 +841,38 @@ SPEC CHECKSUMS: EXJSONUtils: 48b1e764ac35160e6f54d21ab60d7d9501f3e473 EXManifests: 500666d48e8dd7ca5a482c9e729e4a7a6c34081b EXMediaLibrary: 587cd8aad27a6fc8d7c38b950bc75bc1845a7480 - Expo: 707f9b0039eacc6a1dce90c08c9e37b9c417bba2 + Expo: 863488a600a4565698a79577117c70b170054d08 expo-dev-client: 7c1ef51516853465f4d448c14ddf365167d20361 expo-dev-launcher: 90de99d9e5d1a883d81355ca10e87c2f3c81d46e - expo-dev-menu: d4369e74d8d21a0ccdee35f7c732e7118b0fee16 + expo-dev-menu: 4f54ef98df59d9d625677cb18ad4582de92b4a7d expo-dev-menu-interface: 6c82ae323c4b8724dead4763ce3ff24a2108bdb1 ExpoImagePicker: 270dea232b3a072d981dd564e2cafc63a864edb1 ExpoKeepAwake: 69f5f627670d62318410392d03e0b5db0f85759a ExpoLocalization: f26cd431ad9ea3533c5b08c4fabd879176a794bb - ExpoModulesCore: 397fc99e9d6c9dcc010f36d5802097c17b90424c + ExpoModulesCore: 6e0259511f4c4341b6b8357db393624df2280828 EXSplashScreen: cd7fb052dff5ba8311d5c2455ecbebffe1b7a8ca EXUpdatesInterface: dd699d1930e28639dcbd70a402caea98e86364ca - FBLazyVector: 60195509584153283780abdac5569feffb8f08cc - FBReactNativeSpec: 9c191fb58d06dc05ab5559a5505fc32139e9e4a2 + FBLazyVector: 446e84642979fff0ba57f3c804c2228a473aeac2 + FBReactNativeSpec: 241709e132e3bf1526c1c4f00bc5384dd39dfba9 fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b - hermes-engine: 38bfe887e456b33b697187570a08de33969f5db7 + hermes-engine: a1f157c49ea579c28b0296bda8530e980c45bdb3 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1 - RCTRequired: bec48f07daf7bcdc2655a0cde84e07d24d2a9e2a - RCTTypeSafety: 171394eebacf71e1cfad79dbfae7ee8fc16ca80a - React: d7433ccb6a8c36e4cbed59a73c0700fc83c3e98a - React-callinvoker: 15f165009bd22ae829b2b600e50bcc98076ce4b8 - React-Codegen: b5910000eaf1e0c2f47d29be6f82f5f1264420d7 - React-Core: b6f2f78d580a90b83fd7b0d1c6911c799f6eac82 - React-CoreModules: e0cbc1a4f4f3f60e23c476fef7ab37be363ea8c1 - React-cxxreact: c87f3f124b2117d00d410b35f16c2257e25e50fa - React-hermes: c64ca6bdf16a7069773103c9bedaf30ec90ab38f - React-jsi: 39729361645568e238081b3b3180fbad803f25a4 - React-jsiexecutor: 515b703d23ffadeac7687bc2d12fb08b90f0aaa1 - React-jsinspector: 9f7c9137605e72ca0343db4cea88006cb94856dd - React-logger: 957e5dc96d9dbffc6e0f15e0ee4d2b42829ff207 + RCTRequired: 5a024fdf458fa8c0d82fc262e76f982d4dcdecdd + RCTTypeSafety: b6c253064466411c6810b45f66bc1e43ce0c54ba + React: 715292db5bd46989419445a5547954b25d2090f0 + React-callinvoker: 105392d1179058585b564d35b4592fe1c46d6fba + React-Codegen: b75333b93d835afce84b73472927cccaef2c9f8c + React-Core: 88838ed1724c64905fc6c0811d752828a92e395b + React-CoreModules: cd238b4bb8dc8529ccc8b34ceae7267b04ce1882 + React-cxxreact: 291bfab79d8098dc5ebab98f62e6bdfe81b3955a + React-hermes: b1e67e9a81c71745704950516f40ee804349641c + React-jsi: c9d5b563a6af6bb57034a82c2b0d39d0a7483bdc + React-jsiexecutor: d6b7fa9260aa3cb40afee0507e3bc1d17ecaa6f2 + React-jsinspector: 1f51e775819199d3fe9410e69ee8d4c4161c7b06 + React-logger: 0d58569ec51d30d1792c5e86a8e3b78d24b582c6 react-native-blur: 50c9feabacbc5f49b61337ebc32192c6be7ec3c3 react-native-cameraroll: f3050460fe1708378698c16686bfaa5f34099be2 react-native-get-random-values: a6ea6a8a65dc93e96e24a11105b1a9c8cfe1d72a @@ -887,20 +882,19 @@ SPEC CHECKSUMS: react-native-safe-area-context: 39c2d8be3328df5d437ac1700f4f3a4f75716acc react-native-splash-screen: 4312f786b13a81b5169ef346d76d33bc0c6dc457 react-native-version-number: b415bbec6a13f2df62bf978e85bc0d699462f37f - react-native-webview: 994b9f8fbb504d6314dc40d83f94f27c6831b3bf - React-perflogger: af8a3d31546077f42d729b949925cc4549f14def - React-RCTActionSheet: 57cc5adfefbaaf0aae2cf7e10bccd746f2903673 - React-RCTAnimation: 11c61e94da700c4dc915cf134513764d87fc5e2b - React-RCTAppDelegate: c3980adeaadcfd6cb495532e928b36ac6db3c14a - React-RCTBlob: ccc5049d742b41971141415ca86b83b201495695 - React-RCTImage: 7a9226b0944f1e76e8e01e35a9245c2477cdbabb - React-RCTLinking: bbe8cc582046a9c04f79c235b73c93700263e8b4 - React-RCTNetwork: fc2ca322159dc54e06508d4f5c3e934da63dc013 - React-RCTSettings: f1e9db2cdf946426d3f2b210e4ff4ce0f0d842ef - React-RCTText: 1c41dd57e5d742b1396b4eeb251851ce7ff0fca1 - React-RCTVibration: 5199a180d04873366a83855de55ac33ce60fe4d5 - React-runtimeexecutor: 7bf0dafc7b727d93c8cb94eb00a9d3753c446c3e - ReactCommon: 6f65ea5b7d84deb9e386f670dd11ce499ded7b40 + React-perflogger: 0bb0522a12e058f6eb69d888bc16f40c16c4b907 + React-RCTActionSheet: bfd675a10f06a18728ea15d82082d48f228a213a + React-RCTAnimation: 2fa220b2052ec75b733112aca39143d34546a941 + React-RCTAppDelegate: 8564f93c1d9274e95e3b0c746d08a87ff5a621b2 + React-RCTBlob: d0336111f46301ae8aba2e161817e451aad72dd6 + React-RCTImage: fec592c46edb7c12a9cde08780bdb4a688416c62 + React-RCTLinking: 14eccac5d2a3b34b89dbfa29e8ef6219a153fe2d + React-RCTNetwork: 1fbce92e772e39ca3687a2ebb854501ff6226dd7 + React-RCTSettings: 1abea36c9bb16d9979df6c4b42e2ea281b4bbcc5 + React-RCTText: 15355c41561a9f43dfd23616d0a0dd40ba05ed61 + React-RCTVibration: ad17efcfb2fa8f6bfd8ac0cf48d96668b8b28e0b + React-runtimeexecutor: 8fa50b38df6b992c76537993a2b0553d3b088004 + ReactCommon: b49a4b00ca6d181ff74b17c12b2d59ac4add0bde rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba RNBackgroundFetch: 642777e4e76435773c149d565a043d66f1781237 RNCAsyncStorage: 09fc8595e6d6f6d5abf16b23a56b257d9c6b7c5b @@ -921,7 +915,7 @@ SPEC CHECKSUMS: sovran-react-native: fd3dc8f1a4b14acdc4ad25fc6b4ac4f52a2a2a15 Swime: d7b2c277503b6cea317774aedc2dce05613f8b0b TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863 - Yoga: 5ed1699acbba8863755998a4245daa200ff3817b + Yoga: 79dd7410de6f8ad73a77c868d3d368843f0c93e0 PODFILE CHECKSUM: 5570c7b7d6ce7895f95d9db8a3a99b136a3f42c4 diff --git a/ios/bluesky/Info.plist b/ios/bluesky/Info.plist index 117d763e5..f58ed1b55 100644 --- a/ios/bluesky/Info.plist +++ b/ios/bluesky/Info.plist @@ -21,7 +21,7 @@ <key>CFBundlePackageType</key> <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> <key>CFBundleShortVersionString</key> - <string>1.10</string> + <string>1.11</string> <key>CFBundleSignature</key> <string>????</string> <key>CFBundleURLTypes</key> diff --git a/jest/test-pds.ts b/jest/test-pds.ts index 32f3bc9b0..1e87df811 100644 --- a/jest/test-pds.ts +++ b/jest/test-pds.ts @@ -1,86 +1,73 @@ import {AddressInfo} from 'net' import os from 'os' +import net from 'net' 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 AtpAgent from '@atproto/api' +import {PDS, ServerConfig, Database, MemoryBlobStore} from '@atproto/pds' +import * as plc from '@did-plc/lib' +import {PlcServer, Database as PlcDatabase} from '@did-plc/server' +import {BskyAgent} from '@atproto/api' + +const ADMIN_PASSWORD = 'admin-pass' +const SECOND = 1000 +const MINUTE = SECOND * 60 +const HOUR = MINUTE * 60 export interface TestUser { email: string did: string - declarationCid: string handle: string password: string - agent: AtpAgent -} - -export interface TestUsers { - alice: TestUser - bob: TestUser - carla: TestUser + agent: BskyAgent } export interface TestPDS { pdsUrl: string - users: TestUsers + mocker: Mocker 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() + const repoSigningKey = await crypto.Secp256k1Keypair.create() + const plcRotationKey = await crypto.Secp256k1Keypair.create() + const port = await getPort() + + const plcDb = PlcDatabase.mock() - // run plc server - const plcDb = plc.Database.memory() - await plcDb.migrateToLatestOrThrow() - const plcServer = plc.PlcServer.create({db: plcDb}) + const plcServer = 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 recoveryKey = (await crypto.Secp256k1Keypair.create()).did() - const plcClient = new plc.PlcClient(plcUrl) - const serverDid = await plcClient.createDid( - keypair, - recoveryKey, - 'localhost', - 'https://pds.public.url', - ) + const plcClient = new plc.Client(plcUrl) + const serverDid = await plcClient.createDid({ + signingKey: repoSigningKey.did(), + rotationKeys: [recoveryKey, plcRotationKey.did()], + handle: 'localhost', + pds: `http://localhost:${port}`, + signer: plcRotationKey, + }) const blobstoreLoc = path.join(os.tmpdir(), crypto.randomStr(5, 'base32')) - const cfg = new PDSServerConfig({ + const cfg = new ServerConfig({ debugMode: true, version: '0.0.0', scheme: 'http', hostname: 'localhost', + port, serverDid, recoveryKey, - adminPassword: 'admin-pass', + adminPassword: ADMIN_PASSWORD, inviteRequired: false, didPlcUrl: plcUrl, jwtSecret: 'jwt-secret', availableUserDomains: ['.test'], appUrlPasswordReset: 'app://forgot-password', emailNoReplyAddress: 'noreply@blueskyweb.xyz', - publicUrl: 'https://pds.public.url', + publicUrl: `http://localhost:${port}`, imgUriSalt: '9dd04221f5755bce5f55f47464c27e1e', imgUriKey: 'f23ecd142835025f42c3db2cf25dd813956c178392760256211f9d315f8ab4d8', @@ -88,22 +75,33 @@ export async function createServer(): Promise<TestPDS> { blobstoreLocation: `${blobstoreLoc}/blobs`, blobstoreTmp: `${blobstoreLoc}/tmp`, maxSubscriptionBuffer: 200, - repoBackfillLimitMs: 1e3 * 60 * 60, + repoBackfillLimitMs: HOUR, }) - const db = PDSDatabase.memory() + const db = + cfg.dbPostgresUrl !== undefined + ? Database.postgres({ + url: cfg.dbPostgresUrl, + schema: cfg.dbPostgresSchema, + }) + : Database.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) + const pds = PDS.create({ + db, + blobstore, + repoSigningKey, + plcRotationKey, + config: cfg, + }) + await pds.start() + const pdsUrl = `http://localhost:${port}` return { pdsUrl, - users: testUsers, + mocker: new Mocker(pdsUrl), async close() { await pds.destroy() await plcServer.destroy() @@ -111,90 +109,93 @@ export async function createServer(): Promise<TestPDS> { } } -async function genMockData(pdsUrl: string): Promise<TestUsers> { - const date = dateGen() +class Mocker { + agent: BskyAgent + users: Record<string, TestUser> = {} - const agents = { - loggedout: new AtpAgent({service: pdsUrl}), - alice: new AtpAgent({service: pdsUrl}), - bob: new AtpAgent({service: pdsUrl}), - carla: new AtpAgent({service: pdsUrl}), + constructor(public service: string) { + this.agent = new BskyAgent({service}) } - const users: TestUser[] = [ - { - email: 'alice@test.com', - did: '', - declarationCid: '', - handle: 'alice.test', - password: 'hunter2', - agent: agents.alice, - }, - { - email: 'bob@test.com', - did: '', - declarationCid: '', - handle: 'bob.test', - password: 'hunter2', - agent: agents.bob, - }, - { - email: 'carla@test.com', - did: '', - declarationCid: '', - handle: 'carla.test', + + // NOTE + // deterministic date generator + // we use this to ensure the mock dataset is always the same + // which is very useful when testing + *dateGen() { + let start = 1657846031914 + while (true) { + yield new Date(start).toISOString() + start += 1e3 + } + } + + async createUser(name: string) { + const agent = new BskyAgent({service: this.agent.service}) + const email = `fake${Object.keys(this.users).length + 1}@fake.com` + const res = await agent.createAccount({ + email, + handle: name + '.test', password: 'hunter2', - agent: agents.carla, - }, - ] - const alice = users[0] - const bob = users[1] - const carla = users[2] - - let _i = 1 - for (const user of users) { - const res = await agents.loggedout.api.com.atproto.account.create({ - email: user.email, - handle: user.handle, - password: user.password, - }) - user.agent.api.setHeader('Authorization', `Bearer ${res.data.accessJwt}`) - const {data: profile} = await user.agent.api.app.bsky.actor.getProfile({ - actor: user.handle, }) - user.did = res.data.did - user.declarationCid = profile.declaration.cid - await user.agent.api.app.bsky.actor.profile.create( - {did: user.did}, - { - displayName: ucfirst(user.handle).slice(0, -5), - description: `Test user ${_i++}`, - }, - ) + this.users[name] = { + did: res.data.did, + email, + handle: name + '.test', + password: 'hunter2', + agent: agent, + } } - // everybody follows everybody - const follow = async (author: TestUser, subject: TestUser) => { - await author.agent.api.app.bsky.graph.follow.create( - {did: author.did}, - { - subject: { - did: subject.did, - declarationCid: subject.declarationCid, - }, - createdAt: date.next().value || '', - }, - ) + async follow(a: string, b: string) { + await this.users[a].agent.follow(this.users[b].did) + } + + async generateStandardGraph() { + await this.createUser('alice') + await this.createUser('bob') + await this.createUser('carla') + + await this.users.alice.agent.upsertProfile(() => ({ + displayName: 'Alice', + description: 'Test user 1', + })) + + await this.users.bob.agent.upsertProfile(() => ({ + displayName: 'Bob', + description: 'Test user 2', + })) + + await this.users.carla.agent.upsertProfile(() => ({ + displayName: 'Carla', + description: 'Test user 3', + })) + + await this.follow('alice', 'bob') + await this.follow('alice', 'carla') + await this.follow('bob', 'alice') + await this.follow('bob', 'carla') + await this.follow('carla', 'alice') + await this.follow('carla', 'bob') } - 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) +const checkAvailablePort = (port: number) => + new Promise(resolve => { + const server = net.createServer() + server.unref() + server.on('error', () => resolve(false)) + server.listen({port}, () => { + server.close(() => { + resolve(true) + }) + }) + }) + +async function getPort() { + for (let i = 3000; i < 65000; i++) { + if (await checkAvailablePort(i)) { + return i + } + } + throw new Error('Unable to find an available port') } diff --git a/metro.config.js b/metro.config.js index 9e8e8745a..b1714479f 100644 --- a/metro.config.js +++ b/metro.config.js @@ -1,3 +1,7 @@ // Learn more https://docs.expo.io/guides/customizing-metro const {getDefaultConfig} = require('expo/metro-config') -module.exports = getDefaultConfig(__dirname) +const cfg = getDefaultConfig(__dirname) +cfg.resolver.sourceExts = process.env.RN_SRC_EXT + ? process.env.RN_SRC_EXT.split(',').concat(cfg.resolver.sourceExts) + : cfg.resolver.sourceExts +module.exports = cfg diff --git a/package.json b/package.json index 8f2f7e9db..2ac5367a4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bsky.app", - "version": "1.10.0", + "version": "1.11.0", "private": true, "scripts": { "postinstall": "patch-package", @@ -15,12 +15,13 @@ "test-ci": "jest --ci --forceExit --reporters=default --reporters=jest-junit", "test-coverage": "jest --coverage", "lint": "eslint ./src --ext .js,.jsx,.ts,.tsx", - "e2e": "detox test --configuration ios.sim.debug --take-screenshots all" + "e2e:mock-server": "ts-node __e2e__/mock-server.ts", + "e2e:metro": "RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios", + "e2e:build": "detox build -c ios.sim.debug", + "e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all" }, "dependencies": { - "@atproto/api": "0.1.3", - "@atproto/lexicon": "^0.0.4", - "@atproto/xrpc": "^0.0.4", + "@atproto/api": "0.2.0", "@bam.tech/react-native-image-resizer": "^3.0.4", "@expo/webpack-config": "^18.0.1", "@fortawesome/fontawesome-svg-core": "^6.1.1", @@ -55,7 +56,7 @@ "await-lock": "^2.2.2", "base64-js": "^1.5.1", "email-validator": "^2.0.4", - "expo": "~48.0.0-beta.2", + "expo": "~48.0.9", "expo-camera": "~13.2.1", "expo-dev-client": "~2.1.1", "expo-image-picker": "~14.1.1", @@ -63,6 +64,8 @@ "expo-media-library": "~15.2.3", "expo-splash-screen": "~0.18.1", "expo-status-bar": "~1.4.4", + "fast-text-encoding": "^1.0.6", + "graphemer": "^1.4.0", "he": "^1.2.0", "history": "^5.3.0", "js-sha256": "^0.9.0", @@ -84,7 +87,7 @@ "react-avatar-editor": "^13.0.0", "react-circular-progressbar": "^2.1.0", "react-dom": "^18.2.0", - "react-native": "0.71.3", + "react-native": "0.71.4", "react-native-appstate-hook": "^1.0.6", "react-native-background-fetch": "^4.1.8", "react-native-drawer-layout": "^3.2.0", @@ -109,19 +112,17 @@ "react-native-version-number": "^0.3.6", "react-native-web": "^0.18.11", "react-native-web-linear-gradient": "^1.1.2", - "react-native-web-webview": "^1.0.2", - "react-native-webview": "11.26.0", - "react-native-youtube-iframe": "^2.2.2", "rn-fetch-blob": "^0.12.0", "tippy.js": "^6.3.7", "tlds": "^1.234.0", "zod": "^3.20.2" }, "devDependencies": { - "@atproto/pds": "^0.0.3", + "@atproto/pds": "^0.1.0", "@babel/core": "^7.20.0", "@babel/preset-env": "^7.20.0", "@babel/runtime": "^7.20.0", + "@did-plc/server": "^0.0.1", "@react-native-community/eslint-config": "^3.0.0", "@testing-library/jest-native": "^5.4.1", "@testing-library/react-native": "^11.5.2", @@ -150,13 +151,14 @@ "eslint-plugin-ft-flow": "^2.0.3", "html-webpack-plugin": "^5.5.0", "jest": "^29.4.3", - "jest-expo": "^48.0.0-beta.2", + "jest-expo": "^48.0.2", "jest-junit": "^15.0.0", "metro-react-native-babel-preset": "^0.73.7", "prettier": "^2.8.3", "react-native-dotenv": "^3.3.1", "react-scripts": "^5.0.1", "react-test-renderer": "18.2.0", + "ts-node": "^10.9.1", "typescript": "^4.4.4", "url-loader": "^4.1.1", "webpack": "^5.75.0", diff --git a/src/App.native.tsx b/src/App.native.tsx index ebe6a7cd6..0adbae606 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -29,7 +29,6 @@ const App = observer(() => { analytics.init(store) notifee.init(store) SplashScreen.hide() - store.hackCheckIfUpgradeNeeded() Linking.getInitialURL().then((url: string | null) => { if (url) { handleLink(url) diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 2bfc84ea9..a1dbc4af1 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -31,7 +31,7 @@ import {ProfileScreen} from './view/screens/Profile' import {ProfileFollowersScreen} from './view/screens/ProfileFollowers' import {ProfileFollowsScreen} from './view/screens/ProfileFollows' import {PostThreadScreen} from './view/screens/PostThread' -import {PostUpvotedByScreen} from './view/screens/PostUpvotedBy' +import {PostLikedByScreen} from './view/screens/PostLikedBy' import {PostRepostedByScreen} from './view/screens/PostRepostedBy' import {DebugScreen} from './view/screens/Debug' import {LogScreen} from './view/screens/Log' @@ -62,7 +62,7 @@ function commonScreens(Stack: typeof HomeTab) { /> <Stack.Screen name="ProfileFollows" component={ProfileFollowsScreen} /> <Stack.Screen name="PostThread" component={PostThreadScreen} /> - <Stack.Screen name="PostUpvotedBy" component={PostUpvotedByScreen} /> + <Stack.Screen name="PostLikedBy" component={PostLikedByScreen} /> <Stack.Screen name="PostRepostedBy" component={PostRepostedByScreen} /> <Stack.Screen name="Debug" component={DebugScreen} /> <Stack.Screen name="Log" component={LogScreen} /> diff --git a/src/lib/api/api-polyfill.ts b/src/lib/api/api-polyfill.ts index b7be6913a..7c38625a2 100644 --- a/src/lib/api/api-polyfill.ts +++ b/src/lib/api/api-polyfill.ts @@ -1,11 +1,11 @@ -import AtpAgent from '@atproto/api' +import {BskyAgent, stringifyLex, jsonToLex} from '@atproto/api' import RNFS from 'react-native-fs' const GET_TIMEOUT = 15e3 // 15s const POST_TIMEOUT = 60e3 // 60s export function doPolyfill() { - AtpAgent.configure({fetch: fetchHandler}) + BskyAgent.configure({fetch: fetchHandler}) } interface FetchHandlerResponse { @@ -22,7 +22,7 @@ async function fetchHandler( ): Promise<FetchHandlerResponse> { const reqMimeType = reqHeaders['Content-Type'] || reqHeaders['content-type'] if (reqMimeType && reqMimeType.startsWith('application/json')) { - reqBody = JSON.stringify(reqBody) + reqBody = stringifyLex(reqBody) } else if ( typeof reqBody === 'string' && (reqBody.startsWith('/') || reqBody.startsWith('file:')) @@ -65,7 +65,7 @@ async function fetchHandler( let resBody if (resMimeType) { if (resMimeType.startsWith('application/json')) { - resBody = await res.json() + resBody = jsonToLex(await res.json()) } else if (resMimeType.startsWith('text/')) { resBody = await res.text() } else { diff --git a/src/lib/api/api-polyfill.web.ts b/src/lib/api/api-polyfill.web.ts index 1469cf905..1ad22b3d0 100644 --- a/src/lib/api/api-polyfill.web.ts +++ b/src/lib/api/api-polyfill.web.ts @@ -1,4 +1,3 @@ export function doPolyfill() { - // TODO needed? native fetch may work fine -prf - // AtpApi.xrpc.fetch = fetchHandler + // no polyfill is needed on web } diff --git a/src/lib/api/build-suggested-posts.ts b/src/lib/api/build-suggested-posts.ts index defa45311..b9feefc72 100644 --- a/src/lib/api/build-suggested-posts.ts +++ b/src/lib/api/build-suggested-posts.ts @@ -1,9 +1,9 @@ import {RootStoreModel} from 'state/index' import { - AppBskyFeedFeedViewPost, + AppBskyFeedDefs, AppBskyFeedGetAuthorFeed as GetAuthorFeed, } from '@atproto/api' -type ReasonRepost = AppBskyFeedFeedViewPost.ReasonRepost +type ReasonRepost = AppBskyFeedDefs.ReasonRepost async function getMultipleAuthorsPosts( rootStore: RootStoreModel, @@ -12,12 +12,12 @@ async function getMultipleAuthorsPosts( limit: number = 10, ) { const responses = await Promise.all( - authors.map((author, index) => - rootStore.api.app.bsky.feed + authors.map((actor, index) => + rootStore.agent .getAuthorFeed({ - author, + actor, limit, - before: cursor ? cursor.split(',')[index] : undefined, + cursor: cursor ? cursor.split(',')[index] : undefined, }) .catch(_err => ({success: false, headers: {}, data: {feed: []}})), ), @@ -29,14 +29,14 @@ function mergePosts( responses: GetAuthorFeed.Response[], {repostsOnly, bestOfOnly}: {repostsOnly?: boolean; bestOfOnly?: boolean}, ) { - let posts: AppBskyFeedFeedViewPost.Main[] = [] + let posts: AppBskyFeedDefs.FeedViewPost[] = [] if (bestOfOnly) { for (const res of responses) { if (res.success) { - // filter the feed down to the post with the most upvotes + // filter the feed down to the post with the most likes res.data.feed = res.data.feed.reduce( - (acc: AppBskyFeedFeedViewPost.Main[], v) => { + (acc: AppBskyFeedDefs.FeedViewPost[], v) => { if ( !acc?.[0] && !v.reason && @@ -49,7 +49,7 @@ function mergePosts( acc && !v.reason && !v.reply && - v.post.upvoteCount > acc[0]?.post.upvoteCount && + (v.post.likeCount || 0) > (acc[0]?.post.likeCount || 0) && isRecentEnough(v.post.indexedAt) ) { return [v] @@ -92,7 +92,7 @@ function mergePosts( return posts } -function isARepostOfSomeoneElse(post: AppBskyFeedFeedViewPost.Main): boolean { +function isARepostOfSomeoneElse(post: AppBskyFeedDefs.FeedViewPost): boolean { return ( post.reason?.$type === 'app.bsky.feed.feedViewPost#reasonRepost' && post.post.author.did !== (post.reason as ReasonRepost).by.did diff --git a/src/lib/api/feed-manip.ts b/src/lib/api/feed-manip.ts index e9a32b7a6..6fdc9a48f 100644 --- a/src/lib/api/feed-manip.ts +++ b/src/lib/api/feed-manip.ts @@ -1,8 +1,8 @@ -import {AppBskyFeedFeedViewPost} from '@atproto/api' +import {AppBskyFeedDefs} from '@atproto/api' import lande from 'lande' -type FeedViewPost = AppBskyFeedFeedViewPost.Main -import {hasProp} from '@atproto/lexicon' +import {hasProp} from 'lib/type-guards' import {LANGUAGES_MAP_CODE2} from '../../locale/languages' +type FeedViewPost = AppBskyFeedDefs.FeedViewPost export type FeedTunerFn = ( tuner: FeedTuner, @@ -174,7 +174,7 @@ export class FeedTuner { } const item = slices[i].rootItem const isRepost = Boolean(item.reason) - if (!isRepost && item.post.upvoteCount < 2) { + if (!isRepost && (item.post.likeCount || 0) < 2) { slices.splice(i, 1) } } diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 85eca4a61..a5aa916df 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -1,16 +1,16 @@ import { AppBskyEmbedImages, AppBskyEmbedExternal, - ComAtprotoBlobUpload, AppBskyEmbedRecord, + AppBskyEmbedRecordWithMedia, + ComAtprotoRepoUploadBlob, + RichText, } from '@atproto/api' import {AtUri} from '../../third-party/uri' import {RootStoreModel} from 'state/models/root-store' -import {extractEntities} from 'lib/strings/rich-text-detection' import {isNetworkError} from 'lib/strings/errors' import {LinkMeta} from '../link-meta/link-meta' import {Image} from '../media/manip' -import {RichText} from '../strings/rich-text' import {isWeb} from 'platform/detection' export interface ExternalEmbedDraft { @@ -27,7 +27,7 @@ export async function resolveName(store: RootStoreModel, didOrHandle: string) { if (didOrHandle.startsWith('did:')) { return didOrHandle } - const res = await store.api.com.atproto.handle.resolve({ + const res = await store.agent.resolveHandle({ handle: didOrHandle, }) return res.data.did @@ -37,15 +37,15 @@ export async function uploadBlob( store: RootStoreModel, blob: string, encoding: string, -): Promise<ComAtprotoBlobUpload.Response> { +): Promise<ComAtprotoRepoUploadBlob.Response> { if (isWeb) { // `blob` should be a data uri - return store.api.com.atproto.blob.upload(convertDataURIToUint8Array(blob), { + return store.agent.uploadBlob(convertDataURIToUint8Array(blob), { encoding, }) } else { // `blob` should be a path to a file in the local FS - return store.api.com.atproto.blob.upload( + return store.agent.uploadBlob( blob, // this will be special-cased by the fetch monkeypatch in /src/state/lib/api.ts {encoding}, ) @@ -70,22 +70,18 @@ export async function post(store: RootStoreModel, opts: PostOpts) { | AppBskyEmbedImages.Main | AppBskyEmbedExternal.Main | AppBskyEmbedRecord.Main + | AppBskyEmbedRecordWithMedia.Main | undefined let reply - const text = new RichText(opts.rawText, undefined, { - cleanNewlines: true, - }).text.trim() + const rt = new RichText( + {text: opts.rawText.trim()}, + { + cleanNewlines: true, + }, + ) opts.onStateChange?.('Processing...') - const entities = extractEntities(text, opts.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 - } - } - } + await rt.detectFacets(store.agent) if (opts.quote) { embed = { @@ -95,24 +91,37 @@ export async function post(store: RootStoreModel, opts: PostOpts) { cid: opts.quote.cid, }, } as AppBskyEmbedRecord.Main - } else if (opts.images?.length) { - embed = { - $type: 'app.bsky.embed.images', - images: [], - } as AppBskyEmbedImages.Main - let i = 1 + } + + if (opts.images?.length) { + const images: AppBskyEmbedImages.Image[] = [] for (const image of opts.images) { - opts.onStateChange?.(`Uploading image #${i++}...`) + opts.onStateChange?.(`Uploading image #${images.length + 1}...`) const res = await uploadBlob(store, image, 'image/jpeg') - embed.images.push({ - image: { - cid: res.data.cid, - mimeType: 'image/jpeg', - }, + images.push({ + image: res.data.blob, alt: '', // TODO supply alt text }) } - } else if (opts.extLink) { + + if (opts.quote) { + embed = { + $type: 'app.bsky.embed.recordWithMedia', + record: embed, + media: { + $type: 'app.bsky.embed.images', + images, + }, + } as AppBskyEmbedRecordWithMedia.Main + } else { + embed = { + $type: 'app.bsky.embed.images', + images, + } as AppBskyEmbedImages.Main + } + } + + if (opts.extLink && !opts.images?.length) { let thumb if (opts.extLink.localThumb) { opts.onStateChange?.('Uploading link thumbnail...') @@ -138,27 +147,41 @@ export async function post(store: RootStoreModel, opts: PostOpts) { opts.extLink.localThumb.path, encoding, ) - thumb = { - cid: thumbUploadRes.data.cid, - mimeType: encoding, - } + thumb = thumbUploadRes.data.blob } } - embed = { - $type: 'app.bsky.embed.external', - external: { - uri: opts.extLink.uri, - title: opts.extLink.meta?.title || '', - description: opts.extLink.meta?.description || '', - thumb, - }, - } as AppBskyEmbedExternal.Main + + if (opts.quote) { + embed = { + $type: 'app.bsky.embed.recordWithMedia', + record: embed, + media: { + $type: 'app.bsky.embed.external', + external: { + uri: opts.extLink.uri, + title: opts.extLink.meta?.title || '', + description: opts.extLink.meta?.description || '', + thumb, + }, + } as AppBskyEmbedExternal.Main, + } as AppBskyEmbedRecordWithMedia.Main + } else { + embed = { + $type: 'app.bsky.embed.external', + external: { + uri: opts.extLink.uri, + title: opts.extLink.meta?.title || '', + description: opts.extLink.meta?.description || '', + thumb, + }, + } as AppBskyEmbedExternal.Main + } } if (opts.replyTo) { const replyToUrip = new AtUri(opts.replyTo) - const parentPost = await store.api.app.bsky.feed.post.get({ - user: replyToUrip.host, + const parentPost = await store.agent.getPost({ + repo: replyToUrip.host, rkey: replyToUrip.rkey, }) if (parentPost) { @@ -175,16 +198,12 @@ export async function post(store: RootStoreModel, opts: PostOpts) { try { opts.onStateChange?.('Posting...') - return await store.api.app.bsky.feed.post.create( - {did: store.me.did || ''}, - { - text, - reply, - embed, - entities, - createdAt: new Date().toISOString(), - }, - ) + return await store.agent.post({ + text: rt.text, + facets: rt.facets, + reply, + embed, + }) } catch (e: any) { console.error(`Failed to create post: ${e.toString()}`) if (isNetworkError(e)) { @@ -197,49 +216,6 @@ export async function post(store: RootStoreModel, opts: PostOpts) { } } -export async function repost(store: RootStoreModel, uri: string, cid: string) { - return await store.api.app.bsky.feed.repost.create( - {did: store.me.did || ''}, - { - subject: {uri, cid}, - createdAt: new Date().toISOString(), - }, - ) -} - -export async function unrepost(store: RootStoreModel, repostUri: string) { - const repostUrip = new AtUri(repostUri) - return await store.api.app.bsky.feed.repost.delete({ - did: repostUrip.hostname, - rkey: repostUrip.rkey, - }) -} - -export async function follow( - store: RootStoreModel, - subjectDid: string, - subjectDeclarationCid: string, -) { - return await store.api.app.bsky.graph.follow.create( - {did: store.me.did || ''}, - { - subject: { - did: subjectDid, - declarationCid: subjectDeclarationCid, - }, - createdAt: new Date().toISOString(), - }, - ) -} - -export async function unfollow(store: RootStoreModel, followUri: string) { - const followUrip = new AtUri(followUri) - return await store.api.app.bsky.graph.follow.delete({ - did: followUrip.hostname, - rkey: followUrip.rkey, - }) -} - // helpers // = diff --git a/src/lib/media/picker.e2e.tsx b/src/lib/media/picker.e2e.tsx new file mode 100644 index 000000000..9f4765ac2 --- /dev/null +++ b/src/lib/media/picker.e2e.tsx @@ -0,0 +1,116 @@ +import {RootStoreModel} from 'state/index' +import {PickerOpts, CameraOpts, CropperOpts, PickedMedia} from './types' +import { + scaleDownDimensions, + Dim, + compressIfNeeded, + moveToPremanantPath, +} from 'lib/media/manip' +export type {PickedMedia} from './types' +import RNFS from 'react-native-fs' + +let _imageCounter = 0 +async function getFile() { + const files = await RNFS.readDir( + RNFS.LibraryDirectoryPath.split('/') + .slice(0, -5) + .concat(['Media', 'DCIM', '100APPLE']) + .join('/'), + ) + return files[_imageCounter++ % files.length] +} + +export async function openPicker( + _store: RootStoreModel, + opts: PickerOpts, +): Promise<PickedMedia[]> { + const mediaType = opts.mediaType || 'photo' + const items = await getFile() + const toMedia = (item: RNFS.ReadDirItem) => ({ + mediaType, + path: item.path, + mime: 'image/jpeg', + size: item.size, + width: 4288, + height: 2848, + }) + if (Array.isArray(items)) { + return items.map(toMedia) + } + return [toMedia(items)] +} + +export async function openCamera( + _store: RootStoreModel, + opts: CameraOpts, +): Promise<PickedMedia> { + const mediaType = opts.mediaType || 'photo' + const item = await getFile() + return { + mediaType, + path: item.path, + mime: 'image/jpeg', + size: item.size, + width: 4288, + height: 2848, + } +} + +export async function openCropper( + _store: RootStoreModel, + opts: CropperOpts, +): Promise<PickedMedia> { + const mediaType = opts.mediaType || 'photo' + const item = await getFile() + return { + mediaType, + path: item.path, + mime: 'image/jpeg', + size: item.size, + width: 4288, + height: 2848, + } +} + +export async function pickImagesFlow( + store: RootStoreModel, + maxFiles: number, + maxDim: Dim, + maxSize: number, +) { + const items = await openPicker(store, { + multiple: true, + maxFiles, + mediaType: 'photo', + }) + const result = [] + for (const image of items) { + result.push( + await cropAndCompressFlow(store, image.path, image, maxDim, maxSize), + ) + } + return result +} + +export async function cropAndCompressFlow( + store: RootStoreModel, + path: string, + imgDim: Dim, + maxDim: Dim, + maxSize: number, +) { + // choose target dimensions based on the original + // this causes the photo cropper to start with the full image "selected" + const {width, height} = scaleDownDimensions(imgDim, maxDim) + const cropperRes = await openCropper(store, { + mediaType: 'photo', + path, + freeStyleCropEnabled: true, + width, + height, + }) + + const img = await compressIfNeeded(cropperRes, maxSize) + const permanentPath = await moveToPremanantPath(img.path) + return permanentPath +} diff --git a/src/lib/notifee.ts b/src/lib/notifee.ts index 4baf64050..4b53ed724 100644 --- a/src/lib/notifee.ts +++ b/src/lib/notifee.ts @@ -45,7 +45,7 @@ export function displayNotificationFromModel( let author = notif.author.displayName || notif.author.handle let title: string let body: string = '' - if (notif.isUpvote) { + if (notif.isLike) { title = `${author} liked your post` body = notif.additionalPost?.thread?.postRecord?.text || '' } else if (notif.isRepost) { @@ -65,7 +65,7 @@ export function displayNotificationFromModel( } let image if ( - AppBskyEmbedImages.isPresented(notif.additionalPost?.thread?.post.embed) && + AppBskyEmbedImages.isView(notif.additionalPost?.thread?.post.embed) && notif.additionalPost?.thread?.post.embed.images[0]?.thumb ) { image = notif.additionalPost.thread.post.embed.images[0].thumb diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index cc48e2dbe..59d94efa8 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -10,7 +10,7 @@ export type CommonNavigatorParams = { ProfileFollowers: {name: string} ProfileFollows: {name: string} PostThread: {name: string; rkey: string} - PostUpvotedBy: {name: string; rkey: string} + PostLikedBy: {name: string; rkey: string} PostRepostedBy: {name: string; rkey: string} Debug: undefined Log: undefined diff --git a/src/lib/strings/rich-text-detection.ts b/src/lib/strings/rich-text-detection.ts index 386ed48e1..51d09ec5d 100644 --- a/src/lib/strings/rich-text-detection.ts +++ b/src/lib/strings/rich-text-detection.ts @@ -1,64 +1,5 @@ -import {AppBskyFeedPost} from '@atproto/api' -type Entity = AppBskyFeedPost.Entity import {isValidDomain} from './url-helpers' -export function extractEntities( - text: string, - knownHandles?: Set<string>, -): Entity[] | undefined { - let match - let ents: Entity[] = [] - { - // mentions - const re = /(^|\s|\()(@)([a-zA-Z0-9.-]+)(\b)/g - while ((match = re.exec(text))) { - if (knownHandles && !knownHandles.has(match[3])) { - continue // not a known handle - } else if (!match[3].includes('.')) { - continue // probably not a handle - } - const start = text.indexOf(match[3], match.index) - 1 - ents.push({ - type: 'mention', - value: match[3], - index: {start, end: start + match[3].length + 1}, - }) - } - } - { - // links - const re = - /(^|\s|\()((https?:\/\/[\S]+)|((?<domain>[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*))/gim - while ((match = re.exec(text))) { - let value = match[2] - if (!value.startsWith('http')) { - const domain = match.groups?.domain - if (!domain || !isValidDomain(domain)) { - continue - } - 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-- - } - if (/[)]$/.test(value) && !value.includes('(')) { - value = value.slice(0, -1) - index.end-- - } - ents.push({ - type: 'link', - value, - index, - }) - } - } - return ents.length > 0 ? ents : undefined -} - interface DetectedLink { link: string } diff --git a/src/lib/strings/rich-text-sanitize.ts b/src/lib/strings/rich-text-sanitize.ts deleted file mode 100644 index 0b5895707..000000000 --- a/src/lib/strings/rich-text-sanitize.ts +++ /dev/null @@ -1,32 +0,0 @@ -import {RichText} from './rich-text' - -const EXCESS_SPACE_RE = /[\r\n]([\u00AD\u2060\u200D\u200C\u200B\s]*[\r\n]){2,}/ -const REPLACEMENT_STR = '\n\n' - -export function removeExcessNewlines(richText: RichText): RichText { - return clean(richText, EXCESS_SPACE_RE, REPLACEMENT_STR) -} - -// TODO: check on whether this works correctly with multi-byte codepoints -export function clean( - richText: RichText, - targetRegexp: RegExp, - replacementString: string, -): RichText { - richText = richText.clone() - - let match = richText.text.match(targetRegexp) - while (match && typeof match.index !== 'undefined') { - const oldText = richText.text - const removeStartIndex = match.index - const removeEndIndex = removeStartIndex + match[0].length - richText.delete(removeStartIndex, removeEndIndex) - if (richText.text === oldText) { - break // sanity check - } - richText.insert(removeStartIndex, replacementString) - match = richText.text.match(targetRegexp) - } - - return richText -} diff --git a/src/lib/strings/rich-text.ts b/src/lib/strings/rich-text.ts deleted file mode 100644 index 1df2144e0..000000000 --- a/src/lib/strings/rich-text.ts +++ /dev/null @@ -1,216 +0,0 @@ -/* -= Rich Text Manipulation - -When we sanitize rich text, we have to update the entity indices as the -text is modified. This can be modeled as inserts() and deletes() of the -rich text string. The possible scenarios are outlined below, along with -their expected behaviors. - -NOTE: Slices are start inclusive, end exclusive - -== richTextInsert() - -Target string: - - 0 1 2 3 4 5 6 7 8 910 // string indices - h e l l o w o r l d // string value - ^-------^ // target slice {start: 2, end: 7} - -Scenarios: - -A: ^ // insert "test" at 0 -B: ^ // insert "test" at 4 -C: ^ // insert "test" at 8 - -A = before -> move both by num added -B = inner -> move end by num added -C = after -> noop - -Results: - -A: 0 1 2 3 4 5 6 7 8 910 // string indices - t e s t h e l l o w // string value - ^-------^ // target slice {start: 6, end: 11} - -B: 0 1 2 3 4 5 6 7 8 910 // string indices - h e l l t e s t o w // string value - ^---------------^ // target slice {start: 2, end: 11} - -C: 0 1 2 3 4 5 6 7 8 910 // string indices - h e l l o w o t e s // string value - ^-------^ // target slice {start: 2, end: 7} - -== richTextDelete() - -Target string: - - 0 1 2 3 4 5 6 7 8 910 // string indices - h e l l o w o r l d // string value - ^-------^ // target slice {start: 2, end: 7} - -Scenarios: - -A: ^---------------^ // remove slice {start: 0, end: 9} -B: ^-----^ // remove slice {start: 7, end: 11} -C: ^-----------^ // remove slice {start: 4, end: 11} -D: ^-^ // remove slice {start: 3, end: 5} -E: ^-----^ // remove slice {start: 1, end: 5} -F: ^-^ // remove slice {start: 0, end: 2} - -A = entirely outer -> delete slice -B = entirely after -> noop -C = partially after -> move end to remove-start -D = entirely inner -> move end by num removed -E = partially before -> move start to remove-start index, move end by num removed -F = entirely before -> move both by num removed - -Results: - -A: 0 1 2 3 4 5 6 7 8 910 // string indices - l d // string value - // target slice (deleted) - -B: 0 1 2 3 4 5 6 7 8 910 // string indices - h e l l o w // string value - ^-------^ // target slice {start: 2, end: 7} - -C: 0 1 2 3 4 5 6 7 8 910 // string indices - h e l l // string value - ^-^ // target slice {start: 2, end: 4} - -D: 0 1 2 3 4 5 6 7 8 910 // string indices - h e l w o r l d // string value - ^---^ // target slice {start: 2, end: 5} - -E: 0 1 2 3 4 5 6 7 8 910 // string indices - h w o r l d // string value - ^-^ // target slice {start: 1, end: 3} - -F: 0 1 2 3 4 5 6 7 8 910 // string indices - l l o w o r l d // string value - ^-------^ // target slice {start: 0, end: 5} - */ - -import cloneDeep from 'lodash.clonedeep' -import {AppBskyFeedPost} from '@atproto/api' -import {removeExcessNewlines} from './rich-text-sanitize' - -export type Entity = AppBskyFeedPost.Entity -export interface RichTextOpts { - cleanNewlines?: boolean -} - -export class RichText { - constructor( - public text: string, - public entities?: Entity[], - opts?: RichTextOpts, - ) { - if (opts?.cleanNewlines) { - removeExcessNewlines(this).copyInto(this) - } - } - - clone() { - return new RichText(this.text, cloneDeep(this.entities)) - } - - copyInto(target: RichText) { - target.text = this.text - target.entities = cloneDeep(this.entities) - } - - insert(insertIndex: number, insertText: string) { - this.text = - this.text.slice(0, insertIndex) + - insertText + - this.text.slice(insertIndex) - - if (!this.entities?.length) { - return this - } - - const numCharsAdded = insertText.length - for (const ent of this.entities) { - // see comment at top of file for labels of each scenario - // scenario A (before) - if (insertIndex <= ent.index.start) { - // move both by num added - ent.index.start += numCharsAdded - ent.index.end += numCharsAdded - } - // scenario B (inner) - else if (insertIndex >= ent.index.start && insertIndex < ent.index.end) { - // move end by num added - ent.index.end += numCharsAdded - } - // scenario C (after) - // noop - } - return this - } - - delete(removeStartIndex: number, removeEndIndex: number) { - this.text = - this.text.slice(0, removeStartIndex) + this.text.slice(removeEndIndex) - - if (!this.entities?.length) { - return this - } - - const numCharsRemoved = removeEndIndex - removeStartIndex - for (const ent of this.entities) { - // see comment at top of file for labels of each scenario - // scenario A (entirely outer) - if ( - removeStartIndex <= ent.index.start && - removeEndIndex >= ent.index.end - ) { - // delete slice (will get removed in final pass) - ent.index.start = 0 - ent.index.end = 0 - } - // scenario B (entirely after) - else if (removeStartIndex > ent.index.end) { - // noop - } - // scenario C (partially after) - else if ( - removeStartIndex > ent.index.start && - removeStartIndex <= ent.index.end && - removeEndIndex > ent.index.end - ) { - // move end to remove start - ent.index.end = removeStartIndex - } - // scenario D (entirely inner) - else if ( - removeStartIndex >= ent.index.start && - removeEndIndex <= ent.index.end - ) { - // move end by num removed - ent.index.end -= numCharsRemoved - } - // scenario E (partially before) - else if ( - removeStartIndex < ent.index.start && - removeEndIndex >= ent.index.start && - removeEndIndex <= ent.index.end - ) { - // move start to remove-start index, move end by num removed - ent.index.start = removeStartIndex - ent.index.end -= numCharsRemoved - } - // scenario F (entirely before) - else if (removeEndIndex < ent.index.start) { - // move both by num removed - ent.index.start -= numCharsRemoved - ent.index.end -= numCharsRemoved - } - } - - // filter out any entities that were made irrelevant - this.entities = this.entities.filter(ent => ent.index.start < ent.index.end) - return this - } -} diff --git a/src/lib/styles.ts b/src/lib/styles.ts index aa255b21f..409c77548 100644 --- a/src/lib/styles.ts +++ b/src/lib/styles.ts @@ -71,6 +71,7 @@ export const s = StyleSheet.create({ borderBottom1: {borderBottomWidth: 1}, borderLeft1: {borderLeftWidth: 1}, hidden: {display: 'none'}, + dimmed: {opacity: 0.5}, // font weights fw600: {fontWeight: '600'}, diff --git a/src/platform/polyfills.ts b/src/platform/polyfills.ts index 3dbd13981..a64c2c33a 100644 --- a/src/platform/polyfills.ts +++ b/src/platform/polyfills.ts @@ -1,3 +1,5 @@ +import 'fast-text-encoding' +import Graphemer from 'graphemer' export {} /** @@ -48,3 +50,18 @@ globalThis.atob = (str: string): string => { } return result } + +const splitter = new Graphemer() +globalThis.Intl = globalThis.Intl || {} + +// @ts-ignore we're polyfilling -prf +globalThis.Intl.Segmenter = + // @ts-ignore we're polyfilling -prf + globalThis.Intl.Segmenter || + class Segmenter { + constructor() {} + // NOTE + // this is not a precisely correct polyfill but it's sufficient for our needs + // -prf + segment = splitter.iterateGraphemes + } diff --git a/src/platform/polyfills.web.ts b/src/platform/polyfills.web.ts index 7a42f4887..e46963a6f 100644 --- a/src/platform/polyfills.web.ts +++ b/src/platform/polyfills.web.ts @@ -2,3 +2,11 @@ // @ts-ignore whatever typescript wants to complain about here, I dont care about -prf window.setImmediate = (cb: () => void) => setTimeout(cb, 0) + +// @ts-ignore not on the TS signature due to bad support -prf +if (!globalThis.Intl?.Segmenter) { + // NOTE loading as a separate script to reduce main bundle size, as this is only needed in FF -prf + const script = document.createElement('script') + script.setAttribute('src', '/static/js/intl-segmenter-polyfill.min.js') + document.head.appendChild(script) +} diff --git a/src/routes.ts b/src/routes.ts index 6c02a7c50..167efcfb7 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -9,7 +9,7 @@ export const router = new Router({ ProfileFollowers: '/profile/:name/followers', ProfileFollows: '/profile/:name/follows', PostThread: '/profile/:name/post/:rkey', - PostUpvotedBy: '/profile/:name/post/:rkey/upvoted-by', + PostLikedBy: '/profile/:name/post/:rkey/liked-by', PostRepostedBy: '/profile/:name/post/:rkey/reposted-by', Debug: '/sys/debug', Log: '/sys/log', diff --git a/src/state/index.ts b/src/state/index.ts index f0713efeb..4755c28f4 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -1,6 +1,6 @@ import {autorun} from 'mobx' import {AppState, Platform} from 'react-native' -import {AtpAgent} from '@atproto/api' +import {BskyAgent} from '@atproto/api' import {RootStoreModel} from './models/root-store' import * as apiPolyfill from 'lib/api/api-polyfill' import * as storage from 'lib/storage' @@ -19,7 +19,7 @@ export async function setupState(serviceUri = DEFAULT_SERVICE) { apiPolyfill.doPolyfill() - rootStore = new RootStoreModel(new AtpAgent({service: serviceUri})) + rootStore = new RootStoreModel(new BskyAgent({service: serviceUri})) try { data = (await storage.load(ROOT_STATE_STORAGE_KEY)) || {} rootStore.log.debug('Initial hydrate', {hasSession: !!data.session}) diff --git a/src/state/models/cache/image-sizes.ts b/src/state/models/cache/image-sizes.ts index ff0486278..2fd6e0013 100644 --- a/src/state/models/cache/image-sizes.ts +++ b/src/state/models/cache/image-sizes.ts @@ -3,7 +3,7 @@ import {Dim} from 'lib/media/manip' export class ImageSizesCache { sizes: Map<string, Dim> = new Map() - private activeRequests: Map<string, Promise<Dim>> = new Map() + activeRequests: Map<string, Promise<Dim>> = new Map() constructor() {} diff --git a/src/state/models/cache/my-follows.ts b/src/state/models/cache/my-follows.ts index 725b7841e..14eeaae21 100644 --- a/src/state/models/cache/my-follows.ts +++ b/src/state/models/cache/my-follows.ts @@ -1,15 +1,12 @@ import {makeAutoObservable, runInAction} from 'mobx' -import {FollowRecord, AppBskyActorProfile, AppBskyActorRef} from '@atproto/api' +import {FollowRecord, AppBskyActorDefs} from '@atproto/api' import {RootStoreModel} from '../root-store' import {bundleAsync} from 'lib/async/bundle' const CACHE_TTL = 1000 * 60 * 60 // hourly type FollowsListResponse = Awaited<ReturnType<FollowRecord['list']>> type FollowsListResponseRecord = FollowsListResponse['records'][0] -type Profile = - | AppBskyActorProfile.ViewBasic - | AppBskyActorProfile.View - | AppBskyActorRef.WithInfo +type Profile = AppBskyActorDefs.ProfileViewBasic | AppBskyActorDefs.ProfileView /** * This model is used to maintain a synced local cache of the user's @@ -53,21 +50,21 @@ export class MyFollowsCache { fetch = bundleAsync(async () => { this.rootStore.log.debug('MyFollowsModel:fetch running full fetch') - let before + let rkeyStart let records: FollowsListResponseRecord[] = [] do { const res: FollowsListResponse = - await this.rootStore.api.app.bsky.graph.follow.list({ - user: this.rootStore.me.did, - before, + await this.rootStore.agent.app.bsky.graph.follow.list({ + repo: this.rootStore.me.did, + rkeyStart, }) records = records.concat(res.records) - before = res.cursor - } while (typeof before !== 'undefined') + rkeyStart = res.cursor + } while (typeof rkeyStart !== 'undefined') runInAction(() => { this.followDidToRecordMap = {} for (const record of records) { - this.followDidToRecordMap[record.value.subject.did] = record.uri + this.followDidToRecordMap[record.value.subject] = record.uri } this.lastSync = Date.now() this.myDid = this.rootStore.me.did diff --git a/src/state/models/discovery/foafs.ts b/src/state/models/discovery/foafs.ts index 241338a16..27cee8503 100644 --- a/src/state/models/discovery/foafs.ts +++ b/src/state/models/discovery/foafs.ts @@ -1,15 +1,15 @@ -import {AppBskyActorProfile, AppBskyActorRef} from '@atproto/api' +import {AppBskyActorDefs} from '@atproto/api' import {makeAutoObservable, runInAction} from 'mobx' import sampleSize from 'lodash.samplesize' import {bundleAsync} from 'lib/async/bundle' import {RootStoreModel} from '../root-store' -export type RefWithInfoAndFollowers = AppBskyActorRef.WithInfo & { - followers: AppBskyActorProfile.View[] +export type RefWithInfoAndFollowers = AppBskyActorDefs.ProfileViewBasic & { + followers: AppBskyActorDefs.ProfileView[] } -export type ProfileViewFollows = AppBskyActorProfile.View & { - follows: AppBskyActorRef.WithInfo[] +export type ProfileViewFollows = AppBskyActorDefs.ProfileView & { + follows: AppBskyActorDefs.ProfileViewBasic[] } export class FoafsModel { @@ -51,14 +51,14 @@ export class FoafsModel { this.popular.length = 0 // fetch their profiles - const profiles = await this.rootStore.api.app.bsky.actor.getProfiles({ + const profiles = await this.rootStore.agent.getProfiles({ actors: this.sources, }) // fetch their follows const results = await Promise.allSettled( this.sources.map(source => - this.rootStore.api.app.bsky.graph.getFollows({user: source}), + this.rootStore.agent.getFollows({actor: source}), ), ) diff --git a/src/state/models/discovery/suggested-actors.ts b/src/state/models/discovery/suggested-actors.ts index cf8e2dd7b..91c5efd02 100644 --- a/src/state/models/discovery/suggested-actors.ts +++ b/src/state/models/discovery/suggested-actors.ts @@ -1,5 +1,5 @@ import {makeAutoObservable, runInAction} from 'mobx' -import {AppBskyActorProfile as Profile} from '@atproto/api' +import {AppBskyActorDefs} from '@atproto/api' import shuffle from 'lodash.shuffle' import {RootStoreModel} from '../root-store' import {cleanError} from 'lib/strings/errors' @@ -8,7 +8,9 @@ import {SUGGESTED_FOLLOWS} from 'lib/constants' const PAGE_SIZE = 30 -export type SuggestedActor = Profile.ViewBasic | Profile.View +export type SuggestedActor = + | AppBskyActorDefs.ProfileViewBasic + | AppBskyActorDefs.ProfileView export class SuggestedActorsModel { // state @@ -20,7 +22,7 @@ export class SuggestedActorsModel { hasMore = true loadMoreCursor?: string - private hardCodedSuggestions: SuggestedActor[] | undefined + hardCodedSuggestions: SuggestedActor[] | undefined // data suggestions: SuggestedActor[] = [] @@ -82,7 +84,7 @@ export class SuggestedActorsModel { this.loadMoreCursor = undefined } else { // pull from the PDS' algo - res = await this.rootStore.api.app.bsky.actor.getSuggestions({ + res = await this.rootStore.agent.app.bsky.actor.getSuggestions({ limit: this.pageSize, cursor: this.loadMoreCursor, }) @@ -104,7 +106,7 @@ export class SuggestedActorsModel { } }) - private async fetchHardcodedSuggestions() { + async fetchHardcodedSuggestions() { if (this.hardCodedSuggestions) { return } @@ -118,9 +120,9 @@ export class SuggestedActorsModel { ] // fetch the profiles in chunks of 25 (the limit allowed by `getProfiles`) - let profiles: Profile.View[] = [] + let profiles: AppBskyActorDefs.ProfileView[] = [] do { - const res = await this.rootStore.api.app.bsky.actor.getProfiles({ + const res = await this.rootStore.agent.getProfiles({ actors: actors.splice(0, 25), }) profiles = profiles.concat(res.data.profiles) @@ -152,13 +154,13 @@ export class SuggestedActorsModel { // state transitions // = - private _xLoading(isRefreshing = false) { + _xLoading(isRefreshing = false) { this.isLoading = true this.isRefreshing = isRefreshing this.error = '' } - private _xIdle(err?: any) { + _xIdle(err?: any) { this.isLoading = false this.isRefreshing = false this.hasLoaded = true diff --git a/src/state/models/feed-view.ts b/src/state/models/feed-view.ts index 083863fe2..8b62c958f 100644 --- a/src/state/models/feed-view.ts +++ b/src/state/models/feed-view.ts @@ -1,32 +1,29 @@ import {makeAutoObservable, runInAction} from 'mobx' import { AppBskyFeedGetTimeline as GetTimeline, - AppBskyFeedFeedViewPost, + AppBskyFeedDefs, AppBskyFeedPost, AppBskyFeedGetAuthorFeed as GetAuthorFeed, + RichText, } from '@atproto/api' import AwaitLock from 'await-lock' import {bundleAsync} from 'lib/async/bundle' import sampleSize from 'lodash.samplesize' -type FeedViewPost = AppBskyFeedFeedViewPost.Main -type ReasonRepost = AppBskyFeedFeedViewPost.ReasonRepost -type PostView = AppBskyFeedPost.View -import {AtUri} from '../../third-party/uri' import {RootStoreModel} from './root-store' -import * as apilib from 'lib/api/index' import {cleanError} from 'lib/strings/errors' -import {RichText} from 'lib/strings/rich-text' import {SUGGESTED_FOLLOWS} from 'lib/constants' import { getCombinedCursors, getMultipleAuthorsPosts, mergePosts, } from 'lib/api/build-suggested-posts' - import {FeedTuner, FeedViewPostsSlice} from 'lib/api/feed-manip' -const PAGE_SIZE = 30 +type FeedViewPost = AppBskyFeedDefs.FeedViewPost +type ReasonRepost = AppBskyFeedDefs.ReasonRepost +type PostView = AppBskyFeedDefs.PostView +const PAGE_SIZE = 30 let _idCounter = 0 export class FeedItemModel { @@ -51,11 +48,7 @@ export class FeedItemModel { const valid = AppBskyFeedPost.validateRecord(this.post.record) if (valid.success) { this.postRecord = this.post.record - this.richText = new RichText( - this.postRecord.text, - this.postRecord.entities, - {cleanNewlines: true}, - ) + this.richText = new RichText(this.postRecord, {cleanNewlines: true}) } else { rootStore.log.warn( 'Received an invalid app.bsky.feed.post record', @@ -82,7 +75,7 @@ export class FeedItemModel { copyMetrics(v: FeedViewPost) { this.post.replyCount = v.post.replyCount this.post.repostCount = v.post.repostCount - this.post.upvoteCount = v.post.upvoteCount + this.post.likeCount = v.post.likeCount this.post.viewer = v.post.viewer } @@ -92,68 +85,43 @@ export class FeedItemModel { } } - async toggleUpvote() { - const wasUpvoted = !!this.post.viewer.upvote - const wasDownvoted = !!this.post.viewer.downvote - const res = await this.rootStore.api.app.bsky.feed.setVote({ - subject: { - uri: this.post.uri, - cid: this.post.cid, - }, - direction: wasUpvoted ? 'none' : 'up', - }) - runInAction(() => { - if (wasDownvoted) { - this.post.downvoteCount-- - } - if (wasUpvoted) { - this.post.upvoteCount-- - } else { - this.post.upvoteCount++ - } - this.post.viewer.upvote = res.data.upvote - this.post.viewer.downvote = res.data.downvote - }) - } - - async toggleDownvote() { - const wasUpvoted = !!this.post.viewer.upvote - const wasDownvoted = !!this.post.viewer.downvote - const res = await this.rootStore.api.app.bsky.feed.setVote({ - subject: { - uri: this.post.uri, - cid: this.post.cid, - }, - direction: wasDownvoted ? 'none' : 'down', - }) - runInAction(() => { - if (wasUpvoted) { - this.post.upvoteCount-- - } - if (wasDownvoted) { - this.post.downvoteCount-- - } else { - this.post.downvoteCount++ - } - this.post.viewer.upvote = res.data.upvote - this.post.viewer.downvote = res.data.downvote - }) + async toggleLike() { + if (this.post.viewer?.like) { + await this.rootStore.agent.deleteLike(this.post.viewer.like) + runInAction(() => { + this.post.likeCount = this.post.likeCount || 0 + this.post.viewer = this.post.viewer || {} + this.post.likeCount-- + this.post.viewer.like = undefined + }) + } else { + const res = await this.rootStore.agent.like(this.post.uri, this.post.cid) + runInAction(() => { + this.post.likeCount = this.post.likeCount || 0 + this.post.viewer = this.post.viewer || {} + this.post.likeCount++ + this.post.viewer.like = res.uri + }) + } } async toggleRepost() { - if (this.post.viewer.repost) { - await apilib.unrepost(this.rootStore, this.post.viewer.repost) + if (this.post.viewer?.repost) { + await this.rootStore.agent.deleteRepost(this.post.viewer.repost) runInAction(() => { + this.post.repostCount = this.post.repostCount || 0 + this.post.viewer = this.post.viewer || {} this.post.repostCount-- this.post.viewer.repost = undefined }) } else { - const res = await apilib.repost( - this.rootStore, + const res = await this.rootStore.agent.repost( this.post.uri, this.post.cid, ) runInAction(() => { + this.post.repostCount = this.post.repostCount || 0 + this.post.viewer = this.post.viewer || {} this.post.repostCount++ this.post.viewer.repost = res.uri }) @@ -161,10 +129,7 @@ export class FeedItemModel { } async delete() { - await this.rootStore.api.app.bsky.feed.post.delete({ - did: this.post.author.did, - rkey: new AtUri(this.post.uri).rkey, - }) + await this.rootStore.agent.deletePost(this.post.uri) this.rootStore.emitPostDeleted(this.post.uri) } } @@ -250,7 +215,7 @@ export class FeedModel { tuner = new FeedTuner() // used to linearize async modifications to state - private lock = new AwaitLock() + lock = new AwaitLock() // data slices: FeedSliceModel[] = [] @@ -291,8 +256,8 @@ export class FeedModel { const params = this.params as GetAuthorFeed.QueryParams const item = slice.rootItem const isRepost = - item?.reasonRepost?.by?.handle === params.author || - item?.reasonRepost?.by?.did === params.author + item?.reasonRepost?.by?.handle === params.actor || + item?.reasonRepost?.by?.did === params.actor return ( !item.reply || // not a reply isRepost || // but allow if it's a repost @@ -338,7 +303,7 @@ export class FeedModel { return this.setup() } - private get feedTuners() { + get feedTuners() { if (this.feedType === 'goodstuff') { return [ FeedTuner.dedupReposts, @@ -406,7 +371,7 @@ export class FeedModel { this._xLoading() try { const res = await this._getFeed({ - before: this.loadMoreCursor, + cursor: this.loadMoreCursor, limit: PAGE_SIZE, }) await this._appendAll(res) @@ -439,7 +404,7 @@ export class FeedModel { try { do { const res: GetTimeline.Response = await this._getFeed({ - before: cursor, + cursor, limit: Math.min(numToFetch, 100), }) if (res.data.feed.length === 0) { @@ -478,14 +443,18 @@ export class FeedModel { new FeedSliceModel(this.rootStore, `item-${_idCounter++}`, slice), ) if (autoPrepend) { - this.slices = nextSlicesModels.concat( - this.slices.filter(slice1 => - nextSlicesModels.find(slice2 => slice1.uri === slice2.uri), - ), - ) - this.setHasNewLatest(false) + runInAction(() => { + this.slices = nextSlicesModels.concat( + this.slices.filter(slice1 => + nextSlicesModels.find(slice2 => slice1.uri === slice2.uri), + ), + ) + this.setHasNewLatest(false) + }) } else { - this.nextSlices = nextSlicesModels + runInAction(() => { + this.nextSlices = nextSlicesModels + }) this.setHasNewLatest(true) } } else { @@ -519,13 +488,13 @@ export class FeedModel { // state transitions // = - private _xLoading(isRefreshing = false) { + _xLoading(isRefreshing = false) { this.isLoading = true this.isRefreshing = isRefreshing this.error = '' } - private _xIdle(err?: any) { + _xIdle(err?: any) { this.isLoading = false this.isRefreshing = false this.hasLoaded = true @@ -538,14 +507,12 @@ export class FeedModel { // helper functions // = - private async _replaceAll( - res: GetTimeline.Response | GetAuthorFeed.Response, - ) { + async _replaceAll(res: GetTimeline.Response | GetAuthorFeed.Response) { this.pollCursor = res.data.feed[0]?.post.uri return this._appendAll(res, true) } - private async _appendAll( + async _appendAll( res: GetTimeline.Response | GetAuthorFeed.Response, replace = false, ) { @@ -572,7 +539,7 @@ export class FeedModel { }) } - private _updateAll(res: GetTimeline.Response | GetAuthorFeed.Response) { + _updateAll(res: GetTimeline.Response | GetAuthorFeed.Response) { for (const item of res.data.feed) { const existingSlice = this.slices.find(slice => slice.containsUri(item.post.uri), @@ -596,7 +563,7 @@ export class FeedModel { const responses = await getMultipleAuthorsPosts( this.rootStore, sampleSize(SUGGESTED_FOLLOWS(String(this.rootStore.agent.service)), 20), - params.before, + params.cursor, 20, ) const combinedCursor = getCombinedCursors(responses) @@ -611,9 +578,7 @@ export class FeedModel { headers: lastHeaders, } } else if (this.feedType === 'home') { - return this.rootStore.api.app.bsky.feed.getTimeline( - params as GetTimeline.QueryParams, - ) + return this.rootStore.agent.getTimeline(params as GetTimeline.QueryParams) } else if (this.feedType === 'goodstuff') { const res = await getGoodStuff( this.rootStore.session.currentSession?.accessJwt || '', @@ -624,7 +589,7 @@ export class FeedModel { ) return res } else { - return this.rootStore.api.app.bsky.feed.getAuthorFeed( + return this.rootStore.agent.getAuthorFeed( params as GetAuthorFeed.QueryParams, ) } diff --git a/src/state/models/votes-view.ts b/src/state/models/likes-view.ts index ad8698d21..5f9df692e 100644 --- a/src/state/models/votes-view.ts +++ b/src/state/models/likes-view.ts @@ -1,6 +1,6 @@ import {makeAutoObservable, runInAction} from 'mobx' import {AtUri} from '../../third-party/uri' -import {AppBskyFeedGetVotes as GetVotes} from '@atproto/api' +import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api' import {RootStoreModel} from './root-store' import {cleanError} from 'lib/strings/errors' import {bundleAsync} from 'lib/async/bundle' @@ -8,24 +8,24 @@ import * as apilib from 'lib/api/index' const PAGE_SIZE = 30 -export type VoteItem = GetVotes.Vote +export type LikeItem = GetLikes.Like -export class VotesViewModel { +export class LikesViewModel { // state isLoading = false isRefreshing = false hasLoaded = false error = '' resolvedUri = '' - params: GetVotes.QueryParams + params: GetLikes.QueryParams hasMore = true loadMoreCursor?: string // data uri: string = '' - votes: VoteItem[] = [] + likes: LikeItem[] = [] - constructor(public rootStore: RootStoreModel, params: GetVotes.QueryParams) { + constructor(public rootStore: RootStoreModel, params: GetLikes.QueryParams) { makeAutoObservable( this, { @@ -68,9 +68,9 @@ export class VotesViewModel { const params = Object.assign({}, this.params, { uri: this.resolvedUri, limit: PAGE_SIZE, - before: replace ? undefined : this.loadMoreCursor, + cursor: replace ? undefined : this.loadMoreCursor, }) - const res = await this.rootStore.api.app.bsky.feed.getVotes(params) + const res = await this.rootStore.agent.getLikes(params) if (replace) { this._replaceAll(res) } else { @@ -85,13 +85,13 @@ export class VotesViewModel { // state transitions // = - private _xLoading(isRefreshing = false) { + _xLoading(isRefreshing = false) { this.isLoading = true this.isRefreshing = isRefreshing this.error = '' } - private _xIdle(err?: any) { + _xIdle(err?: any) { this.isLoading = false this.isRefreshing = false this.hasLoaded = true @@ -104,7 +104,7 @@ export class VotesViewModel { // helper functions // = - private async _resolveUri() { + async _resolveUri() { const urip = new AtUri(this.params.uri) if (!urip.host.startsWith('did:')) { try { @@ -118,14 +118,14 @@ export class VotesViewModel { }) } - private _replaceAll(res: GetVotes.Response) { - this.votes = [] + _replaceAll(res: GetLikes.Response) { + this.likes = [] this._appendAll(res) } - private _appendAll(res: GetVotes.Response) { + _appendAll(res: GetLikes.Response) { this.loadMoreCursor = res.data.cursor this.hasMore = !!this.loadMoreCursor - this.votes = this.votes.concat(res.data.votes) + this.likes = this.likes.concat(res.data.likes) } } diff --git a/src/state/models/log.ts b/src/state/models/log.ts index ed701dc61..d80617139 100644 --- a/src/state/models/log.ts +++ b/src/state/models/log.ts @@ -1,5 +1,5 @@ import {makeAutoObservable} from 'mobx' -import {XRPCError, XRPCInvalidResponseError} from '@atproto/xrpc' +// import {XRPCError, XRPCInvalidResponseError} from '@atproto/xrpc' TODO const MAX_ENTRIES = 300 @@ -32,7 +32,7 @@ export class LogModel { makeAutoObservable(this) } - private add(entry: LogEntry) { + add(entry: LogEntry) { this.entries.push(entry) while (this.entries.length > MAX_ENTRIES) { this.entries = this.entries.slice(50) @@ -79,14 +79,14 @@ export class LogModel { function detailsToStr(details?: any) { if (details && typeof details !== 'string') { if ( - details instanceof XRPCInvalidResponseError || + // details instanceof XRPCInvalidResponseError || TODO 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 instanceof XRPCError || TODO details.constructor.name === 'XRPCError' ) { return `An XRPC error occurred.\nStatus: ${details.status}\nError: ${details.error}\nMessage: ${details.message}` diff --git a/src/state/models/me.ts b/src/state/models/me.ts index 120749155..5f670b8f9 100644 --- a/src/state/models/me.ts +++ b/src/state/models/me.ts @@ -85,7 +85,7 @@ export class MeModel { if (sess.hasSession) { this.did = sess.currentSession?.did || '' this.handle = sess.currentSession?.handle || '' - const profile = await this.rootStore.api.app.bsky.actor.getProfile({ + const profile = await this.rootStore.agent.getProfile({ actor: this.did, }) runInAction(() => { diff --git a/src/state/models/notifications-view.ts b/src/state/models/notifications-view.ts index e88af590b..4f7a52fd9 100644 --- a/src/state/models/notifications-view.ts +++ b/src/state/models/notifications-view.ts @@ -1,11 +1,10 @@ import {makeAutoObservable, runInAction} from 'mobx' import { - AppBskyNotificationList as ListNotifications, - AppBskyActorRef as ActorRef, + AppBskyNotificationListNotifications as ListNotifications, + AppBskyActorDefs, AppBskyFeedPost, AppBskyFeedRepost, - AppBskyFeedVote, - AppBskyGraphAssertion, + AppBskyFeedLike, AppBskyGraphFollow, } from '@atproto/api' import AwaitLock from 'await-lock' @@ -28,8 +27,7 @@ export interface GroupedNotification extends ListNotifications.Notification { type SupportedRecord = | AppBskyFeedPost.Record | AppBskyFeedRepost.Record - | AppBskyFeedVote.Record - | AppBskyGraphAssertion.Record + | AppBskyFeedLike.Record | AppBskyGraphFollow.Record export class NotificationsViewItemModel { @@ -39,11 +37,10 @@ export class NotificationsViewItemModel { // data uri: string = '' cid: string = '' - author: ActorRef.WithInfo = { + author: AppBskyActorDefs.ProfileViewBasic = { did: '', handle: '', avatar: '', - declaration: {cid: '', actorType: ''}, } reason: string = '' reasonSubject?: string @@ -86,8 +83,8 @@ export class NotificationsViewItemModel { } } - get isUpvote() { - return this.reason === 'vote' + get isLike() { + return this.reason === 'like' } get isRepost() { @@ -102,16 +99,22 @@ export class NotificationsViewItemModel { return this.reason === 'reply' } - get isFollow() { - return this.reason === 'follow' + get isQuote() { + return this.reason === 'quote' } - get isAssertion() { - return this.reason === 'assertion' + get isFollow() { + return this.reason === 'follow' } get needsAdditionalData() { - if (this.isUpvote || this.isRepost || this.isReply || this.isMention) { + if ( + this.isLike || + this.isRepost || + this.isReply || + this.isQuote || + this.isMention + ) { return !this.additionalPost } return false @@ -124,7 +127,7 @@ export class NotificationsViewItemModel { const record = this.record if ( AppBskyFeedRepost.isRecord(record) || - AppBskyFeedVote.isRecord(record) + AppBskyFeedLike.isRecord(record) ) { return record.subject.uri } @@ -135,8 +138,7 @@ export class NotificationsViewItemModel { for (const ns of [ AppBskyFeedPost, AppBskyFeedRepost, - AppBskyFeedVote, - AppBskyGraphAssertion, + AppBskyFeedLike, AppBskyGraphFollow, ]) { if (ns.isRecord(v)) { @@ -163,9 +165,9 @@ export class NotificationsViewItemModel { return } let postUri - if (this.isReply || this.isMention) { + if (this.isReply || this.isQuote || this.isMention) { postUri = this.uri - } else if (this.isUpvote || this.isRepost) { + } else if (this.isLike || this.isRepost) { postUri = this.subjectUri } if (postUri) { @@ -194,7 +196,7 @@ export class NotificationsViewModel { loadMoreCursor?: string // used to linearize async modifications to state - private lock = new AwaitLock() + lock = new AwaitLock() // data notifications: NotificationsViewItemModel[] = [] @@ -266,7 +268,7 @@ export class NotificationsViewModel { const params = Object.assign({}, this.params, { limit: PAGE_SIZE, }) - const res = await this.rootStore.api.app.bsky.notification.list(params) + const res = await this.rootStore.agent.listNotifications(params) await this._replaceAll(res) this._xIdle() } catch (e: any) { @@ -297,9 +299,9 @@ export class NotificationsViewModel { try { const params = Object.assign({}, this.params, { limit: PAGE_SIZE, - before: this.loadMoreCursor, + cursor: this.loadMoreCursor, }) - const res = await this.rootStore.api.app.bsky.notification.list(params) + const res = await this.rootStore.agent.listNotifications(params) await this._appendAll(res) this._xIdle() } catch (e: any) { @@ -325,7 +327,7 @@ export class NotificationsViewModel { try { this._xLoading() try { - const res = await this.rootStore.api.app.bsky.notification.list({ + const res = await this.rootStore.agent.listNotifications({ limit: PAGE_SIZE, }) await this._prependAll(res) @@ -357,8 +359,8 @@ export class NotificationsViewModel { try { do { const res: ListNotifications.Response = - await this.rootStore.api.app.bsky.notification.list({ - before: cursor, + await this.rootStore.agent.listNotifications({ + cursor, limit: Math.min(numToFetch, 100), }) if (res.data.notifications.length === 0) { @@ -390,7 +392,7 @@ export class NotificationsViewModel { */ loadUnreadCount = bundleAsync(async () => { const old = this.unreadCount - const res = await this.rootStore.api.app.bsky.notification.getCount() + const res = await this.rootStore.agent.countUnreadNotifications() runInAction(() => { this.unreadCount = res.data.count }) @@ -408,9 +410,7 @@ export class NotificationsViewModel { for (const notif of this.notifications) { notif.isRead = true } - await this.rootStore.api.app.bsky.notification.updateSeen({ - seenAt: new Date().toISOString(), - }) + await this.rootStore.agent.updateSeenNotifications() } catch (e: any) { this.rootStore.log.warn('Failed to update notifications read state', e) } @@ -418,7 +418,7 @@ export class NotificationsViewModel { async getNewMostRecent(): Promise<NotificationsViewItemModel | undefined> { let old = this.mostRecentNotificationUri - const res = await this.rootStore.api.app.bsky.notification.list({ + const res = await this.rootStore.agent.listNotifications({ limit: 1, }) if (!res.data.notifications[0] || old === res.data.notifications[0].uri) { @@ -437,13 +437,13 @@ export class NotificationsViewModel { // state transitions // = - private _xLoading(isRefreshing = false) { + _xLoading(isRefreshing = false) { this.isLoading = true this.isRefreshing = isRefreshing this.error = '' } - private _xIdle(err?: any) { + _xIdle(err?: any) { this.isLoading = false this.isRefreshing = false this.hasLoaded = true @@ -456,14 +456,14 @@ export class NotificationsViewModel { // helper functions // = - private async _replaceAll(res: ListNotifications.Response) { + async _replaceAll(res: ListNotifications.Response) { if (res.data.notifications[0]) { this.mostRecentNotificationUri = res.data.notifications[0].uri } return this._appendAll(res, true) } - private async _appendAll(res: ListNotifications.Response, replace = false) { + async _appendAll(res: ListNotifications.Response, replace = false) { this.loadMoreCursor = res.data.cursor this.hasMore = !!this.loadMoreCursor const promises = [] @@ -494,7 +494,7 @@ export class NotificationsViewModel { }) } - private async _prependAll(res: ListNotifications.Response) { + async _prependAll(res: ListNotifications.Response) { const promises = [] const itemModels: NotificationsViewItemModel[] = [] const dedupedNotifs = res.data.notifications.filter( @@ -525,7 +525,7 @@ export class NotificationsViewModel { }) } - private _updateAll(res: ListNotifications.Response) { + _updateAll(res: ListNotifications.Response) { for (const item of res.data.notifications) { const existingItem = this.notifications.find(item2 => isEq(item, item2)) if (existingItem) { diff --git a/src/state/models/post-thread-view.ts b/src/state/models/post-thread-view.ts index d58ee691b..c5395b9c8 100644 --- a/src/state/models/post-thread-view.ts +++ b/src/state/models/post-thread-view.ts @@ -2,12 +2,13 @@ import {makeAutoObservable, runInAction} from 'mobx' import { AppBskyFeedGetPostThread as GetPostThread, AppBskyFeedPost as FeedPost, + AppBskyFeedDefs, + RichText, } from '@atproto/api' import {AtUri} from '../../third-party/uri' import {RootStoreModel} from './root-store' import * as apilib from 'lib/api/index' import {cleanError} from 'lib/strings/errors' -import {RichText} from 'lib/strings/rich-text' function* reactKeyGenerator(): Generator<string> { let counter = 0 @@ -26,10 +27,10 @@ export class PostThreadViewPostModel { _hasMore = false // data - post: FeedPost.View + post: AppBskyFeedDefs.PostView postRecord?: FeedPost.Record - parent?: PostThreadViewPostModel | GetPostThread.NotFoundPost - replies?: (PostThreadViewPostModel | GetPostThread.NotFoundPost)[] + parent?: PostThreadViewPostModel | AppBskyFeedDefs.NotFoundPost + replies?: (PostThreadViewPostModel | AppBskyFeedDefs.NotFoundPost)[] richText?: RichText get uri() { @@ -43,7 +44,7 @@ export class PostThreadViewPostModel { constructor( public rootStore: RootStoreModel, reactKey: string, - v: GetPostThread.ThreadViewPost, + v: AppBskyFeedDefs.ThreadViewPost, ) { this._reactKey = reactKey this.post = v.post @@ -51,11 +52,7 @@ export class PostThreadViewPostModel { const valid = FeedPost.validateRecord(this.post.record) if (valid.success) { this.postRecord = this.post.record - this.richText = new RichText( - this.postRecord.text, - this.postRecord.entities, - {cleanNewlines: true}, - ) + this.richText = new RichText(this.postRecord, {cleanNewlines: true}) } else { rootStore.log.warn( 'Received an invalid app.bsky.feed.post record', @@ -74,14 +71,14 @@ export class PostThreadViewPostModel { assignTreeModels( keyGen: Generator<string>, - v: GetPostThread.ThreadViewPost, + v: AppBskyFeedDefs.ThreadViewPost, higlightedPostUri: string, includeParent = true, includeChildren = true, ) { // parents if (includeParent && v.parent) { - if (GetPostThread.isThreadViewPost(v.parent)) { + if (AppBskyFeedDefs.isThreadViewPost(v.parent)) { const parentModel = new PostThreadViewPostModel( this.rootStore, keyGen.next().value, @@ -100,7 +97,7 @@ export class PostThreadViewPostModel { ) } this.parent = parentModel - } else if (GetPostThread.isNotFoundPost(v.parent)) { + } else if (AppBskyFeedDefs.isNotFoundPost(v.parent)) { this.parent = v.parent } } @@ -108,7 +105,7 @@ export class PostThreadViewPostModel { if (includeChildren && v.replies) { const replies = [] for (const item of v.replies) { - if (GetPostThread.isThreadViewPost(item)) { + if (AppBskyFeedDefs.isThreadViewPost(item)) { const itemModel = new PostThreadViewPostModel( this.rootStore, keyGen.next().value, @@ -128,7 +125,7 @@ export class PostThreadViewPostModel { ) } replies.push(itemModel) - } else if (GetPostThread.isNotFoundPost(item)) { + } else if (AppBskyFeedDefs.isNotFoundPost(item)) { replies.push(item) } } @@ -136,68 +133,43 @@ export class PostThreadViewPostModel { } } - async toggleUpvote() { - const wasUpvoted = !!this.post.viewer.upvote - const wasDownvoted = !!this.post.viewer.downvote - const res = await this.rootStore.api.app.bsky.feed.setVote({ - subject: { - uri: this.post.uri, - cid: this.post.cid, - }, - direction: wasUpvoted ? 'none' : 'up', - }) - runInAction(() => { - if (wasDownvoted) { - this.post.downvoteCount-- - } - if (wasUpvoted) { - this.post.upvoteCount-- - } else { - this.post.upvoteCount++ - } - this.post.viewer.upvote = res.data.upvote - this.post.viewer.downvote = res.data.downvote - }) - } - - async toggleDownvote() { - const wasUpvoted = !!this.post.viewer.upvote - const wasDownvoted = !!this.post.viewer.downvote - const res = await this.rootStore.api.app.bsky.feed.setVote({ - subject: { - uri: this.post.uri, - cid: this.post.cid, - }, - direction: wasDownvoted ? 'none' : 'down', - }) - runInAction(() => { - if (wasUpvoted) { - this.post.upvoteCount-- - } - if (wasDownvoted) { - this.post.downvoteCount-- - } else { - this.post.downvoteCount++ - } - this.post.viewer.upvote = res.data.upvote - this.post.viewer.downvote = res.data.downvote - }) + async toggleLike() { + if (this.post.viewer?.like) { + await this.rootStore.agent.deleteLike(this.post.viewer.like) + runInAction(() => { + this.post.likeCount = this.post.likeCount || 0 + this.post.viewer = this.post.viewer || {} + this.post.likeCount-- + this.post.viewer.like = undefined + }) + } else { + const res = await this.rootStore.agent.like(this.post.uri, this.post.cid) + runInAction(() => { + this.post.likeCount = this.post.likeCount || 0 + this.post.viewer = this.post.viewer || {} + this.post.likeCount++ + this.post.viewer.like = res.uri + }) + } } async toggleRepost() { - if (this.post.viewer.repost) { - await apilib.unrepost(this.rootStore, this.post.viewer.repost) + if (this.post.viewer?.repost) { + await this.rootStore.agent.deleteRepost(this.post.viewer.repost) runInAction(() => { + this.post.repostCount = this.post.repostCount || 0 + this.post.viewer = this.post.viewer || {} this.post.repostCount-- this.post.viewer.repost = undefined }) } else { - const res = await apilib.repost( - this.rootStore, + const res = await this.rootStore.agent.repost( this.post.uri, this.post.cid, ) runInAction(() => { + this.post.repostCount = this.post.repostCount || 0 + this.post.viewer = this.post.viewer || {} this.post.repostCount++ this.post.viewer.repost = res.uri }) @@ -205,10 +177,7 @@ export class PostThreadViewPostModel { } async delete() { - await this.rootStore.api.app.bsky.feed.post.delete({ - did: this.post.author.did, - rkey: new AtUri(this.post.uri).rkey, - }) + await this.rootStore.agent.deletePost(this.post.uri) this.rootStore.emitPostDeleted(this.post.uri) } } @@ -301,14 +270,14 @@ export class PostThreadViewModel { // state transitions // = - private _xLoading(isRefreshing = false) { + _xLoading(isRefreshing = false) { this.isLoading = true this.isRefreshing = isRefreshing this.error = '' this.notFound = false } - private _xIdle(err?: any) { + _xIdle(err?: any) { this.isLoading = false this.isRefreshing = false this.hasLoaded = true @@ -322,7 +291,7 @@ export class PostThreadViewModel { // loader functions // = - private async _resolveUri() { + async _resolveUri() { const urip = new AtUri(this.params.uri) if (!urip.host.startsWith('did:')) { try { @@ -336,10 +305,10 @@ export class PostThreadViewModel { }) } - private async _load(isRefreshing = false) { + async _load(isRefreshing = false) { this._xLoading(isRefreshing) try { - const res = await this.rootStore.api.app.bsky.feed.getPostThread( + const res = await this.rootStore.agent.getPostThread( Object.assign({}, this.params, {uri: this.resolvedUri}), ) this._replaceAll(res) @@ -349,18 +318,18 @@ export class PostThreadViewModel { } } - private _replaceAll(res: GetPostThread.Response) { + _replaceAll(res: GetPostThread.Response) { sortThread(res.data.thread) const keyGen = reactKeyGenerator() const thread = new PostThreadViewPostModel( this.rootStore, keyGen.next().value, - res.data.thread as GetPostThread.ThreadViewPost, + res.data.thread as AppBskyFeedDefs.ThreadViewPost, ) thread._isHighlightedPost = true thread.assignTreeModels( keyGen, - res.data.thread as GetPostThread.ThreadViewPost, + res.data.thread as AppBskyFeedDefs.ThreadViewPost, thread.uri, ) this.thread = thread @@ -368,25 +337,25 @@ export class PostThreadViewModel { } type MaybePost = - | GetPostThread.ThreadViewPost - | GetPostThread.NotFoundPost + | AppBskyFeedDefs.ThreadViewPost + | AppBskyFeedDefs.NotFoundPost | {[k: string]: unknown; $type: string} function sortThread(post: MaybePost) { if (post.notFound) { return } - post = post as GetPostThread.ThreadViewPost + post = post as AppBskyFeedDefs.ThreadViewPost if (post.replies) { post.replies.sort((a: MaybePost, b: MaybePost) => { - post = post as GetPostThread.ThreadViewPost + post = post as AppBskyFeedDefs.ThreadViewPost if (a.notFound) { return 1 } if (b.notFound) { return -1 } - a = a as GetPostThread.ThreadViewPost - b = b as GetPostThread.ThreadViewPost + a = a as AppBskyFeedDefs.ThreadViewPost + b = b as AppBskyFeedDefs.ThreadViewPost const aIsByOp = a.post.author.did === post.post.author.did const bIsByOp = b.post.author.did === post.post.author.did if (aIsByOp && bIsByOp) { diff --git a/src/state/models/post.ts b/src/state/models/post.ts index 749e98bb0..c7f2896ba 100644 --- a/src/state/models/post.ts +++ b/src/state/models/post.ts @@ -58,12 +58,12 @@ export class PostModel implements RemoveIndex<Post.Record> { // state transitions // = - private _xLoading() { + _xLoading() { this.isLoading = true this.error = '' } - private _xIdle(err?: any) { + _xIdle(err?: any) { this.isLoading = false this.hasLoaded = true this.error = cleanError(err) @@ -75,12 +75,12 @@ export class PostModel implements RemoveIndex<Post.Record> { // loader functions // = - private async _load() { + async _load() { this._xLoading() try { const urip = new AtUri(this.uri) - const res = await this.rootStore.api.app.bsky.feed.post.get({ - user: urip.host, + const res = await this.rootStore.agent.getPost({ + repo: urip.host, rkey: urip.rkey, }) // TODO @@ -94,7 +94,7 @@ export class PostModel implements RemoveIndex<Post.Record> { } } - private _replaceAll(res: Post.Record) { + _replaceAll(res: Post.Record) { this.text = res.text this.entities = res.entities this.reply = res.reply diff --git a/src/state/models/profile-view.ts b/src/state/models/profile-view.ts index 9d3eeff58..eacc6a298 100644 --- a/src/state/models/profile-view.ts +++ b/src/state/models/profile-view.ts @@ -2,15 +2,12 @@ import {makeAutoObservable, runInAction} from 'mobx' import {PickedMedia} from 'lib/media/picker' import { AppBskyActorGetProfile as GetProfile, - AppBskySystemDeclRef, - AppBskyActorUpdateProfile, + AppBskyActorProfile, + RichText, } from '@atproto/api' -type DeclRef = AppBskySystemDeclRef.Main -import {extractEntities} from 'lib/strings/rich-text-detection' import {RootStoreModel} from './root-store' import * as apilib from 'lib/api/index' import {cleanError} from 'lib/strings/errors' -import {RichText} from 'lib/strings/rich-text' export const ACTOR_TYPE_USER = 'app.bsky.system.actorUser' @@ -35,22 +32,18 @@ export class ProfileViewModel { // data did: string = '' handle: string = '' - declaration: DeclRef = { - cid: '', - actorType: '', - } creator: string = '' - displayName?: string - description?: string - avatar?: string - banner?: string + displayName?: string = '' + description?: string = '' + avatar?: string = '' + banner?: string = '' followersCount: number = 0 followsCount: number = 0 postsCount: number = 0 viewer = new ProfileViewViewerModel() // added data - descriptionRichText?: RichText + descriptionRichText?: RichText = new RichText({text: ''}) constructor( public rootStore: RootStoreModel, @@ -79,10 +72,6 @@ export class ProfileViewModel { return this.hasLoaded && !this.hasContent } - get isUser() { - return this.declaration.actorType === ACTOR_TYPE_USER - } - // public api // = @@ -111,18 +100,14 @@ export class ProfileViewModel { } if (followUri) { - await apilib.unfollow(this.rootStore, followUri) + await this.rootStore.agent.deleteFollow(followUri) runInAction(() => { this.followersCount-- this.viewer.following = undefined this.rootStore.me.follows.removeFollow(this.did) }) } else { - const res = await apilib.follow( - this.rootStore, - this.did, - this.declaration.cid, - ) + const res = await this.rootStore.agent.follow(this.did) runInAction(() => { this.followersCount++ this.viewer.following = res.uri @@ -132,49 +117,48 @@ export class ProfileViewModel { } async updateProfile( - updates: AppBskyActorUpdateProfile.InputSchema, + updates: AppBskyActorProfile.Record, newUserAvatar: PickedMedia | undefined | null, newUserBanner: PickedMedia | undefined | null, ) { - if (newUserAvatar) { - const res = await apilib.uploadBlob( - this.rootStore, - newUserAvatar.path, - newUserAvatar.mime, - ) - updates.avatar = { - cid: res.data.cid, - mimeType: newUserAvatar.mime, + await this.rootStore.agent.upsertProfile(async existing => { + existing = existing || {} + existing.displayName = updates.displayName + existing.description = updates.description + if (newUserAvatar) { + const res = await apilib.uploadBlob( + this.rootStore, + newUserAvatar.path, + newUserAvatar.mime, + ) + existing.avatar = res.data.blob + } else if (newUserAvatar === null) { + existing.avatar = undefined } - } else if (newUserAvatar === null) { - updates.avatar = null - } - if (newUserBanner) { - const res = await apilib.uploadBlob( - this.rootStore, - newUserBanner.path, - newUserBanner.mime, - ) - updates.banner = { - cid: res.data.cid, - mimeType: newUserBanner.mime, + if (newUserBanner) { + const res = await apilib.uploadBlob( + this.rootStore, + newUserBanner.path, + newUserBanner.mime, + ) + existing.banner = res.data.blob + } else if (newUserBanner === null) { + existing.banner = undefined } - } else if (newUserBanner === null) { - updates.banner = null - } - await this.rootStore.api.app.bsky.actor.updateProfile(updates) + return existing + }) await this.rootStore.me.load() await this.refresh() } async muteAccount() { - await this.rootStore.api.app.bsky.graph.mute({user: this.did}) + await this.rootStore.agent.mute(this.did) this.viewer.muted = true await this.refresh() } async unmuteAccount() { - await this.rootStore.api.app.bsky.graph.unmute({user: this.did}) + await this.rootStore.agent.unmute(this.did) this.viewer.muted = false await this.refresh() } @@ -182,13 +166,13 @@ export class ProfileViewModel { // state transitions // = - private _xLoading(isRefreshing = false) { + _xLoading(isRefreshing = false) { this.isLoading = true this.isRefreshing = isRefreshing this.error = '' } - private _xIdle(err?: any) { + _xIdle(err?: any) { this.isLoading = false this.isRefreshing = false this.hasLoaded = true @@ -201,40 +185,40 @@ export class ProfileViewModel { // loader functions // = - private async _load(isRefreshing = false) { + async _load(isRefreshing = false) { this._xLoading(isRefreshing) try { - const res = await this.rootStore.api.app.bsky.actor.getProfile( - this.params, - ) + const res = await this.rootStore.agent.getProfile(this.params) this.rootStore.profiles.overwrite(this.params.actor, res) // cache invalidation this._replaceAll(res) + await this._createRichText() this._xIdle() } catch (e: any) { this._xIdle(e) } } - private _replaceAll(res: GetProfile.Response) { + _replaceAll(res: GetProfile.Response) { this.did = res.data.did this.handle = res.data.handle - Object.assign(this.declaration, res.data.declaration) - this.creator = res.data.creator this.displayName = res.data.displayName this.description = res.data.description this.avatar = res.data.avatar this.banner = res.data.banner - this.followersCount = res.data.followersCount - this.followsCount = res.data.followsCount - this.postsCount = res.data.postsCount + this.followersCount = res.data.followersCount || 0 + this.followsCount = res.data.followsCount || 0 + this.postsCount = res.data.postsCount || 0 if (res.data.viewer) { Object.assign(this.viewer, res.data.viewer) this.rootStore.me.follows.hydrate(this.did, res.data.viewer.following) } + } + + async _createRichText() { this.descriptionRichText = new RichText( - this.description || '', - extractEntities(this.description || ''), + {text: this.description || ''}, {cleanNewlines: true}, ) + await this.descriptionRichText.detectFacets(this.rootStore.agent) } } diff --git a/src/state/models/profiles-view.ts b/src/state/models/profiles-view.ts index 4241e50e1..30e6d0442 100644 --- a/src/state/models/profiles-view.ts +++ b/src/state/models/profiles-view.ts @@ -31,7 +31,7 @@ export class ProfilesViewModel { } } try { - const promise = this.rootStore.api.app.bsky.actor.getProfile({ + const promise = this.rootStore.agent.getProfile({ actor: did, }) this.cache.set(did, promise) diff --git a/src/state/models/reposted-by-view.ts b/src/state/models/reposted-by-view.ts index 69a728d6f..c9b089c70 100644 --- a/src/state/models/reposted-by-view.ts +++ b/src/state/models/reposted-by-view.ts @@ -2,7 +2,7 @@ import {makeAutoObservable, runInAction} from 'mobx' import {AtUri} from '../../third-party/uri' import { AppBskyFeedGetRepostedBy as GetRepostedBy, - AppBskyActorRef as ActorRef, + AppBskyActorDefs, } from '@atproto/api' import {RootStoreModel} from './root-store' import {bundleAsync} from 'lib/async/bundle' @@ -11,7 +11,7 @@ import * as apilib from 'lib/api/index' const PAGE_SIZE = 30 -export type RepostedByItem = ActorRef.WithInfo +export type RepostedByItem = AppBskyActorDefs.ProfileViewBasic export class RepostedByViewModel { // state @@ -71,9 +71,9 @@ export class RepostedByViewModel { const params = Object.assign({}, this.params, { uri: this.resolvedUri, limit: PAGE_SIZE, - before: replace ? undefined : this.loadMoreCursor, + cursor: replace ? undefined : this.loadMoreCursor, }) - const res = await this.rootStore.api.app.bsky.feed.getRepostedBy(params) + const res = await this.rootStore.agent.getRepostedBy(params) if (replace) { this._replaceAll(res) } else { @@ -88,13 +88,13 @@ export class RepostedByViewModel { // state transitions // = - private _xLoading(isRefreshing = false) { + _xLoading(isRefreshing = false) { this.isLoading = true this.isRefreshing = isRefreshing this.error = '' } - private _xIdle(err?: any) { + _xIdle(err?: any) { this.isLoading = false this.isRefreshing = false this.hasLoaded = true @@ -107,7 +107,7 @@ export class RepostedByViewModel { // helper functions // = - private async _resolveUri() { + async _resolveUri() { const urip = new AtUri(this.params.uri) if (!urip.host.startsWith('did:')) { try { @@ -121,12 +121,12 @@ export class RepostedByViewModel { }) } - private _replaceAll(res: GetRepostedBy.Response) { + _replaceAll(res: GetRepostedBy.Response) { this.repostedBy = [] this._appendAll(res) } - private _appendAll(res: GetRepostedBy.Response) { + _appendAll(res: GetRepostedBy.Response) { this.loadMoreCursor = res.data.cursor this.hasMore = !!this.loadMoreCursor this.repostedBy = this.repostedBy.concat(res.data.repostedBy) diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts index d8336d005..0c2a31d28 100644 --- a/src/state/models/root-store.ts +++ b/src/state/models/root-store.ts @@ -2,8 +2,8 @@ * The root store is the base of all modeled state. */ -import {makeAutoObservable, runInAction} from 'mobx' -import {AtpAgent} from '@atproto/api' +import {makeAutoObservable} from 'mobx' +import {BskyAgent} from '@atproto/api' import {createContext, useContext} from 'react' import {DeviceEventEmitter, EmitterSubscription} from 'react-native' import * as BgScheduler from 'lib/bg-scheduler' @@ -29,7 +29,7 @@ export const appInfo = z.object({ export type AppInfo = z.infer<typeof appInfo> export class RootStoreModel { - agent: AtpAgent + agent: BskyAgent appInfo?: AppInfo log = new LogModel() session = new SessionModel(this) @@ -40,41 +40,16 @@ export class RootStoreModel { linkMetas = new LinkMetasCache(this) imageSizes = new ImageSizesCache() - // HACK - // this flag is to track the lexicon breaking refactor - // it should be removed once we get that done - // -prf - hackUpgradeNeeded = false - async hackCheckIfUpgradeNeeded() { - try { - this.log.debug('hackCheckIfUpgradeNeeded()') - const res = await fetch('https://bsky.social/xrpc/app.bsky.feed.getLikes') - await res.text() - runInAction(() => { - this.hackUpgradeNeeded = res.status !== 501 - this.log.debug( - `hackCheckIfUpgradeNeeded() said ${this.hackUpgradeNeeded}`, - ) - }) - } catch (e) { - this.log.error('Failed to hackCheckIfUpgradeNeeded', {e}) - } - } - - constructor(agent: AtpAgent) { + constructor(agent: BskyAgent) { this.agent = agent makeAutoObservable(this, { - api: false, + agent: false, serialize: false, hydrate: false, }) this.initBgFetch() } - get api() { - return this.agent.api - } - setAppInfo(info: AppInfo) { this.appInfo = info } @@ -131,7 +106,7 @@ export class RootStoreModel { /** * Called by the session model. Refreshes session-oriented state. */ - async handleSessionChange(agent: AtpAgent) { + async handleSessionChange(agent: BskyAgent) { this.log.debug('RootStoreModel:handleSessionChange') this.agent = agent this.me.clear() @@ -259,7 +234,7 @@ export class RootStoreModel { async onBgFetch(taskId: string) { this.log.debug(`Background fetch fired for task ${taskId}`) if (this.session.hasSession) { - const res = await this.api.app.bsky.notification.getCount() + const res = await this.agent.countUnreadNotifications() const hasNewNotifs = this.me.notifications.unreadCount !== res.data.count this.emitUnreadNotifications(res.data.count) this.log.debug( @@ -286,7 +261,7 @@ export class RootStoreModel { } const throwawayInst = new RootStoreModel( - new AtpAgent({service: 'http://localhost'}), + new BskyAgent({service: 'http://localhost'}), ) // this will be replaced by the loader, we just need to supply a value at init const RootStoreContext = createContext<RootStoreModel>(throwawayInst) export const RootStoreProvider = RootStoreContext.Provider diff --git a/src/state/models/session.ts b/src/state/models/session.ts index e131b2b2c..c2e10880d 100644 --- a/src/state/models/session.ts +++ b/src/state/models/session.ts @@ -1,9 +1,9 @@ import {makeAutoObservable, runInAction} from 'mobx' import { - AtpAgent, + BskyAgent, AtpSessionEvent, AtpSessionData, - ComAtprotoServerGetAccountsConfig as GetAccountsConfig, + ComAtprotoServerDescribeServer as DescribeServer, } from '@atproto/api' import normalizeUrl from 'normalize-url' import {isObj, hasProp} from 'lib/type-guards' @@ -11,7 +11,7 @@ import {networkRetry} from 'lib/async/retry' import {z} from 'zod' import {RootStoreModel} from './root-store' -export type ServiceDescription = GetAccountsConfig.OutputSchema +export type ServiceDescription = DescribeServer.OutputSchema export const activeSession = z.object({ service: z.string(), @@ -40,7 +40,7 @@ export class SessionModel { // emergency log facility to help us track down this logout issue // remove when resolved // -prf - private _log(message: string, details?: Record<string, any>) { + _log(message: string, details?: Record<string, any>) { details = details || {} details.state = { data: this.data, @@ -73,6 +73,7 @@ export class SessionModel { rootStore: false, serialize: false, hydrate: false, + hasSession: false, }) } @@ -154,7 +155,7 @@ export class SessionModel { /** * Sets the active session */ - async setActiveSession(agent: AtpAgent, did: string) { + async setActiveSession(agent: BskyAgent, did: string) { this._log('SessionModel:setActiveSession') this.data = { service: agent.service.toString(), @@ -166,7 +167,7 @@ export class SessionModel { /** * Upserts a session into the accounts */ - private persistSession( + persistSession( service: string, did: string, event: AtpSessionEvent, @@ -225,7 +226,7 @@ export class SessionModel { /** * Clears any session tokens from the accounts; used on logout. */ - private clearSessionTokens() { + clearSessionTokens() { this._log('SessionModel:clearSessionTokens') this.accounts = this.accounts.map(acct => ({ service: acct.service, @@ -239,10 +240,8 @@ export class SessionModel { /** * Fetches additional information about an account on load. */ - private async loadAccountInfo(agent: AtpAgent, did: string) { - const res = await agent.api.app.bsky.actor - .getProfile({actor: did}) - .catch(_e => undefined) + async loadAccountInfo(agent: BskyAgent, did: string) { + const res = await agent.getProfile({actor: did}).catch(_e => undefined) if (res) { return { dispayName: res.data.displayName, @@ -255,8 +254,8 @@ export class SessionModel { * Helper to fetch the accounts config settings from an account. */ async describeService(service: string): Promise<ServiceDescription> { - const agent = new AtpAgent({service}) - const res = await agent.api.com.atproto.server.getAccountsConfig({}) + const agent = new BskyAgent({service}) + const res = await agent.com.atproto.server.describeServer({}) return res.data } @@ -272,7 +271,7 @@ export class SessionModel { return false } - const agent = new AtpAgent({ + const agent = new BskyAgent({ service: account.service, persistSession: (evt: AtpSessionEvent, sess?: AtpSessionData) => { this.persistSession(account.service, account.did, evt, sess) @@ -321,7 +320,7 @@ export class SessionModel { password: string }) { this._log('SessionModel:login') - const agent = new AtpAgent({service}) + const agent = new BskyAgent({service}) await agent.login({identifier, password}) if (!agent.session) { throw new Error('Failed to establish session') @@ -355,7 +354,7 @@ export class SessionModel { inviteCode?: string }) { this._log('SessionModel:createAccount') - const agent = new AtpAgent({service}) + const agent = new BskyAgent({service}) await agent.createAccount({ handle, password, @@ -389,7 +388,7 @@ export class SessionModel { // need to evaluate why deleting the session has caused errors at times // -prf /*if (this.hasSession) { - this.rootStore.api.com.atproto.session.delete().catch((e: any) => { + this.rootStore.agent.com.atproto.session.delete().catch((e: any) => { this.rootStore.log.warn( '(Minor issue) Failed to delete session on the server', e, @@ -415,7 +414,7 @@ export class SessionModel { if (!sess) { return } - const res = await this.rootStore.api.app.bsky.actor + const res = await this.rootStore.agent .getProfile({actor: sess.did}) .catch(_e => undefined) if (res?.success) { diff --git a/src/state/models/suggested-posts-view.ts b/src/state/models/suggested-posts-view.ts index 7a5ca81b9..46bf235ff 100644 --- a/src/state/models/suggested-posts-view.ts +++ b/src/state/models/suggested-posts-view.ts @@ -72,12 +72,12 @@ export class SuggestedPostsView { // state transitions // = - private _xLoading() { + _xLoading() { this.isLoading = true this.error = '' } - private _xIdle(err?: any) { + _xIdle(err?: any) { this.isLoading = false this.hasLoaded = true this.error = cleanError(err) diff --git a/src/state/models/ui/create-account.ts b/src/state/models/ui/create-account.ts index a212fe05e..e661cb59d 100644 --- a/src/state/models/ui/create-account.ts +++ b/src/state/models/ui/create-account.ts @@ -2,7 +2,7 @@ import {makeAutoObservable} from 'mobx' import {RootStoreModel} from '../root-store' import {ServiceDescription} from '../session' import {DEFAULT_SERVICE} from 'state/index' -import {ComAtprotoAccountCreate} from '@atproto/api' +import {ComAtprotoServerCreateAccount} from '@atproto/api' import * as EmailValidator from 'email-validator' import {createFullHandle} from 'lib/strings/handles' import {cleanError} from 'lib/strings/errors' @@ -99,7 +99,7 @@ export class CreateAccountModel { }) } catch (e: any) { let errMsg = e.toString() - if (e instanceof ComAtprotoAccountCreate.InvalidInviteCodeError) { + if (e instanceof ComAtprotoServerCreateAccount.InvalidInviteCodeError) { errMsg = 'Invite code not accepted. Check that you input it correctly and try again.' } diff --git a/src/state/models/ui/profile.ts b/src/state/models/ui/profile.ts index 280541b74..59529aa39 100644 --- a/src/state/models/ui/profile.ts +++ b/src/state/models/ui/profile.ts @@ -40,7 +40,7 @@ export class ProfileUiModel { ) this.profile = new ProfileViewModel(rootStore, {actor: params.user}) this.feed = new FeedModel(rootStore, 'author', { - author: params.user, + actor: params.user, limit: 10, }) } @@ -64,16 +64,8 @@ export class ProfileUiModel { return this.profile.isRefreshing || this.currentView.isRefreshing } - get isUser() { - return this.profile.isUser - } - get selectorItems() { - if (this.isUser) { - return USER_SELECTOR_ITEMS - } else { - return USER_SELECTOR_ITEMS - } + return USER_SELECTOR_ITEMS } get selectedView() { diff --git a/src/state/models/ui/search.ts b/src/state/models/ui/search.ts index 91e1b24bf..8436b0984 100644 --- a/src/state/models/ui/search.ts +++ b/src/state/models/ui/search.ts @@ -1,6 +1,6 @@ import {makeAutoObservable, runInAction} from 'mobx' import {searchProfiles, searchPosts} from 'lib/api/search' -import {AppBskyActorProfile as Profile} from '@atproto/api' +import {AppBskyActorDefs} from '@atproto/api' import {RootStoreModel} from '../root-store' export class SearchUIModel { @@ -8,7 +8,7 @@ export class SearchUIModel { isProfilesLoading = false query: string = '' postUris: string[] = [] - profiles: Profile.View[] = [] + profiles: AppBskyActorDefs.ProfileView[] = [] constructor(public rootStore: RootStoreModel) { makeAutoObservable(this) @@ -34,10 +34,10 @@ export class SearchUIModel { this.isPostsLoading = false }) - let profiles: Profile.View[] = [] + let profiles: AppBskyActorDefs.ProfileView[] = [] if (profilesSearch?.length) { do { - const res = await this.rootStore.api.app.bsky.actor.getProfiles({ + const res = await this.rootStore.agent.getProfiles({ actors: profilesSearch.splice(0, 25).map(p => p.did), }) profiles = profiles.concat(res.data.profiles) diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts index fec1e2899..7f57d5b54 100644 --- a/src/state/models/ui/shell.ts +++ b/src/state/models/ui/shell.ts @@ -1,3 +1,4 @@ +import {AppBskyEmbedRecord} from '@atproto/api' import {RootStoreModel} from '../root-store' import {makeAutoObservable} from 'mobx' import {ProfileViewModel} from '../profile-view' @@ -111,6 +112,7 @@ export interface ComposerOptsQuote { displayName?: string avatar?: string } + embeds?: AppBskyEmbedRecord.ViewRecord['embeds'] } export interface ComposerOpts { replyTo?: ComposerOptsPostRef diff --git a/src/state/models/user-autocomplete-view.ts b/src/state/models/user-autocomplete-view.ts index 8e4211c27..ad89bb08b 100644 --- a/src/state/models/user-autocomplete-view.ts +++ b/src/state/models/user-autocomplete-view.ts @@ -1,5 +1,5 @@ import {makeAutoObservable, runInAction} from 'mobx' -import {AppBskyActorRef} from '@atproto/api' +import {AppBskyActorDefs} from '@atproto/api' import AwaitLock from 'await-lock' import {RootStoreModel} from './root-store' @@ -11,8 +11,8 @@ export class UserAutocompleteViewModel { lock = new AwaitLock() // data - follows: AppBskyActorRef.WithInfo[] = [] - searchRes: AppBskyActorRef.WithInfo[] = [] + follows: AppBskyActorDefs.ProfileViewBasic[] = [] + searchRes: AppBskyActorDefs.ProfileViewBasic[] = [] knownHandles: Set<string> = new Set() constructor(public rootStore: RootStoreModel) { @@ -76,9 +76,9 @@ export class UserAutocompleteViewModel { // internal // = - private async _getFollows() { - const res = await this.rootStore.api.app.bsky.graph.getFollows({ - user: this.rootStore.me.did || '', + async _getFollows() { + const res = await this.rootStore.agent.getFollows({ + actor: this.rootStore.me.did || '', }) runInAction(() => { this.follows = res.data.follows @@ -88,13 +88,13 @@ export class UserAutocompleteViewModel { }) } - private async _search() { - const res = await this.rootStore.api.app.bsky.actor.searchTypeahead({ + async _search() { + const res = await this.rootStore.agent.searchActorsTypeahead({ term: this.prefix, limit: 8, }) runInAction(() => { - this.searchRes = res.data.users + this.searchRes = res.data.actors for (const u of this.searchRes) { this.knownHandles.add(u.handle) } diff --git a/src/state/models/user-followers-view.ts b/src/state/models/user-followers-view.ts index 7400262a4..055032eb7 100644 --- a/src/state/models/user-followers-view.ts +++ b/src/state/models/user-followers-view.ts @@ -1,7 +1,7 @@ import {makeAutoObservable} from 'mobx' import { AppBskyGraphGetFollowers as GetFollowers, - AppBskyActorRef as ActorRef, + AppBskyActorDefs as ActorDefs, } from '@atproto/api' import {RootStoreModel} from './root-store' import {cleanError} from 'lib/strings/errors' @@ -9,7 +9,7 @@ import {bundleAsync} from 'lib/async/bundle' const PAGE_SIZE = 30 -export type FollowerItem = ActorRef.WithInfo +export type FollowerItem = ActorDefs.ProfileViewBasic export class UserFollowersViewModel { // state @@ -22,10 +22,9 @@ export class UserFollowersViewModel { loadMoreCursor?: string // data - subject: ActorRef.WithInfo = { + subject: ActorDefs.ProfileViewBasic = { did: '', handle: '', - declaration: {cid: '', actorType: ''}, } followers: FollowerItem[] = [] @@ -71,9 +70,9 @@ export class UserFollowersViewModel { try { const params = Object.assign({}, this.params, { limit: PAGE_SIZE, - before: replace ? undefined : this.loadMoreCursor, + cursor: replace ? undefined : this.loadMoreCursor, }) - const res = await this.rootStore.api.app.bsky.graph.getFollowers(params) + const res = await this.rootStore.agent.getFollowers(params) if (replace) { this._replaceAll(res) } else { @@ -88,13 +87,13 @@ export class UserFollowersViewModel { // state transitions // = - private _xLoading(isRefreshing = false) { + _xLoading(isRefreshing = false) { this.isLoading = true this.isRefreshing = isRefreshing this.error = '' } - private _xIdle(err?: any) { + _xIdle(err?: any) { this.isLoading = false this.isRefreshing = false this.hasLoaded = true @@ -107,12 +106,12 @@ export class UserFollowersViewModel { // helper functions // = - private _replaceAll(res: GetFollowers.Response) { + _replaceAll(res: GetFollowers.Response) { this.followers = [] this._appendAll(res) } - private _appendAll(res: GetFollowers.Response) { + _appendAll(res: GetFollowers.Response) { this.loadMoreCursor = res.data.cursor this.hasMore = !!this.loadMoreCursor this.followers = this.followers.concat(res.data.followers) diff --git a/src/state/models/user-follows-view.ts b/src/state/models/user-follows-view.ts index 7d28d7ebd..6d9d84592 100644 --- a/src/state/models/user-follows-view.ts +++ b/src/state/models/user-follows-view.ts @@ -1,7 +1,7 @@ import {makeAutoObservable} from 'mobx' import { AppBskyGraphGetFollows as GetFollows, - AppBskyActorRef as ActorRef, + AppBskyActorDefs as ActorDefs, } from '@atproto/api' import {RootStoreModel} from './root-store' import {cleanError} from 'lib/strings/errors' @@ -9,7 +9,7 @@ import {bundleAsync} from 'lib/async/bundle' const PAGE_SIZE = 30 -export type FollowItem = ActorRef.WithInfo +export type FollowItem = ActorDefs.ProfileViewBasic export class UserFollowsViewModel { // state @@ -22,10 +22,9 @@ export class UserFollowsViewModel { loadMoreCursor?: string // data - subject: ActorRef.WithInfo = { + subject: ActorDefs.ProfileViewBasic = { did: '', handle: '', - declaration: {cid: '', actorType: ''}, } follows: FollowItem[] = [] @@ -71,9 +70,9 @@ export class UserFollowsViewModel { try { const params = Object.assign({}, this.params, { limit: PAGE_SIZE, - before: replace ? undefined : this.loadMoreCursor, + cursor: replace ? undefined : this.loadMoreCursor, }) - const res = await this.rootStore.api.app.bsky.graph.getFollows(params) + const res = await this.rootStore.agent.getFollows(params) if (replace) { this._replaceAll(res) } else { @@ -88,13 +87,13 @@ export class UserFollowsViewModel { // state transitions // = - private _xLoading(isRefreshing = false) { + _xLoading(isRefreshing = false) { this.isLoading = true this.isRefreshing = isRefreshing this.error = '' } - private _xIdle(err?: any) { + _xIdle(err?: any) { this.isLoading = false this.isRefreshing = false this.hasLoaded = true @@ -107,12 +106,12 @@ export class UserFollowsViewModel { // helper functions // = - private _replaceAll(res: GetFollows.Response) { + _replaceAll(res: GetFollows.Response) { this.follows = [] this._appendAll(res) } - private _appendAll(res: GetFollows.Response) { + _appendAll(res: GetFollows.Response) { this.loadMoreCursor = res.data.cursor this.hasMore = !!this.loadMoreCursor this.follows = this.follows.concat(res.data.follows) diff --git a/src/view/com/auth/create/CreateAccount.tsx b/src/view/com/auth/create/CreateAccount.tsx index 618c15cf5..6ece903d6 100644 --- a/src/view/com/auth/create/CreateAccount.tsx +++ b/src/view/com/auth/create/CreateAccount.tsx @@ -75,16 +75,14 @@ export const CreateAccount = observer( {model.step === 3 && <Step3 model={model} />} </View> <View style={[s.flexRow, s.pl20, s.pr20]}> - <TouchableOpacity onPress={onPressBackInner}> + <TouchableOpacity onPress={onPressBackInner} testID="backBtn"> <Text type="xl" style={pal.link}> Back </Text> </TouchableOpacity> <View style={s.flex1} /> {model.canNext ? ( - <TouchableOpacity - testID="createAccountButton" - onPress={onPressNext}> + <TouchableOpacity testID="nextBtn" onPress={onPressNext}> {model.isProcessing ? ( <ActivityIndicator /> ) : ( @@ -95,7 +93,7 @@ export const CreateAccount = observer( </TouchableOpacity> ) : model.didServiceDescriptionFetchFail ? ( <TouchableOpacity - testID="registerRetryButton" + testID="retryConnectBtn" onPress={onPressRetryConnect}> <Text type="xl-bold" style={[pal.link, s.pr5]}> Retry diff --git a/src/view/com/auth/create/Step1.tsx b/src/view/com/auth/create/Step1.tsx index 0a628f9d0..ca964ede2 100644 --- a/src/view/com/auth/create/Step1.tsx +++ b/src/view/com/auth/create/Step1.tsx @@ -60,12 +60,14 @@ export const Step1 = observer(({model}: {model: CreateAccountModel}) => { This is the company that keeps you online. </Text> <Option + testID="blueskyServerBtn" isSelected={isDefaultSelected} label="Bluesky" help=" (default)" onPress={onPressDefault} /> <Option + testID="otherServerBtn" isSelected={!isDefaultSelected} label="Other" onPress={onPressOther}> @@ -74,6 +76,7 @@ export const Step1 = observer(({model}: {model: CreateAccountModel}) => { Enter the address of your provider: </Text> <TextInput + testID="customServerInput" icon="globe" placeholder="Hosting provider address" value={model.serviceUrl} @@ -83,12 +86,14 @@ export const Step1 = observer(({model}: {model: CreateAccountModel}) => { {LOGIN_INCLUDE_DEV_SERVERS && ( <View style={[s.flexRow, s.mt10]}> <Button + testID="stagingServerBtn" type="default" style={s.mr5} label="Staging" onPress={() => onDebugChangeServiceUrl(STAGING_SERVICE)} /> <Button + testID="localDevServerBtn" type="default" label="Dev Server" onPress={() => onDebugChangeServiceUrl(LOCAL_DEV_SERVICE)} @@ -112,11 +117,13 @@ function Option({ label, help, onPress, + testID, }: React.PropsWithChildren<{ isSelected: boolean label: string help?: string onPress: () => void + testID?: string }>) { const theme = useTheme() const pal = usePalette('default') @@ -129,7 +136,7 @@ function Option({ return ( <View style={[styles.option, pal.border]}> - <TouchableWithoutFeedback onPress={onPress}> + <TouchableWithoutFeedback onPress={onPress} testID={testID}> <View style={styles.optionHeading}> <View style={[styles.circle, pal.border]}> {isSelected ? ( diff --git a/src/view/com/auth/create/Step2.tsx b/src/view/com/auth/create/Step2.tsx index f115bf6ac..8df997bd3 100644 --- a/src/view/com/auth/create/Step2.tsx +++ b/src/view/com/auth/create/Step2.tsx @@ -59,6 +59,7 @@ export const Step2 = observer(({model}: {model: CreateAccountModel}) => { Email address </Text> <TextInput + testID="emailInput" icon="envelope" placeholder="Enter your email address" value={model.email} @@ -72,6 +73,7 @@ export const Step2 = observer(({model}: {model: CreateAccountModel}) => { Password </Text> <TextInput + testID="passwordInput" icon="lock" placeholder="Choose your password" value={model.password} @@ -86,7 +88,7 @@ export const Step2 = observer(({model}: {model: CreateAccountModel}) => { Legal check </Text> <TouchableOpacity - testID="registerIs13Input" + testID="is13Input" style={[styles.toggleBtn, pal.border]} onPress={() => model.setIs13(!model.is13)}> <View style={[pal.borderDark, styles.checkbox]}> diff --git a/src/view/com/auth/create/Step3.tsx b/src/view/com/auth/create/Step3.tsx index 652591171..13ab39a10 100644 --- a/src/view/com/auth/create/Step3.tsx +++ b/src/view/com/auth/create/Step3.tsx @@ -17,6 +17,7 @@ export const Step3 = observer(({model}: {model: CreateAccountModel}) => { <StepHeader step="3" title="Your user handle" /> <View style={s.pb10}> <TextInput + testID="handleInput" icon="at" placeholder="eg alice" value={model.handle} diff --git a/src/view/com/auth/login/Login.tsx b/src/view/com/auth/login/Login.tsx index f99e72daa..eff1642f0 100644 --- a/src/view/com/auth/login/Login.tsx +++ b/src/view/com/auth/login/Login.tsx @@ -13,7 +13,7 @@ import { FontAwesomeIconStyle, } from '@fortawesome/react-native-fontawesome' import * as EmailValidator from 'email-validator' -import AtpAgent from '@atproto/api' +import {BskyAgent} from '@atproto/api' import {useAnalytics} from 'lib/analytics' import {Text} from '../../util/text/Text' import {UserAvatar} from '../../util/UserAvatar' @@ -506,8 +506,8 @@ const ForgotPasswordForm = ({ setIsProcessing(true) try { - const agent = new AtpAgent({service: serviceUrl}) - await agent.api.com.atproto.account.requestPasswordReset({email}) + const agent = new BskyAgent({service: serviceUrl}) + await agent.com.atproto.server.requestPasswordReset({email}) onEmailSent() } catch (e: any) { const errMsg = e.toString() @@ -648,8 +648,8 @@ const SetNewPasswordForm = ({ setIsProcessing(true) try { - const agent = new AtpAgent({service: serviceUrl}) - await agent.api.com.atproto.account.resetPassword({ + const agent = new BskyAgent({service: serviceUrl}) + await agent.com.atproto.server.resetPassword({ token: resetCode, password, }) diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 572eea927..6009debdd 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useRef, useState} from 'react' +import React from 'react' import {observer} from 'mobx-react-lite' import { ActivityIndicator, @@ -13,6 +13,7 @@ import { } from 'react-native' import LinearGradient from 'react-native-linear-gradient' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {RichText} from '@atproto/api' import {useAnalytics} from 'lib/analytics' import {UserAutocompleteViewModel} from 'state/models/user-autocomplete-view' import {ExternalEmbed} from './ExternalEmbed' @@ -30,11 +31,11 @@ import {SelectPhotoBtn} from './photos/SelectPhotoBtn' import {OpenCameraBtn} from './photos/OpenCameraBtn' import {SelectedPhotos} from './photos/SelectedPhotos' import {usePalette} from 'lib/hooks/usePalette' -import QuoteEmbed from '../util/PostEmbeds/QuoteEmbed' +import QuoteEmbed from '../util/post-embeds/QuoteEmbed' import {useExternalLinkFetch} from './useExternalLinkFetch' import {isDesktopWeb} from 'platform/detection' -const MAX_TEXT_LENGTH = 256 +const MAX_GRAPHEME_LENGTH = 300 export const ComposePost = observer(function ComposePost({ replyTo, @@ -50,17 +51,23 @@ export const ComposePost = observer(function ComposePost({ const {track} = useAnalytics() const pal = usePalette('default') const store = useStores() - const textInput = useRef<TextInputRef>(null) - const [isProcessing, setIsProcessing] = useState(false) - const [processingState, setProcessingState] = useState('') - const [error, setError] = useState('') - const [text, setText] = useState('') - const [quote, setQuote] = useState<ComposerOpts['quote'] | undefined>( + const textInput = React.useRef<TextInputRef>(null) + const [isProcessing, setIsProcessing] = React.useState(false) + const [processingState, setProcessingState] = React.useState('') + const [error, setError] = React.useState('') + const [richtext, setRichText] = React.useState(new RichText({text: ''})) + const graphemeLength = React.useMemo( + () => richtext.graphemeLength, + [richtext], + ) + const [quote, setQuote] = React.useState<ComposerOpts['quote'] | undefined>( initQuote, ) const {extLink, setExtLink} = useExternalLinkFetch({setQuote}) - const [suggestedLinks, setSuggestedLinks] = useState<Set<string>>(new Set()) - const [selectedPhotos, setSelectedPhotos] = useState<string[]>([]) + const [suggestedLinks, setSuggestedLinks] = React.useState<Set<string>>( + new Set(), + ) + const [selectedPhotos, setSelectedPhotos] = React.useState<string[]>([]) const autocompleteView = React.useMemo<UserAutocompleteViewModel>( () => new UserAutocompleteViewModel(store), @@ -78,11 +85,11 @@ export const ComposePost = observer(function ComposePost({ }, [textInput, onClose]) // initial setup - useEffect(() => { + React.useEffect(() => { autocompleteView.setup() }, [autocompleteView]) - useEffect(() => { + React.useEffect(() => { // HACK // wait a moment before focusing the input to resolve some layout bugs with the keyboard-avoiding-view // -prf @@ -132,18 +139,18 @@ export const ComposePost = observer(function ComposePost({ if (isProcessing) { return } - if (text.length > MAX_TEXT_LENGTH) { + if (richtext.graphemeLength > MAX_GRAPHEME_LENGTH) { return } setError('') - if (text.trim().length === 0 && selectedPhotos.length === 0) { + if (richtext.text.trim().length === 0 && selectedPhotos.length === 0) { setError('Did you want to say anything?') return false } setIsProcessing(true) try { await apilib.post(store, { - rawText: text, + rawText: richtext.text, replyTo: replyTo?.uri, images: selectedPhotos, quote: quote, @@ -172,7 +179,7 @@ export const ComposePost = observer(function ComposePost({ Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`) }, [ isProcessing, - text, + richtext, setError, setIsProcessing, replyTo, @@ -187,7 +194,7 @@ export const ComposePost = observer(function ComposePost({ track, ]) - const canPost = text.length <= MAX_TEXT_LENGTH + const canPost = graphemeLength <= MAX_GRAPHEME_LENGTH const selectTextInputPlaceholder = replyTo ? 'Write your reply' @@ -215,7 +222,7 @@ export const ComposePost = observer(function ComposePost({ </View> ) : canPost ? ( <TouchableOpacity - testID="composerPublishButton" + testID="composerPublishBtn" onPress={onPressPublish}> <LinearGradient colors={[gradients.blueLight.start, gradients.blueLight.end]} @@ -271,42 +278,41 @@ export const ComposePost = observer(function ComposePost({ <UserAvatar avatar={store.me.avatar} size={50} /> <TextInput ref={textInput} - text={text} + richtext={richtext} placeholder={selectTextInputPlaceholder} suggestedLinks={suggestedLinks} autocompleteView={autocompleteView} - onTextChanged={setText} + setRichText={setRichText} onPhotoPasted={onPhotoPasted} onSuggestedLinksChanged={setSuggestedLinks} onError={setError} /> </View> - {quote ? ( - <View style={s.mt5}> - <QuoteEmbed quote={quote} /> - </View> - ) : undefined} - <SelectedPhotos selectedPhotos={selectedPhotos} onSelectPhotos={onSelectPhotos} /> - {!selectedPhotos.length && extLink && ( + {selectedPhotos.length === 0 && extLink && ( <ExternalEmbed link={extLink} onRemove={() => setExtLink(undefined)} /> )} + {quote ? ( + <View style={s.mt5}> + <QuoteEmbed quote={quote} /> + </View> + ) : undefined} </ScrollView> {!extLink && selectedPhotos.length === 0 && - suggestedLinks.size > 0 && - !quote ? ( + suggestedLinks.size > 0 ? ( <View style={s.mb5}> {Array.from(suggestedLinks).map(url => ( <TouchableOpacity key={`suggested-${url}`} + testID="addLinkCardBtn" style={[pal.borderDark, styles.addExtLinkBtn]} onPress={() => onPressAddLinkCard(url)}> <Text style={pal.text}> @@ -318,17 +324,17 @@ export const ComposePost = observer(function ComposePost({ ) : null} <View style={[pal.border, styles.bottomBar]}> <SelectPhotoBtn - enabled={!quote && selectedPhotos.length < 4} + enabled={selectedPhotos.length < 4} selectedPhotos={selectedPhotos} onSelectPhotos={setSelectedPhotos} /> <OpenCameraBtn - enabled={!quote && selectedPhotos.length < 4} + enabled={selectedPhotos.length < 4} selectedPhotos={selectedPhotos} onSelectPhotos={setSelectedPhotos} /> <View style={s.flex1} /> - <CharProgress count={text.length} /> + <CharProgress count={graphemeLength} /> </View> </SafeAreaView> </TouchableWithoutFeedback> @@ -408,6 +414,7 @@ const styles = StyleSheet.create({ borderRadius: 24, paddingHorizontal: 16, paddingVertical: 12, + marginHorizontal: 10, marginBottom: 4, }, bottomBar: { diff --git a/src/view/com/composer/char-progress/CharProgress.tsx b/src/view/com/composer/char-progress/CharProgress.tsx index b17cad1ba..eaaaea5e5 100644 --- a/src/view/com/composer/char-progress/CharProgress.tsx +++ b/src/view/com/composer/char-progress/CharProgress.tsx @@ -8,26 +8,24 @@ import ProgressPie from 'react-native-progress/Pie' import {s} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' -const MAX_TEXT_LENGTH = 256 -const DANGER_TEXT_LENGTH = MAX_TEXT_LENGTH +const MAX_LENGTH = 300 +const DANGER_LENGTH = MAX_LENGTH export function CharProgress({count}: {count: number}) { const pal = usePalette('default') - const textColor = count > DANGER_TEXT_LENGTH ? '#e60000' : pal.colors.text - const circleColor = count > DANGER_TEXT_LENGTH ? '#e60000' : pal.colors.link + const textColor = count > DANGER_LENGTH ? '#e60000' : pal.colors.text + const circleColor = count > DANGER_LENGTH ? '#e60000' : pal.colors.link return ( <> - <Text style={[s.mr10, {color: textColor}]}> - {MAX_TEXT_LENGTH - count} - </Text> + <Text style={[s.mr10, {color: textColor}]}>{MAX_LENGTH - count}</Text> <View> - {count > DANGER_TEXT_LENGTH ? ( + {count > DANGER_LENGTH ? ( <ProgressPie size={30} borderWidth={4} borderColor={circleColor} color={circleColor} - progress={Math.min((count - MAX_TEXT_LENGTH) / MAX_TEXT_LENGTH, 1)} + progress={Math.min((count - MAX_LENGTH) / MAX_LENGTH, 1)} /> ) : ( <ProgressCircle @@ -35,7 +33,7 @@ export function CharProgress({count}: {count: number}) { borderWidth={1} borderColor={pal.colors.border} color={circleColor} - progress={count / MAX_TEXT_LENGTH} + progress={count / MAX_LENGTH} /> )} </View> diff --git a/src/view/com/composer/photos/OpenCameraBtn.tsx b/src/view/com/composer/photos/OpenCameraBtn.tsx index cf4a4c7d1..118728781 100644 --- a/src/view/com/composer/photos/OpenCameraBtn.tsx +++ b/src/view/com/composer/photos/OpenCameraBtn.tsx @@ -76,7 +76,11 @@ export function OpenCameraBtn({ hitSlop={HITSLOP}> <FontAwesomeIcon icon="camera" - style={(enabled ? pal.link : pal.textLight) as FontAwesomeIconStyle} + style={ + (enabled + ? pal.link + : [pal.textLight, s.dimmed]) as FontAwesomeIconStyle + } size={24} /> </TouchableOpacity> diff --git a/src/view/com/composer/photos/SelectPhotoBtn.tsx b/src/view/com/composer/photos/SelectPhotoBtn.tsx index bdcb0534a..888118a85 100644 --- a/src/view/com/composer/photos/SelectPhotoBtn.tsx +++ b/src/view/com/composer/photos/SelectPhotoBtn.tsx @@ -86,7 +86,11 @@ export function SelectPhotoBtn({ hitSlop={HITSLOP}> <FontAwesomeIcon icon={['far', 'image']} - style={(enabled ? pal.link : pal.textLight) as FontAwesomeIconStyle} + style={ + (enabled + ? pal.link + : [pal.textLight, s.dimmed]) as FontAwesomeIconStyle + } size={24} /> </TouchableOpacity> diff --git a/src/view/com/composer/text-input/TextInput.tsx b/src/view/com/composer/text-input/TextInput.tsx index e72b41f0a..393d168fe 100644 --- a/src/view/com/composer/text-input/TextInput.tsx +++ b/src/view/com/composer/text-input/TextInput.tsx @@ -9,13 +9,13 @@ import PasteInput, { PastedFile, PasteInputRef, } from '@mattermost/react-native-paste-input' +import {AppBskyRichtextFacet, RichText} from '@atproto/api' import isEqual from 'lodash.isequal' import {UserAutocompleteViewModel} from 'state/models/user-autocomplete-view' import {Autocomplete} from './mobile/Autocomplete' import {Text} from 'view/com/util/text/Text' import {useStores} from 'state/index' import {cleanError} from 'lib/strings/errors' -import {detectLinkables, extractEntities} from 'lib/strings/rich-text-detection' import {getImageDim} from 'lib/media/manip' import {cropAndCompressFlow} from 'lib/media/picker' import {getMentionAt, insertMentionAt} from 'lib/strings/mention-manip' @@ -33,11 +33,11 @@ export interface TextInputRef { } interface TextInputProps { - text: string + richtext: RichText placeholder: string suggestedLinks: Set<string> autocompleteView: UserAutocompleteViewModel - onTextChanged: (v: string) => void + setRichText: (v: RichText) => void onPhotoPasted: (uri: string) => void onSuggestedLinksChanged: (uris: Set<string>) => void onError: (err: string) => void @@ -51,11 +51,11 @@ interface Selection { export const TextInput = React.forwardRef( ( { - text, + richtext, placeholder, suggestedLinks, autocompleteView, - onTextChanged, + setRichText, onPhotoPasted, onSuggestedLinksChanged, onError, @@ -92,7 +92,9 @@ export const TextInput = React.forwardRef( const onChangeText = React.useCallback( (newText: string) => { - onTextChanged(newText) + const newRt = new RichText({text: newText}) + newRt.detectFacetsWithoutResolution() + setRichText(newRt) const prefix = getMentionAt( newText, @@ -105,20 +107,21 @@ export const TextInput = React.forwardRef( autocompleteView.setActive(false) } - const ents = extractEntities(newText)?.filter( - ent => ent.type === 'link', - ) - const set = new Set(ents ? ents.map(e => e.value) : []) + const set: Set<string> = new Set() + if (newRt.facets) { + for (const facet of newRt.facets) { + for (const feature of facet.features) { + if (AppBskyRichtextFacet.isLink(feature)) { + set.add(feature.uri) + } + } + } + } if (!isEqual(set, suggestedLinks)) { onSuggestedLinksChanged(set) } }, - [ - onTextChanged, - autocompleteView, - suggestedLinks, - onSuggestedLinksChanged, - ], + [setRichText, autocompleteView, suggestedLinks, onSuggestedLinksChanged], ) const onPaste = React.useCallback( @@ -159,31 +162,35 @@ export const TextInput = React.forwardRef( const onSelectAutocompleteItem = React.useCallback( (item: string) => { onChangeText( - insertMentionAt(text, textInputSelection.current?.start || 0, item), + insertMentionAt( + richtext.text, + textInputSelection.current?.start || 0, + item, + ), ) autocompleteView.setActive(false) }, - [onChangeText, text, autocompleteView], + [onChangeText, richtext, autocompleteView], ) const textDecorated = React.useMemo(() => { let i = 0 - return detectLinkables(text).map(v => { - if (typeof v === 'string') { + return Array.from(richtext.segments()).map(segment => { + if (!segment.facet) { return ( <Text key={i++} style={[pal.text, styles.textInputFormatting]}> - {v} + {segment.text} </Text> ) } else { return ( <Text key={i++} style={[pal.link, styles.textInputFormatting]}> - {v.link} + {segment.text} </Text> ) } }) - }, [text, pal.link, pal.text]) + }, [richtext, pal.link, pal.text]) return ( <View style={styles.container}> diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index 4b23e891b..ad891fa5b 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -1,5 +1,6 @@ import React from 'react' import {StyleSheet, View} from 'react-native' +import {RichText} from '@atproto/api' import {useEditor, EditorContent, JSONContent} from '@tiptap/react' import {Document} from '@tiptap/extension-document' import {Link} from '@tiptap/extension-link' @@ -17,11 +18,11 @@ export interface TextInputRef { } interface TextInputProps { - text: string + richtext: RichText placeholder: string suggestedLinks: Set<string> autocompleteView: UserAutocompleteViewModel - onTextChanged: (v: string) => void + setRichText: (v: RichText) => void onPhotoPasted: (uri: string) => void onSuggestedLinksChanged: (uris: Set<string>) => void onError: (err: string) => void @@ -30,11 +31,11 @@ interface TextInputProps { export const TextInput = React.forwardRef( ( { - text, + richtext, placeholder, suggestedLinks, autocompleteView, - onTextChanged, + setRichText, // onPhotoPasted, TODO onSuggestedLinksChanged, }: // onError, TODO @@ -60,15 +61,15 @@ export const TextInput = React.forwardRef( }), Text, ], - content: text, + content: richtext.text.toString(), autofocus: true, editable: true, injectCSS: true, onUpdate({editor: editorProp}) { const json = editorProp.getJSON() - const newText = editorJsonToText(json).trim() - onTextChanged(newText) + const newRt = new RichText({text: editorJsonToText(json).trim()}) + setRichText(newRt) const newSuggestedLinks = new Set(editorJsonToLinks(json)) if (!isEqual(newSuggestedLinks, suggestedLinks)) { diff --git a/src/view/com/discover/SuggestedFollows.tsx b/src/view/com/discover/SuggestedFollows.tsx index 0d09038ba..e4ada5204 100644 --- a/src/view/com/discover/SuggestedFollows.tsx +++ b/src/view/com/discover/SuggestedFollows.tsx @@ -1,6 +1,6 @@ import React from 'react' import {StyleSheet, View} from 'react-native' -import {AppBskyActorRef, AppBskyActorProfile} from '@atproto/api' +import {AppBskyActorDefs} from '@atproto/api' import {RefWithInfoAndFollowers} from 'state/models/discovery/foafs' import {ProfileCardWithFollowBtn} from '../profile/ProfileCard' import {Text} from '../util/text/Text' @@ -12,9 +12,9 @@ export const SuggestedFollows = ({ }: { title: string suggestions: ( - | AppBskyActorRef.WithInfo + | AppBskyActorDefs.ProfileViewBasic + | AppBskyActorDefs.ProfileView | RefWithInfoAndFollowers - | AppBskyActorProfile.View )[] }) => { const pal = usePalette('default') @@ -28,7 +28,6 @@ export const SuggestedFollows = ({ <ProfileCardWithFollowBtn key={item.did} did={item.did} - declarationCid={item.declaration.cid} handle={item.handle} displayName={item.displayName} avatar={item.avatar} @@ -36,12 +35,12 @@ export const SuggestedFollows = ({ noBorder description={ item.description - ? (item as AppBskyActorProfile.View).description + ? (item as AppBskyActorDefs.ProfileView).description : '' } followers={ item.followers - ? (item.followers as AppBskyActorProfile.View[]) + ? (item.followers as AppBskyActorDefs.ProfileView[]) : undefined } /> diff --git a/src/view/com/modals/ChangeHandle.tsx b/src/view/com/modals/ChangeHandle.tsx index f15f7ca43..37bad6957 100644 --- a/src/view/com/modals/ChangeHandle.tsx +++ b/src/view/com/modals/ChangeHandle.tsx @@ -105,7 +105,7 @@ export function Component({onChanged}: {onChanged: () => void}) { track('EditHandle:SetNewHandle') const newHandle = isCustom ? handle : createFullHandle(handle, userDomain) store.log.debug(`Updating handle to ${newHandle}`) - await store.api.com.atproto.handle.update({ + await store.agent.updateHandle({ handle: newHandle, }) store.shell.closeModal() @@ -310,7 +310,7 @@ function CustomHandleForm({ try { setIsVerifying(true) setError('') - const res = await store.api.com.atproto.handle.resolve({handle}) + const res = await store.agent.com.atproto.identity.resolveHandle({handle}) if (res.data.did === store.me.did) { setCanSave(true) } else { @@ -331,7 +331,7 @@ function CustomHandleForm({ canSave, onPressSave, store.log, - store.api, + store.agent, ]) // rendering diff --git a/src/view/com/modals/Confirm.tsx b/src/view/com/modals/Confirm.tsx index 60c104f99..2bfcf4118 100644 --- a/src/view/com/modals/Confirm.tsx +++ b/src/view/com/modals/Confirm.tsx @@ -39,7 +39,7 @@ export function Component({ } } return ( - <View style={[s.flex1, s.pl10, s.pr10]}> + <View testID="confirmModal" style={[s.flex1, s.pl10, s.pr10]}> <Text style={styles.title}>{title}</Text> {typeof message === 'string' ? ( <Text style={styles.description}>{message}</Text> @@ -56,7 +56,7 @@ export function Component({ <ActivityIndicator /> </View> ) : ( - <TouchableOpacity style={s.mt10} onPress={onPress}> + <TouchableOpacity testID="confirmBtn" style={s.mt10} onPress={onPress}> <LinearGradient colors={[gradients.blueLight.start, gradients.blueLight.end]} start={{x: 0, y: 0}} diff --git a/src/view/com/modals/DeleteAccount.tsx b/src/view/com/modals/DeleteAccount.tsx index 23cd9eb82..353122163 100644 --- a/src/view/com/modals/DeleteAccount.tsx +++ b/src/view/com/modals/DeleteAccount.tsx @@ -32,7 +32,7 @@ export function Component({}: {}) { setError('') setIsProcessing(true) try { - await store.api.com.atproto.account.requestDelete() + await store.agent.com.atproto.server.requestAccountDelete() setIsEmailSent(true) } catch (e: any) { setError(cleanError(e)) @@ -43,7 +43,7 @@ export function Component({}: {}) { setError('') setIsProcessing(true) try { - await store.api.com.atproto.account.delete({ + await store.agent.com.atproto.server.deleteAccount({ did: store.me.did, password, token: confirmCode, diff --git a/src/view/com/modals/EditProfile.tsx b/src/view/com/modals/EditProfile.tsx index 6eb21d17d..0b81d7f39 100644 --- a/src/view/com/modals/EditProfile.tsx +++ b/src/view/com/modals/EditProfile.tsx @@ -123,7 +123,7 @@ export function Component({ } return ( - <View style={[s.flex1, pal.view]}> + <View style={[s.flex1, pal.view]} testID="editProfileModal"> <ScrollView style={styles.inner}> <Text style={[styles.title, pal.text]}>Edit my profile</Text> <View style={styles.photos}> @@ -147,6 +147,7 @@ export function Component({ <View> <Text style={[styles.label, pal.text]}>Display Name</Text> <TextInput + testID="editProfileDisplayNameInput" style={[styles.textInput, pal.text]} placeholder="e.g. Alice Roberts" placeholderTextColor={colors.gray4} @@ -157,6 +158,7 @@ export function Component({ <View style={s.pb10}> <Text style={[styles.label, pal.text]}>Description</Text> <TextInput + testID="editProfileDescriptionInput" style={[styles.textArea, pal.text]} placeholder="e.g. Artist, dog-lover, and memelord." placeholderTextColor={colors.gray4} @@ -171,7 +173,10 @@ export function Component({ <ActivityIndicator /> </View> ) : ( - <TouchableOpacity style={s.mt10} onPress={onPressSave}> + <TouchableOpacity + testID="editProfileSaveBtn" + style={s.mt10} + onPress={onPressSave}> <LinearGradient colors={[gradients.blueLight.start, gradients.blueLight.end]} start={{x: 0, y: 0}} @@ -181,7 +186,10 @@ export function Component({ </LinearGradient> </TouchableOpacity> )} - <TouchableOpacity style={s.mt5} onPress={onPressCancel}> + <TouchableOpacity + testID="editProfileCancelBtn" + style={s.mt5} + onPress={onPressCancel}> <View style={[styles.btn]}> <Text style={[s.black, s.bold, pal.text]}>Cancel</Text> </View> diff --git a/src/view/com/modals/ReportAccount.tsx b/src/view/com/modals/ReportAccount.tsx index c9ee004b8..601bccbd1 100644 --- a/src/view/com/modals/ReportAccount.tsx +++ b/src/view/com/modals/ReportAccount.tsx @@ -5,7 +5,7 @@ import { TouchableOpacity, View, } from 'react-native' -import {ComAtprotoReportReasonType} from '@atproto/api' +import {ComAtprotoModerationDefs} from '@atproto/api' import LinearGradient from 'react-native-linear-gradient' import {useStores} from 'state/index' import {s, colors, gradients} from 'lib/styles' @@ -39,16 +39,16 @@ export function Component({did}: {did: string}) { setIsProcessing(true) try { // NOTE: we should update the lexicon of reasontype to include more options -prf - let reasonType = ComAtprotoReportReasonType.OTHER + let reasonType = ComAtprotoModerationDefs.REASONOTHER if (issue === 'spam') { - reasonType = ComAtprotoReportReasonType.SPAM + reasonType = ComAtprotoModerationDefs.REASONSPAM } const reason = ITEMS.find(item => item.key === issue)?.label || '' - await store.api.com.atproto.report.create({ + await store.agent.com.atproto.moderation.createReport({ reasonType, reason, subject: { - $type: 'com.atproto.repo.repoRef', + $type: 'com.atproto.admin.defs#repoRef', did, }, }) @@ -61,12 +61,18 @@ export function Component({did}: {did: string}) { } } return ( - <View style={[s.flex1, s.pl10, s.pr10, pal.view]}> + <View + testID="reportAccountModal" + style={[s.flex1, s.pl10, s.pr10, pal.view]}> <Text style={[pal.text, styles.title]}>Report account</Text> <Text style={[pal.textLight, styles.description]}> What is the issue with this account? </Text> - <RadioGroup items={ITEMS} onSelect={onSelectIssue} /> + <RadioGroup + testID="reportAccountRadios" + items={ITEMS} + onSelect={onSelectIssue} + /> {error ? ( <View style={s.mt10}> <ErrorMessage message={error} /> @@ -77,7 +83,10 @@ export function Component({did}: {did: string}) { <ActivityIndicator /> </View> ) : issue ? ( - <TouchableOpacity style={s.mt10} onPress={onPress}> + <TouchableOpacity + testID="sendReportBtn" + style={s.mt10} + onPress={onPress}> <LinearGradient colors={[gradients.blueLight.start, gradients.blueLight.end]} start={{x: 0, y: 0}} diff --git a/src/view/com/modals/ReportPost.tsx b/src/view/com/modals/ReportPost.tsx index 3e876c6c8..01a132af0 100644 --- a/src/view/com/modals/ReportPost.tsx +++ b/src/view/com/modals/ReportPost.tsx @@ -5,7 +5,7 @@ import { TouchableOpacity, View, } from 'react-native' -import {ComAtprotoReportReasonType} from '@atproto/api' +import {ComAtprotoModerationDefs} from '@atproto/api' import LinearGradient from 'react-native-linear-gradient' import {useStores} from 'state/index' import {s, colors, gradients} from 'lib/styles' @@ -46,16 +46,16 @@ export function Component({ setIsProcessing(true) try { // NOTE: we should update the lexicon of reasontype to include more options -prf - let reasonType = ComAtprotoReportReasonType.OTHER + let reasonType = ComAtprotoModerationDefs.REASONOTHER if (issue === 'spam') { - reasonType = ComAtprotoReportReasonType.SPAM + reasonType = ComAtprotoModerationDefs.REASONSPAM } const reason = ITEMS.find(item => item.key === issue)?.label || '' - await store.api.com.atproto.report.create({ + await store.agent.createModerationReport({ reasonType, reason, subject: { - $type: 'com.atproto.repo.recordRef', + $type: 'com.atproto.repo.strongRef', uri: postUri, cid: postCid, }, @@ -69,12 +69,16 @@ export function Component({ } } return ( - <View style={[s.flex1, s.pl10, s.pr10, pal.view]}> + <View testID="reportPostModal" style={[s.flex1, s.pl10, s.pr10, pal.view]}> <Text style={[pal.text, styles.title]}>Report post</Text> <Text style={[pal.textLight, styles.description]}> What is the issue with this post? </Text> - <RadioGroup items={ITEMS} onSelect={onSelectIssue} /> + <RadioGroup + testID="reportPostRadios" + items={ITEMS} + onSelect={onSelectIssue} + /> {error ? ( <View style={s.mt10}> <ErrorMessage message={error} /> @@ -85,7 +89,10 @@ export function Component({ <ActivityIndicator /> </View> ) : issue ? ( - <TouchableOpacity style={s.mt10} onPress={onPress}> + <TouchableOpacity + testID="sendReportBtn" + style={s.mt10} + onPress={onPress}> <LinearGradient colors={[gradients.blueLight.start, gradients.blueLight.end]} start={{x: 0, y: 0}} diff --git a/src/view/com/modals/Repost.tsx b/src/view/com/modals/Repost.tsx index b4669a046..d5ed66b70 100644 --- a/src/view/com/modals/Repost.tsx +++ b/src/view/com/modals/Repost.tsx @@ -26,22 +26,28 @@ export function Component({ } return ( - <View style={[s.flex1, pal.view, styles.container]}> + <View testID="repostModal" style={[s.flex1, pal.view, styles.container]}> <View style={s.pb20}> - <TouchableOpacity style={[styles.actionBtn]} onPress={onRepost}> + <TouchableOpacity + testID="repostBtn" + style={[styles.actionBtn]} + onPress={onRepost}> <RepostIcon strokeWidth={2} size={24} style={s.blue3} /> <Text type="title-lg" style={[styles.actionBtnLabel, pal.text]}> {!isReposted ? 'Repost' : 'Undo repost'} </Text> </TouchableOpacity> - <TouchableOpacity style={[styles.actionBtn]} onPress={onQuote}> + <TouchableOpacity + testID="quoteBtn" + style={[styles.actionBtn]} + onPress={onQuote}> <FontAwesomeIcon icon="quote-left" size={24} style={s.blue3} /> <Text type="title-lg" style={[styles.actionBtnLabel, pal.text]}> Quote Post </Text> </TouchableOpacity> </View> - <TouchableOpacity onPress={onPress}> + <TouchableOpacity testID="cancelBtn" onPress={onPress}> <LinearGradient colors={[gradients.blueLight.start, gradients.blueLight.end]} start={{x: 0, y: 0}} diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx index 1c2299b03..7d584e8e6 100644 --- a/src/view/com/notifications/FeedItem.tsx +++ b/src/view/com/notifications/FeedItem.tsx @@ -47,10 +47,10 @@ export const FeedItem = observer(function FeedItem({ const pal = usePalette('default') const [isAuthorsExpanded, setAuthorsExpanded] = React.useState<boolean>(false) const itemHref = React.useMemo(() => { - if (item.isUpvote || item.isRepost) { + if (item.isLike || item.isRepost) { const urip = new AtUri(item.subjectUri) return `/profile/${urip.host}/post/${urip.rkey}` - } else if (item.isFollow || item.isAssertion) { + } else if (item.isFollow) { return `/profile/${item.author.handle}` } else if (item.isReply) { const urip = new AtUri(item.uri) @@ -59,9 +59,9 @@ export const FeedItem = observer(function FeedItem({ return '' }, [item]) const itemTitle = React.useMemo(() => { - if (item.isUpvote || item.isRepost) { + if (item.isLike || item.isRepost) { return 'Post' - } else if (item.isFollow || item.isAssertion) { + } else if (item.isFollow) { return item.author.handle } else if (item.isReply) { return 'Post' @@ -77,7 +77,7 @@ export const FeedItem = observer(function FeedItem({ return <View /> } - if (item.isReply || item.isMention) { + if (item.isReply || item.isMention || item.isQuote) { if (item.additionalPost?.error) { // hide errors - it doesnt help the user to show them return <View /> @@ -103,7 +103,7 @@ export const FeedItem = observer(function FeedItem({ let action = '' let icon: Props['icon'] | 'HeartIconSolid' let iconStyle: Props['style'] = [] - if (item.isUpvote) { + if (item.isLike) { action = 'liked your post' icon = 'HeartIconSolid' iconStyle = [ @@ -114,9 +114,6 @@ export const FeedItem = observer(function FeedItem({ action = 'reposted your post' icon = 'retweet' iconStyle = [s.green3 as FontAwesomeIconStyle] - } else if (item.isReply) { - action = 'replied to your post' - icon = ['far', 'comment'] } else if (item.isFollow) { action = 'followed you' icon = 'user-plus' @@ -208,7 +205,7 @@ export const FeedItem = observer(function FeedItem({ </View> </View> </TouchableWithoutFeedback> - {item.isUpvote || item.isRepost ? ( + {item.isLike || item.isRepost || item.isQuote ? ( <AdditionalPostText additionalPost={item.additionalPost} /> ) : ( <></> @@ -352,9 +349,9 @@ function AdditionalPostText({ return <View /> } const text = additionalPost.thread?.postRecord.text - const images = ( - additionalPost.thread.post.embed as AppBskyEmbedImages.Presented - )?.images + const images = AppBskyEmbedImages.isView(additionalPost.thread.post.embed) + ? additionalPost.thread.post.embed.images + : undefined return ( <> {text?.length > 0 && <Text style={pal.textLight}>{text}</Text>} diff --git a/src/view/com/pager/FeedsTabBar.tsx b/src/view/com/pager/FeedsTabBar.tsx index 9831218ec..76e0a6fc6 100644 --- a/src/view/com/pager/FeedsTabBar.tsx +++ b/src/view/com/pager/FeedsTabBar.tsx @@ -9,7 +9,9 @@ import {usePalette} from 'lib/hooks/usePalette' import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' export const FeedsTabBar = observer( - (props: RenderTabBarFnProps & {onPressSelected: () => void}) => { + ( + props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void}, + ) => { const store = useStores() const pal = usePalette('default') const interp = useAnimatedValue(0) @@ -32,7 +34,10 @@ export const FeedsTabBar = observer( return ( <Animated.View style={[pal.view, styles.tabBar, transform]}> - <TouchableOpacity style={styles.tabBarAvi} onPress={onPressAvi}> + <TouchableOpacity + testID="viewHeaderDrawerBtn" + style={styles.tabBarAvi} + onPress={onPressAvi}> <UserAvatar avatar={store.me.avatar} size={30} /> </TouchableOpacity> <TabBar diff --git a/src/view/com/pager/Pager.tsx b/src/view/com/pager/Pager.tsx index 416828a27..34747db6d 100644 --- a/src/view/com/pager/Pager.tsx +++ b/src/view/com/pager/Pager.tsx @@ -20,6 +20,7 @@ interface Props { initialPage?: number renderTabBar: RenderTabBarFn onPageSelected?: (index: number) => void + testID?: string } export const Pager = ({ children, @@ -27,6 +28,7 @@ export const Pager = ({ initialPage = 0, renderTabBar, onPageSelected, + testID, }: React.PropsWithChildren<Props>) => { const [selectedPage, setSelectedPage] = React.useState(0) const position = useAnimatedValue(0) @@ -49,7 +51,7 @@ export const Pager = ({ ) return ( - <View> + <View testID={testID}> {tabBarPosition === 'top' && renderTabBar({ selectedPage, diff --git a/src/view/com/pager/TabBar.tsx b/src/view/com/pager/TabBar.tsx index 0b45d95f5..2070898bf 100644 --- a/src/view/com/pager/TabBar.tsx +++ b/src/view/com/pager/TabBar.tsx @@ -15,6 +15,7 @@ interface Layout { } export interface TabBarProps { + testID?: string selectedPage: number items: string[] position: Animated.Value @@ -26,6 +27,7 @@ export interface TabBarProps { } export function TabBar({ + testID, selectedPage, items, position, @@ -92,12 +94,15 @@ export function TabBar({ } return ( - <View style={[pal.view, styles.outer]} onLayout={onLayout}> + <View testID={testID} style={[pal.view, styles.outer]} onLayout={onLayout}> <Animated.View style={[styles.indicator, indicatorStyle]} /> {items.map((item, i) => { const selected = i === selectedPage return ( - <TouchableWithoutFeedback key={i} onPress={() => onPressItem(i)}> + <TouchableWithoutFeedback + key={i} + testID={testID ? `${testID}-${item}` : undefined} + onPress={() => onPressItem(i)}> <View style={ indicatorPosition === 'top' ? styles.itemTop : styles.itemBottom diff --git a/src/view/com/post-thread/PostVotedBy.tsx b/src/view/com/post-thread/PostLikedBy.tsx index f86798097..9fb46702e 100644 --- a/src/view/com/post-thread/PostVotedBy.tsx +++ b/src/view/com/post-thread/PostLikedBy.tsx @@ -2,24 +2,18 @@ import React, {useEffect} from 'react' import {observer} from 'mobx-react-lite' import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native' import {CenteredView, FlatList} from '../util/Views' -import {VotesViewModel, VoteItem} from 'state/models/votes-view' +import {LikesViewModel, LikeItem} from 'state/models/likes-view' import {ErrorMessage} from '../util/error/ErrorMessage' import {ProfileCardWithFollowBtn} from '../profile/ProfileCard' import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' -export const PostVotedBy = observer(function PostVotedBy({ - uri, - direction, -}: { - uri: string - direction: 'up' | 'down' -}) { +export const PostLikedBy = observer(function PostVotedBy({uri}: {uri: string}) { const pal = usePalette('default') const store = useStores() const view = React.useMemo( - () => new VotesViewModel(store, {uri, direction}), - [store, uri, direction], + () => new LikesViewModel(store, {uri}), + [store, uri], ) useEffect(() => { @@ -55,11 +49,10 @@ export const PostVotedBy = observer(function PostVotedBy({ // loaded // = - const renderItem = ({item}: {item: VoteItem}) => ( + const renderItem = ({item}: {item: LikeItem}) => ( <ProfileCardWithFollowBtn key={item.actor.did} did={item.actor.did} - declarationCid={item.actor.declaration.cid} handle={item.actor.handle} displayName={item.actor.displayName} avatar={item.actor.avatar} @@ -68,7 +61,7 @@ export const PostVotedBy = observer(function PostVotedBy({ ) return ( <FlatList - data={view.votes} + data={view.likes} keyExtractor={item => item.actor.did} refreshControl={ <RefreshControl diff --git a/src/view/com/post-thread/PostRepostedBy.tsx b/src/view/com/post-thread/PostRepostedBy.tsx index fda54469c..147d0271f 100644 --- a/src/view/com/post-thread/PostRepostedBy.tsx +++ b/src/view/com/post-thread/PostRepostedBy.tsx @@ -64,7 +64,6 @@ export const PostRepostedBy = observer(function PostRepostedBy({ <ProfileCardWithFollowBtn key={item.did} did={item.did} - declarationCid={item.declaration.cid} handle={item.handle} displayName={item.displayName} avatar={item.avatar} diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index d0452331b..569c6e392 100644 --- a/src/view/com/post-thread/PostThread.tsx +++ b/src/view/com/post-thread/PostThread.tsx @@ -1,17 +1,30 @@ import React, {useRef} from 'react' import {observer} from 'mobx-react-lite' -import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native' +import { + ActivityIndicator, + RefreshControl, + StyleSheet, + TouchableOpacity, + View, +} from 'react-native' import {CenteredView, FlatList} from '../util/Views' import { PostThreadViewModel, PostThreadViewPostModel, } from 'state/models/post-thread-view' +import { + FontAwesomeIcon, + FontAwesomeIconStyle, +} from '@fortawesome/react-native-fontawesome' import {PostThreadItem} from './PostThreadItem' import {ComposePrompt} from '../composer/Prompt' import {ErrorMessage} from '../util/error/ErrorMessage' +import {Text} from '../util/text/Text' import {s} from 'lib/styles' import {isDesktopWeb} from 'platform/detection' import {usePalette} from 'lib/hooks/usePalette' +import {useNavigation} from '@react-navigation/native' +import {NavigationProp} from 'lib/routes/types' const REPLY_PROMPT = {_reactKey: '__reply__', _isHighlightedPost: false} const BOTTOM_BORDER = { @@ -32,6 +45,7 @@ export const PostThread = observer(function PostThread({ const pal = usePalette('default') const ref = useRef<FlatList>(null) const [isRefreshing, setIsRefreshing] = React.useState(false) + const navigation = useNavigation<NavigationProp>() const posts = React.useMemo(() => { if (view.thread) { return Array.from(flattenThread(view.thread)).concat([BOTTOM_BORDER]) @@ -41,6 +55,7 @@ export const PostThread = observer(function PostThread({ // events // = + const onRefresh = React.useCallback(async () => { setIsRefreshing(true) try { @@ -50,6 +65,7 @@ export const PostThread = observer(function PostThread({ } setIsRefreshing(false) }, [view, setIsRefreshing]) + const onLayout = React.useCallback(() => { const index = posts.findIndex(post => post._isHighlightedPost) if (index !== -1) { @@ -60,6 +76,7 @@ export const PostThread = observer(function PostThread({ }) } }, [posts, ref]) + const onScrollToIndexFailed = React.useCallback( (info: { index: number @@ -73,6 +90,15 @@ export const PostThread = observer(function PostThread({ }, [ref], ) + + const onPressBack = React.useCallback(() => { + if (navigation.canGoBack()) { + navigation.goBack() + } else { + navigation.navigate('Home') + } + }, [navigation]) + const renderItem = React.useCallback( ({item}: {item: YieldedItem}) => { if (item === REPLY_PROMPT) { @@ -104,6 +130,30 @@ export const PostThread = observer(function PostThread({ // error // = if (view.hasError) { + if (view.notFound) { + return ( + <CenteredView> + <View style={[pal.view, pal.border, styles.notFoundContainer]}> + <Text type="title-lg" style={[pal.text, s.mb5]}> + Post not found + </Text> + <Text type="md" style={[pal.text, s.mb10]}> + The post may have been deleted. + </Text> + <TouchableOpacity onPress={onPressBack}> + <Text type="2xl" style={pal.link}> + <FontAwesomeIcon + icon="angle-left" + style={[pal.link as FontAwesomeIconStyle, s.mr5]} + size={14} + /> + Back + </Text> + </TouchableOpacity> + </View> + </CenteredView> + ) + } return ( <CenteredView> <ErrorMessage message={view.error} onPressTryAgain={onRefresh} /> @@ -159,12 +209,18 @@ function* flattenThread( yield* flattenThread(reply as PostThreadViewPostModel) } } - } else if (!isAscending && !post.parent && post.post.replyCount > 0) { + } else if (!isAscending && !post.parent && post.post.replyCount) { post._hasMore = true } } const styles = StyleSheet.create({ + notFoundContainer: { + margin: 10, + paddingHorizontal: 18, + paddingVertical: 14, + borderRadius: 6, + }, bottomBorder: { borderBottomWidth: 1, }, diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 17c7943d9..cf2148060 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -19,7 +19,7 @@ import {ago} from 'lib/strings/time' import {pluralize} from 'lib/strings/helpers' import {useStores} from 'state/index' import {PostMeta} from '../util/PostMeta' -import {PostEmbeds} from '../util/PostEmbeds' +import {PostEmbeds} from '../util/post-embeds' import {PostCtrls} from '../util/PostCtrls' import {PostMutedWrapper} from '../util/PostMuted' import {ErrorMessage} from '../util/error/ErrorMessage' @@ -38,7 +38,7 @@ export const PostThreadItem = observer(function PostThreadItem({ const store = useStores() const [deleted, setDeleted] = React.useState(false) const record = item.postRecord - const hasEngagement = item.post.upvoteCount || item.post.repostCount + const hasEngagement = item.post.likeCount || item.post.repostCount const itemUri = item.post.uri const itemCid = item.post.cid @@ -49,11 +49,11 @@ export const PostThreadItem = observer(function PostThreadItem({ const itemTitle = `Post by ${item.post.author.handle}` const authorHref = `/profile/${item.post.author.handle}` const authorTitle = item.post.author.handle - const upvotesHref = React.useMemo(() => { + const likesHref = React.useMemo(() => { const urip = new AtUri(item.post.uri) - return `/profile/${item.post.author.handle}/post/${urip.rkey}/upvoted-by` + return `/profile/${item.post.author.handle}/post/${urip.rkey}/liked-by` }, [item.post.uri, item.post.author.handle]) - const upvotesTitle = 'Likes on this post' + const likesTitle = 'Likes on this post' const repostsHref = React.useMemo(() => { const urip = new AtUri(item.post.uri) return `/profile/${item.post.author.handle}/post/${urip.rkey}/reposted-by` @@ -80,10 +80,10 @@ export const PostThreadItem = observer(function PostThreadItem({ .toggleRepost() .catch(e => store.log.error('Failed to toggle repost', e)) }, [item, store]) - const onPressToggleUpvote = React.useCallback(() => { + const onPressToggleLike = React.useCallback(() => { return item - .toggleUpvote() - .catch(e => store.log.error('Failed to toggle upvote', e)) + .toggleLike() + .catch(e => store.log.error('Failed to toggle like', e)) }, [item, store]) const onCopyPostText = React.useCallback(() => { Clipboard.setString(record?.text || '') @@ -125,153 +125,151 @@ export const PostThreadItem = observer(function PostThreadItem({ if (item._isHighlightedPost) { return ( - <> - <View - style={[ - styles.outer, - styles.outerHighlighted, - {borderTopColor: pal.colors.border}, - pal.view, - ]}> - <View style={styles.layout}> - <View style={styles.layoutAvi}> - <Link href={authorHref} title={authorTitle} asAnchor> - <UserAvatar size={52} avatar={item.post.author.avatar} /> - </Link> - </View> - <View style={styles.layoutContent}> - <View style={[styles.meta, styles.metaExpandedLine1]}> - <View style={[s.flexRow, s.alignBaseline]}> - <Link - style={styles.metaItem} - href={authorHref} - title={authorTitle}> - <Text - type="xl-bold" - style={[pal.text]} - numberOfLines={1} - lineHeight={1.2}> - {item.post.author.displayName || item.post.author.handle} - </Text> - </Link> - <Text type="md" style={[styles.metaItem, pal.textLight]}> - · {ago(item.post.indexedAt)} - </Text> - </View> - <View style={s.flex1} /> - <PostDropdownBtn - style={styles.metaItem} - itemUri={itemUri} - itemCid={itemCid} - itemHref={itemHref} - itemTitle={itemTitle} - isAuthor={item.post.author.did === store.me.did} - onCopyPostText={onCopyPostText} - onOpenTranslate={onOpenTranslate} - onDeletePost={onDeletePost}> - <FontAwesomeIcon - icon="ellipsis-h" - size={14} - style={[s.mt2, s.mr5, pal.textLight]} - /> - </PostDropdownBtn> - </View> - <View style={styles.meta}> + <View + testID={`postThreadItem-by-${item.post.author.handle}`} + style={[ + styles.outer, + styles.outerHighlighted, + {borderTopColor: pal.colors.border}, + pal.view, + ]}> + <View style={styles.layout}> + <View style={styles.layoutAvi}> + <Link href={authorHref} title={authorTitle} asAnchor> + <UserAvatar size={52} avatar={item.post.author.avatar} /> + </Link> + </View> + <View style={styles.layoutContent}> + <View style={[styles.meta, styles.metaExpandedLine1]}> + <View style={[s.flexRow, s.alignBaseline]}> <Link style={styles.metaItem} href={authorHref} title={authorTitle}> - <Text type="md" style={[pal.textLight]} numberOfLines={1}> - @{item.post.author.handle} + <Text + type="xl-bold" + style={[pal.text]} + numberOfLines={1} + lineHeight={1.2}> + {item.post.author.displayName || item.post.author.handle} </Text> </Link> + <Text type="md" style={[styles.metaItem, pal.textLight]}> + · {ago(item.post.indexedAt)} + </Text> </View> - </View> - </View> - <View style={[s.pl10, s.pr10, s.pb10]}> - {item.richText?.text ? ( - <View - style={[ - styles.postTextContainer, - styles.postTextLargeContainer, - ]}> - <RichText - type="post-text-lg" - richText={item.richText} - lineHeight={1.3} - /> - </View> - ) : undefined} - <PostEmbeds embed={item.post.embed} style={s.mb10} /> - {item._isHighlightedPost && hasEngagement ? ( - <View style={[styles.expandedInfo, pal.border]}> - {item.post.repostCount ? ( - <Link - style={styles.expandedInfoItem} - href={repostsHref} - title={repostsTitle}> - <Text type="lg" style={pal.textLight}> - <Text type="xl-bold" style={pal.text}> - {item.post.repostCount} - </Text>{' '} - {pluralize(item.post.repostCount, 'repost')} - </Text> - </Link> - ) : ( - <></> - )} - {item.post.upvoteCount ? ( - <Link - style={styles.expandedInfoItem} - href={upvotesHref} - title={upvotesTitle}> - <Text type="lg" style={pal.textLight}> - <Text type="xl-bold" style={pal.text}> - {item.post.upvoteCount} - </Text>{' '} - {pluralize(item.post.upvoteCount, 'like')} - </Text> - </Link> - ) : ( - <></> - )} - </View> - ) : ( - <></> - )} - <View style={[s.pl10, s.pb5]}> - <PostCtrls - big + <View style={s.flex1} /> + <PostDropdownBtn + testID="postDropdownBtn" + style={styles.metaItem} itemUri={itemUri} itemCid={itemCid} itemHref={itemHref} itemTitle={itemTitle} - author={{ - avatar: item.post.author.avatar!, - handle: item.post.author.handle, - displayName: item.post.author.displayName!, - }} - text={item.richText?.text || record.text} - indexedAt={item.post.indexedAt} isAuthor={item.post.author.did === store.me.did} - isReposted={!!item.post.viewer.repost} - isUpvoted={!!item.post.viewer.upvote} - onPressReply={onPressReply} - onPressToggleRepost={onPressToggleRepost} - onPressToggleUpvote={onPressToggleUpvote} onCopyPostText={onCopyPostText} onOpenTranslate={onOpenTranslate} - onDeletePost={onDeletePost} + onDeletePost={onDeletePost}> + <FontAwesomeIcon + icon="ellipsis-h" + size={14} + style={[s.mt2, s.mr5, pal.textLight]} + /> + </PostDropdownBtn> + </View> + <View style={styles.meta}> + <Link + style={styles.metaItem} + href={authorHref} + title={authorTitle}> + <Text type="md" style={[pal.textLight]} numberOfLines={1}> + @{item.post.author.handle} + </Text> + </Link> + </View> + </View> + </View> + <View style={[s.pl10, s.pr10, s.pb10]}> + {item.richText?.text ? ( + <View + style={[styles.postTextContainer, styles.postTextLargeContainer]}> + <RichText + type="post-text-lg" + richText={item.richText} + lineHeight={1.3} /> </View> + ) : undefined} + <PostEmbeds embed={item.post.embed} style={s.mb10} /> + {item._isHighlightedPost && hasEngagement ? ( + <View style={[styles.expandedInfo, pal.border]}> + {item.post.repostCount ? ( + <Link + style={styles.expandedInfoItem} + href={repostsHref} + title={repostsTitle}> + <Text testID="repostCount" type="lg" style={pal.textLight}> + <Text type="xl-bold" style={pal.text}> + {item.post.repostCount} + </Text>{' '} + {pluralize(item.post.repostCount, 'repost')} + </Text> + </Link> + ) : ( + <></> + )} + {item.post.likeCount ? ( + <Link + style={styles.expandedInfoItem} + href={likesHref} + title={likesTitle}> + <Text testID="likeCount" type="lg" style={pal.textLight}> + <Text type="xl-bold" style={pal.text}> + {item.post.likeCount} + </Text>{' '} + {pluralize(item.post.likeCount, 'like')} + </Text> + </Link> + ) : ( + <></> + )} + </View> + ) : ( + <></> + )} + <View style={[s.pl10, s.pb5]}> + <PostCtrls + big + itemUri={itemUri} + itemCid={itemCid} + itemHref={itemHref} + itemTitle={itemTitle} + author={{ + avatar: item.post.author.avatar!, + handle: item.post.author.handle, + displayName: item.post.author.displayName!, + }} + text={item.richText?.text || record.text} + indexedAt={item.post.indexedAt} + isAuthor={item.post.author.did === store.me.did} + isReposted={!!item.post.viewer?.repost} + isLiked={!!item.post.viewer?.like} + onPressReply={onPressReply} + onPressToggleRepost={onPressToggleRepost} + onPressToggleLike={onPressToggleLike} + onCopyPostText={onCopyPostText} + onOpenTranslate={onOpenTranslate} + onDeletePost={onDeletePost} + /> </View> </View> - </> + </View> ) } else { return ( <PostMutedWrapper isMuted={item.post.author.viewer?.muted === true}> <Link + testID={`postThreadItem-by-${item.post.author.handle}`} style={[styles.outer, {borderTopColor: pal.colors.border}, pal.view]} href={itemHref} title={itemTitle} @@ -305,7 +303,6 @@ export const PostThreadItem = observer(function PostThreadItem({ timestamp={item.post.indexedAt} postHref={itemHref} did={item.post.author.did} - declarationCid={item.post.author.declaration.cid} /> {item.richText?.text ? ( <View style={styles.postTextContainer}> @@ -333,12 +330,12 @@ export const PostThreadItem = observer(function PostThreadItem({ isAuthor={item.post.author.did === store.me.did} replyCount={item.post.replyCount} repostCount={item.post.repostCount} - upvoteCount={item.post.upvoteCount} - isReposted={!!item.post.viewer.repost} - isUpvoted={!!item.post.viewer.upvote} + likeCount={item.post.likeCount} + isReposted={!!item.post.viewer?.repost} + isLiked={!!item.post.viewer?.like} onPressReply={onPressReply} onPressToggleRepost={onPressToggleRepost} - onPressToggleUpvote={onPressToggleUpvote} + onPressToggleLike={onPressToggleLike} onCopyPostText={onCopyPostText} onOpenTranslate={onOpenTranslate} onDeletePost={onDeletePost} diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index a6c66d143..6b3dc3ac6 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -15,7 +15,7 @@ import {PostThreadViewModel} from 'state/models/post-thread-view' import {Link} from '../util/Link' import {UserInfoText} from '../util/UserInfoText' import {PostMeta} from '../util/PostMeta' -import {PostEmbeds} from '../util/PostEmbeds' +import {PostEmbeds} from '../util/post-embeds' import {PostCtrls} from '../util/PostCtrls' import {PostMutedWrapper} from '../util/PostMuted' import {Text} from '../util/text/Text' @@ -118,10 +118,10 @@ export const Post = observer(function Post({ .toggleRepost() .catch(e => store.log.error('Failed to toggle repost', e)) } - const onPressToggleUpvote = () => { + const onPressToggleLike = () => { return item - .toggleUpvote() - .catch(e => store.log.error('Failed to toggle upvote', e)) + .toggleLike() + .catch(e => store.log.error('Failed to toggle like', e)) } const onCopyPostText = () => { Clipboard.setString(record.text) @@ -166,7 +166,6 @@ export const Post = observer(function Post({ timestamp={item.post.indexedAt} postHref={itemHref} did={item.post.author.did} - declarationCid={item.post.author.declaration.cid} /> {replyAuthorDid !== '' && ( <View style={[s.flexRow, s.mb2, s.alignCenter]}> @@ -211,12 +210,12 @@ export const Post = observer(function Post({ isAuthor={item.post.author.did === store.me.did} replyCount={item.post.replyCount} repostCount={item.post.repostCount} - upvoteCount={item.post.upvoteCount} - isReposted={!!item.post.viewer.repost} - isUpvoted={!!item.post.viewer.upvote} + likeCount={item.post.likeCount} + isReposted={!!item.post.viewer?.repost} + isLiked={!!item.post.viewer?.like} onPressReply={onPressReply} onPressToggleRepost={onPressToggleRepost} - onPressToggleUpvote={onPressToggleUpvote} + onPressToggleLike={onPressToggleLike} onCopyPostText={onCopyPostText} onOpenTranslate={onOpenTranslate} onDeletePost={onDeletePost} diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx index 4154cbe75..d07afca34 100644 --- a/src/view/com/posts/Feed.tsx +++ b/src/view/com/posts/Feed.tsx @@ -128,6 +128,7 @@ export const Feed = observer(function Feed({ <View testID={testID} style={style}> {data.length > 0 && ( <FlatList + testID={testID ? `${testID}-flatlist` : undefined} ref={scrollElRef} data={data} keyExtractor={item => item._reactKey} diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index 573b92fd3..734034a89 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -13,7 +13,7 @@ import {Text} from '../util/text/Text' import {UserInfoText} from '../util/UserInfoText' import {PostMeta} from '../util/PostMeta' import {PostCtrls} from '../util/PostCtrls' -import {PostEmbeds} from '../util/PostEmbeds' +import {PostEmbeds} from '../util/post-embeds' import {PostMutedWrapper} from '../util/PostMuted' import {RichText} from '../util/text/RichText' import * as Toast from '../util/Toast' @@ -79,11 +79,11 @@ export const FeedItem = observer(function ({ .toggleRepost() .catch(e => store.log.error('Failed to toggle repost', e)) } - const onPressToggleUpvote = () => { + const onPressToggleLike = () => { track('FeedItem:PostLike') return item - .toggleUpvote() - .catch(e => store.log.error('Failed to toggle upvote', e)) + .toggleLike() + .catch(e => store.log.error('Failed to toggle like', e)) } const onCopyPostText = () => { Clipboard.setString(record?.text || '') @@ -127,7 +127,12 @@ export const FeedItem = observer(function ({ return ( <PostMutedWrapper isMuted={isMuted}> - <Link style={outerStyles} href={itemHref} title={itemTitle} noFeedback> + <Link + testID={`feedItem-by-${item.post.author.handle}`} + style={outerStyles} + href={itemHref} + title={itemTitle} + noFeedback> {isThreadChild && ( <View style={[styles.topReplyLine, {borderColor: pal.colors.replyLine}]} @@ -189,7 +194,6 @@ export const FeedItem = observer(function ({ timestamp={item.post.indexedAt} postHref={itemHref} did={item.post.author.did} - declarationCid={item.post.author.declaration.cid} showFollowBtn={showFollowBtn} /> {!isThreadChild && replyAuthorDid !== '' && ( @@ -239,12 +243,12 @@ export const FeedItem = observer(function ({ isAuthor={item.post.author.did === store.me.did} replyCount={item.post.replyCount} repostCount={item.post.repostCount} - upvoteCount={item.post.upvoteCount} - isReposted={!!item.post.viewer.repost} - isUpvoted={!!item.post.viewer.upvote} + likeCount={item.post.likeCount} + isReposted={!!item.post.viewer?.repost} + isLiked={!!item.post.viewer?.like} onPressReply={onPressReply} onPressToggleRepost={onPressToggleRepost} - onPressToggleUpvote={onPressToggleUpvote} + onPressToggleLike={onPressToggleLike} onCopyPostText={onCopyPostText} onOpenTranslate={onOpenTranslate} onDeletePost={onDeletePost} diff --git a/src/view/com/profile/FollowButton.tsx b/src/view/com/profile/FollowButton.tsx index 5204f5a40..f22eb9b4a 100644 --- a/src/view/com/profile/FollowButton.tsx +++ b/src/view/com/profile/FollowButton.tsx @@ -2,19 +2,16 @@ import React from 'react' import {observer} from 'mobx-react-lite' import {Button, ButtonType} from '../util/forms/Button' import {useStores} from 'state/index' -import * as apilib from 'lib/api/index' import * as Toast from '../util/Toast' const FollowButton = observer( ({ type = 'inverted', did, - declarationCid, onToggleFollow, }: { type?: ButtonType did: string - declarationCid: string onToggleFollow?: (v: boolean) => void }) => { const store = useStores() @@ -23,7 +20,7 @@ const FollowButton = observer( const onToggleFollowInner = async () => { if (store.me.follows.isFollowing(did)) { try { - await apilib.unfollow(store, store.me.follows.getFollowUri(did)) + await store.agent.deleteFollow(store.me.follows.getFollowUri(did)) store.me.follows.removeFollow(did) onToggleFollow?.(false) } catch (e: any) { @@ -32,7 +29,7 @@ const FollowButton = observer( } } else { try { - const res = await apilib.follow(store, did, declarationCid) + const res = await store.agent.follow(did) store.me.follows.addFollow(did, res.uri) onToggleFollow?.(true) } catch (e: any) { diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx index 748648742..0beac8a7f 100644 --- a/src/view/com/profile/ProfileCard.tsx +++ b/src/view/com/profile/ProfileCard.tsx @@ -1,7 +1,7 @@ import React from 'react' import {StyleSheet, View} from 'react-native' import {observer} from 'mobx-react-lite' -import {AppBskyActorProfile} from '@atproto/api' +import {AppBskyActorDefs} from '@atproto/api' import {Link} from '../util/Link' import {Text} from '../util/text/Text' import {UserAvatar} from '../util/UserAvatar' @@ -11,6 +11,7 @@ import {useStores} from 'state/index' import FollowButton from './FollowButton' export function ProfileCard({ + testID, handle, displayName, avatar, @@ -21,6 +22,7 @@ export function ProfileCard({ followers, renderButton, }: { + testID?: string handle: string displayName?: string avatar?: string @@ -28,12 +30,13 @@ export function ProfileCard({ isFollowedBy?: boolean noBg?: boolean noBorder?: boolean - followers?: AppBskyActorProfile.View[] | undefined + followers?: AppBskyActorDefs.ProfileView[] | undefined renderButton?: () => JSX.Element }) { const pal = usePalette('default') return ( <Link + testID={testID} style={[ styles.outer, pal.border, @@ -106,7 +109,6 @@ export function ProfileCard({ export const ProfileCardWithFollowBtn = observer( ({ did, - declarationCid, handle, displayName, avatar, @@ -117,7 +119,6 @@ export const ProfileCardWithFollowBtn = observer( followers, }: { did: string - declarationCid: string handle: string displayName?: string avatar?: string @@ -125,7 +126,7 @@ export const ProfileCardWithFollowBtn = observer( isFollowedBy?: boolean noBg?: boolean noBorder?: boolean - followers?: AppBskyActorProfile.View[] | undefined + followers?: AppBskyActorDefs.ProfileView[] | undefined }) => { const store = useStores() const isMe = store.me.handle === handle @@ -140,11 +141,7 @@ export const ProfileCardWithFollowBtn = observer( noBg={noBg} noBorder={noBorder} followers={followers} - renderButton={ - isMe - ? undefined - : () => <FollowButton did={did} declarationCid={declarationCid} /> - } + renderButton={isMe ? undefined : () => <FollowButton did={did} />} /> ) }, diff --git a/src/view/com/profile/ProfileFollowers.tsx b/src/view/com/profile/ProfileFollowers.tsx index d1488403a..8d489ad0a 100644 --- a/src/view/com/profile/ProfileFollowers.tsx +++ b/src/view/com/profile/ProfileFollowers.tsx @@ -19,7 +19,7 @@ export const ProfileFollowers = observer(function ProfileFollowers({ const pal = usePalette('default') const store = useStores() const view = React.useMemo( - () => new UserFollowersViewModel(store, {user: name}), + () => new UserFollowersViewModel(store, {actor: name}), [store, name], ) @@ -64,7 +64,6 @@ export const ProfileFollowers = observer(function ProfileFollowers({ <ProfileCardWithFollowBtn key={item.did} did={item.did} - declarationCid={item.declaration.cid} handle={item.handle} displayName={item.displayName} avatar={item.avatar} diff --git a/src/view/com/profile/ProfileFollows.tsx b/src/view/com/profile/ProfileFollows.tsx index ddb64787a..849b33441 100644 --- a/src/view/com/profile/ProfileFollows.tsx +++ b/src/view/com/profile/ProfileFollows.tsx @@ -16,7 +16,7 @@ export const ProfileFollows = observer(function ProfileFollows({ const pal = usePalette('default') const store = useStores() const view = React.useMemo( - () => new UserFollowsViewModel(store, {user: name}), + () => new UserFollowsViewModel(store, {actor: name}), [store, name], ) @@ -61,7 +61,6 @@ export const ProfileFollows = observer(function ProfileFollows({ <ProfileCardWithFollowBtn key={item.did} did={item.did} - declarationCid={item.declaration.cid} handle={item.handle} displayName={item.displayName} avatar={item.avatar} diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx index 06dd20989..6294c627b 100644 --- a/src/view/com/profile/ProfileHeader.tsx +++ b/src/view/com/profile/ProfileHeader.tsx @@ -33,7 +33,61 @@ import {isDesktopWeb} from 'platform/detection' const BACK_HITSLOP = {left: 30, top: 30, right: 30, bottom: 30} -export const ProfileHeader = observer(function ProfileHeader({ +export const ProfileHeader = observer( + ({ + view, + onRefreshAll, + }: { + view: ProfileViewModel + onRefreshAll: () => void + }) => { + const pal = usePalette('default') + + // loading + // = + if (!view || !view.hasLoaded) { + return ( + <View style={pal.view}> + <LoadingPlaceholder width="100%" height={120} /> + <View + style={[ + pal.view, + {borderColor: pal.colors.background}, + styles.avi, + ]}> + <LoadingPlaceholder width={80} height={80} style={styles.br40} /> + </View> + <View style={styles.content}> + <View style={[styles.buttonsLine]}> + <LoadingPlaceholder width={100} height={31} style={styles.br50} /> + </View> + <View style={styles.displayNameLine}> + <Text type="title-2xl" style={[pal.text, styles.title]}> + {view.displayName || view.handle} + </Text> + </View> + </View> + </View> + ) + } + + // error + // = + if (view.hasError) { + return ( + <View testID="profileHeaderHasError"> + <Text>{view.error}</Text> + </View> + ) + } + + // loaded + // = + return <ProfileHeaderLoaded view={view} onRefreshAll={onRefreshAll} /> + }, +) + +const ProfileHeaderLoaded = observer(function ProfileHeaderLoaded({ view, onRefreshAll, }: { @@ -44,14 +98,17 @@ export const ProfileHeader = observer(function ProfileHeader({ const store = useStores() const navigation = useNavigation<NavigationProp>() const {track} = useAnalytics() + const onPressBack = React.useCallback(() => { navigation.goBack() }, [navigation]) + const onPressAvi = React.useCallback(() => { if (view.avatar) { store.shell.openLightbox(new ProfileImageLightbox(view)) } }, [store, view]) + const onPressToggleFollow = React.useCallback(() => { view?.toggleFollowing().then( () => { @@ -64,6 +121,7 @@ export const ProfileHeader = observer(function ProfileHeader({ err => store.log.error('Failed to toggle follow', err), ) }, [view, store]) + const onPressEditProfile = React.useCallback(() => { track('ProfileHeader:EditProfileButtonClicked') store.shell.openModal({ @@ -72,18 +130,22 @@ export const ProfileHeader = observer(function ProfileHeader({ onUpdate: onRefreshAll, }) }, [track, store, view, onRefreshAll]) + const onPressFollowers = React.useCallback(() => { track('ProfileHeader:FollowersButtonClicked') navigation.push('ProfileFollowers', {name: view.handle}) }, [track, navigation, view]) + const onPressFollows = React.useCallback(() => { track('ProfileHeader:FollowsButtonClicked') navigation.push('ProfileFollows', {name: view.handle}) }, [track, navigation, view]) + const onPressShare = React.useCallback(() => { track('ProfileHeader:ShareButtonClicked') Share.share({url: toShareUrl(`/profile/${view.handle}`)}) }, [track, view]) + const onPressMuteAccount = React.useCallback(async () => { track('ProfileHeader:MuteAccountButtonClicked') try { @@ -94,6 +156,7 @@ export const ProfileHeader = observer(function ProfileHeader({ Toast.show(`There was an issue! ${e.toString()}`) } }, [track, view, store]) + const onPressUnmuteAccount = React.useCallback(async () => { track('ProfileHeader:UnmuteAccountButtonClicked') try { @@ -104,6 +167,7 @@ export const ProfileHeader = observer(function ProfileHeader({ Toast.show(`There was an issue! ${e.toString()}`) } }, [track, view, store]) + const onPressReportAccount = React.useCallback(() => { track('ProfileHeader:ReportAccountButtonClicked') store.shell.openModal({ @@ -112,54 +176,39 @@ export const ProfileHeader = observer(function ProfileHeader({ }) }, [track, store, view]) - // loading - // = - if (!view || !view.hasLoaded) { - return ( - <View style={pal.view}> - <LoadingPlaceholder width="100%" height={120} /> - <View - style={[pal.view, {borderColor: pal.colors.background}, styles.avi]}> - <LoadingPlaceholder width={80} height={80} style={styles.br40} /> - </View> - <View style={styles.content}> - <View style={[styles.buttonsLine]}> - <LoadingPlaceholder width={100} height={31} style={styles.br50} /> - </View> - <View style={styles.displayNameLine}> - <Text type="title-2xl" style={[pal.text, styles.title]}> - {view.displayName || view.handle} - </Text> - </View> - </View> - </View> - ) - } - - // error - // = - if (view.hasError) { - return ( - <View testID="profileHeaderHasError"> - <Text>{view.error}</Text> - </View> - ) - } - - // loaded - // = - const isMe = store.me.did === view.did - let dropdownItems: DropdownItem[] = [{label: 'Share', onPress: onPressShare}] - if (!isMe) { - dropdownItems.push({ - label: view.viewer.muted ? 'Unmute Account' : 'Mute Account', - onPress: view.viewer.muted ? onPressUnmuteAccount : onPressMuteAccount, - }) - dropdownItems.push({ - label: 'Report Account', - onPress: onPressReportAccount, - }) - } + const isMe = React.useMemo( + () => store.me.did === view.did, + [store.me.did, view.did], + ) + const dropdownItems: DropdownItem[] = React.useMemo(() => { + let items: DropdownItem[] = [ + { + testID: 'profileHeaderDropdownSahreBtn', + label: 'Share', + onPress: onPressShare, + }, + ] + if (!isMe) { + items.push({ + testID: 'profileHeaderDropdownMuteBtn', + label: view.viewer.muted ? 'Unmute Account' : 'Mute Account', + onPress: view.viewer.muted ? onPressUnmuteAccount : onPressMuteAccount, + }) + items.push({ + testID: 'profileHeaderDropdownReportBtn', + label: 'Report Account', + onPress: onPressReportAccount, + }) + } + return items + }, [ + isMe, + view.viewer.muted, + onPressShare, + onPressUnmuteAccount, + onPressMuteAccount, + onPressReportAccount, + ]) return ( <View style={pal.view}> <UserBanner banner={view.banner} /> @@ -178,6 +227,7 @@ export const ProfileHeader = observer(function ProfileHeader({ <> {store.me.follows.isFollowing(view.did) ? ( <TouchableOpacity + testID="unfollowBtn" onPress={onPressToggleFollow} style={[styles.btn, styles.mainBtn, pal.btn]}> <FontAwesomeIcon @@ -191,7 +241,7 @@ export const ProfileHeader = observer(function ProfileHeader({ </TouchableOpacity> ) : ( <TouchableOpacity - testID="profileHeaderToggleFollowButton" + testID="followBtn" onPress={onPressToggleFollow} style={[styles.btn, styles.primaryBtn]}> <FontAwesomeIcon @@ -207,6 +257,7 @@ export const ProfileHeader = observer(function ProfileHeader({ )} {dropdownItems?.length ? ( <DropdownButton + testID="profileHeaderDropdownBtn" type="bare" items={dropdownItems} style={[styles.btn, styles.secondaryBtn, pal.btn]}> @@ -215,7 +266,10 @@ export const ProfileHeader = observer(function ProfileHeader({ ) : undefined} </View> <View style={styles.displayNameLine}> - <Text type="title-2xl" style={[pal.text, styles.title]}> + <Text + testID="profileHeaderDisplayName" + type="title-2xl" + style={[pal.text, styles.title]}> {view.displayName || view.handle} </Text> </View> @@ -241,19 +295,17 @@ export const ProfileHeader = observer(function ProfileHeader({ {pluralize(view.followersCount, 'follower')} </Text> </TouchableOpacity> - {view.isUser ? ( - <TouchableOpacity - testID="profileHeaderFollowsButton" - style={[s.flexRow, s.mr10]} - onPress={onPressFollows}> - <Text type="md" style={[s.bold, s.mr2, pal.text]}> - {view.followsCount} - </Text> - <Text type="md" style={[pal.textLight]}> - following - </Text> - </TouchableOpacity> - ) : undefined} + <TouchableOpacity + testID="profileHeaderFollowsButton" + style={[s.flexRow, s.mr10]} + onPress={onPressFollows}> + <Text type="md" style={[s.bold, s.mr2, pal.text]}> + {view.followsCount} + </Text> + <Text type="md" style={[pal.textLight]}> + following + </Text> + </TouchableOpacity> <View style={[s.flexRow, s.mr10]}> <Text type="md" style={[s.bold, s.mr2, pal.text]}> {view.postsCount} @@ -265,13 +317,16 @@ export const ProfileHeader = observer(function ProfileHeader({ </View> {view.descriptionRichText ? ( <RichText + testID="profileHeaderDescription" style={[styles.description, pal.text]} numberOfLines={15} richText={view.descriptionRichText} /> ) : undefined} {view.viewer.muted ? ( - <View style={[styles.detailLine, pal.btn, s.p5]}> + <View + testID="profileHeaderMutedNotice" + style={[styles.detailLine, pal.btn, s.p5]}> <FontAwesomeIcon icon={['far', 'eye-slash']} style={[pal.text, s.mr5]} diff --git a/src/view/com/search/SearchResults.tsx b/src/view/com/search/SearchResults.tsx index 4bf46515c..b53965f44 100644 --- a/src/view/com/search/SearchResults.tsx +++ b/src/view/com/search/SearchResults.tsx @@ -97,7 +97,6 @@ const Profiles = observer(({model}: {model: SearchUIModel}) => { <ProfileCardWithFollowBtn key={item.did} did={item.did} - declarationCid={item.declaration.cid} handle={item.handle} displayName={item.displayName} avatar={item.avatar} diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx index f356f0b09..703869be1 100644 --- a/src/view/com/util/Link.tsx +++ b/src/view/com/util/Link.tsx @@ -29,6 +29,7 @@ type Event = | GestureResponderEvent export const Link = observer(function Link({ + testID, style, href, title, @@ -36,6 +37,7 @@ export const Link = observer(function Link({ noFeedback, asAnchor, }: { + testID?: string style?: StyleProp<ViewStyle> href?: string title?: string @@ -58,6 +60,7 @@ export const Link = observer(function Link({ if (noFeedback) { return ( <TouchableWithoutFeedback + testID={testID} onPress={onPress} // @ts-ignore web only -prf href={asAnchor ? href : undefined}> @@ -69,6 +72,7 @@ export const Link = observer(function Link({ } return ( <TouchableOpacity + testID={testID} style={style} onPress={onPress} // @ts-ignore web only -prf @@ -79,6 +83,7 @@ export const Link = observer(function Link({ }) export const TextLink = observer(function TextLink({ + testID, type = 'md', style, href, @@ -86,6 +91,7 @@ export const TextLink = observer(function TextLink({ numberOfLines, lineHeight, }: { + testID?: string type?: TypographyVariant style?: StyleProp<TextStyle> href: string @@ -106,6 +112,7 @@ export const TextLink = observer(function TextLink({ return ( <Text + testID={testID} type={type} style={style} numberOfLines={numberOfLines} @@ -120,6 +127,7 @@ export const TextLink = observer(function TextLink({ * Only acts as a link on desktop web */ export const DesktopWebTextLink = observer(function DesktopWebTextLink({ + testID, type = 'md', style, href, @@ -127,6 +135,7 @@ export const DesktopWebTextLink = observer(function DesktopWebTextLink({ numberOfLines, lineHeight, }: { + testID?: string type?: TypographyVariant style?: StyleProp<TextStyle> href: string @@ -137,6 +146,7 @@ export const DesktopWebTextLink = observer(function DesktopWebTextLink({ if (isDesktopWeb) { return ( <TextLink + testID={testID} type={type} style={style} href={href} @@ -148,6 +158,7 @@ export const DesktopWebTextLink = observer(function DesktopWebTextLink({ } return ( <Text + testID={testID} type={type} style={style} numberOfLines={numberOfLines} diff --git a/src/view/com/util/PostCtrls.tsx b/src/view/com/util/PostCtrls.tsx index 00e35eef7..6904928f4 100644 --- a/src/view/com/util/PostCtrls.tsx +++ b/src/view/com/util/PostCtrls.tsx @@ -45,12 +45,12 @@ interface PostCtrlsOpts { style?: StyleProp<ViewStyle> replyCount?: number repostCount?: number - upvoteCount?: number + likeCount?: number isReposted: boolean - isUpvoted: boolean + isLiked: boolean onPressReply: () => void onPressToggleRepost: () => Promise<void> - onPressToggleUpvote: () => Promise<void> + onPressToggleLike: () => Promise<void> onCopyPostText: () => void onOpenTranslate: () => void onDeletePost: () => void @@ -157,26 +157,26 @@ export function PostCtrls(opts: PostCtrlsOpts) { }) } - const onPressToggleUpvoteWrapper = () => { - if (!opts.isUpvoted) { + const onPressToggleLikeWrapper = () => { + if (!opts.isLiked) { ReactNativeHapticFeedback.trigger('impactMedium') setLikeMod(1) opts - .onPressToggleUpvote() + .onPressToggleLike() .catch(_e => undefined) .then(() => setLikeMod(0)) // DISABLED see #135 // likeRef.current?.trigger( // {start: ctrlAnimStart, style: ctrlAnimStyle}, // async () => { - // await opts.onPressToggleUpvote().catch(_e => undefined) + // await opts.onPressToggleLike().catch(_e => undefined) // setLikeMod(0) // }, // ) } else { setLikeMod(-1) opts - .onPressToggleUpvote() + .onPressToggleLike() .catch(_e => undefined) .then(() => setLikeMod(0)) } @@ -186,6 +186,7 @@ export function PostCtrls(opts: PostCtrlsOpts) { <View style={[styles.ctrls, opts.style]}> <View style={s.flex1}> <TouchableOpacity + testID="replyBtn" style={styles.ctrl} hitSlop={HITSLOP} onPress={opts.onPressReply}> @@ -203,6 +204,7 @@ export function PostCtrls(opts: PostCtrlsOpts) { </View> <View style={s.flex1}> <TouchableOpacity + testID="repostBtn" hitSlop={HITSLOP} onPress={onPressToggleRepostWrapper} style={styles.ctrl}> @@ -230,6 +232,7 @@ export function PostCtrls(opts: PostCtrlsOpts) { } {typeof opts.repostCount !== 'undefined' ? ( <Text + testID="repostCount" style={ opts.isReposted || repostMod > 0 ? [s.bold, s.green3, s.f15, s.ml5] @@ -242,12 +245,13 @@ export function PostCtrls(opts: PostCtrlsOpts) { </View> <View style={s.flex1}> <TouchableOpacity + testID="likeBtn" style={styles.ctrl} hitSlop={HITSLOP} - onPress={onPressToggleUpvoteWrapper}> - {opts.isUpvoted || likeMod > 0 ? ( + onPress={onPressToggleLikeWrapper}> + {opts.isLiked || likeMod > 0 ? ( <HeartIconSolid - style={styles.ctrlIconUpvoted as StyleProp<ViewStyle>} + style={styles.ctrlIconLiked as StyleProp<ViewStyle>} size={opts.big ? 22 : 16} /> ) : ( @@ -259,9 +263,9 @@ export function PostCtrls(opts: PostCtrlsOpts) { )} { undefined /*DISABLED see #135 <TriggerableAnimated ref={likeRef}> - {opts.isUpvoted || likeMod > 0 ? ( + {opts.isLiked || likeMod > 0 ? ( <HeartIconSolid - style={styles.ctrlIconUpvoted as ViewStyle} + style={styles.ctrlIconLiked as ViewStyle} size={opts.big ? 22 : 16} /> ) : ( @@ -276,14 +280,15 @@ export function PostCtrls(opts: PostCtrlsOpts) { )} </TriggerableAnimated>*/ } - {typeof opts.upvoteCount !== 'undefined' ? ( + {typeof opts.likeCount !== 'undefined' ? ( <Text + testID="likeCount" style={ - opts.isUpvoted || likeMod > 0 + opts.isLiked || likeMod > 0 ? [s.bold, s.red3, s.f15, s.ml5] : [defaultCtrlColor, s.f15, s.ml5] }> - {opts.upvoteCount + likeMod} + {opts.likeCount + likeMod} </Text> ) : undefined} </TouchableOpacity> @@ -291,6 +296,7 @@ export function PostCtrls(opts: PostCtrlsOpts) { <View style={s.flex1}> {opts.big ? undefined : ( <PostDropdownBtn + testID="postDropdownBtn" style={styles.ctrl} itemUri={opts.itemUri} itemCid={opts.itemCid} @@ -330,7 +336,7 @@ const styles = StyleSheet.create({ ctrlIconReposted: { color: colors.green3, }, - ctrlIconUpvoted: { + ctrlIconLiked: { color: colors.red3, }, mt1: { diff --git a/src/view/com/util/PostEmbeds/YoutubeEmbed.tsx b/src/view/com/util/PostEmbeds/YoutubeEmbed.tsx deleted file mode 100644 index d9425fe4e..000000000 --- a/src/view/com/util/PostEmbeds/YoutubeEmbed.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import React, {useEffect} from 'react' -import {useState} from 'react' -import { - View, - StyleSheet, - Pressable, - TouchableWithoutFeedback, - EmitterSubscription, -} from 'react-native' -import YoutubePlayer from 'react-native-youtube-iframe' -import {usePalette} from 'lib/hooks/usePalette' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import ExternalLinkEmbed from './ExternalLinkEmbed' -import {PresentedExternal} from '@atproto/api/dist/client/types/app/bsky/embed/external' -import {useStores} from 'state/index' - -const YoutubeEmbed = ({ - link, - videoId, -}: { - videoId: string - link: PresentedExternal -}) => { - const store = useStores() - const [displayVideoPlayer, setDisplayVideoPlayer] = useState(false) - const [playerDimensions, setPlayerDimensions] = useState({ - width: 0, - height: 0, - }) - const pal = usePalette('default') - const handlePlayButtonPressed = () => { - setDisplayVideoPlayer(true) - } - const handleOnLayout = (event: { - nativeEvent: {layout: {width: any; height: any}} - }) => { - setPlayerDimensions({ - width: event.nativeEvent.layout.width, - height: event.nativeEvent.layout.height, - }) - } - useEffect(() => { - let sub: EmitterSubscription - if (displayVideoPlayer) { - sub = store.onNavigation(() => { - setDisplayVideoPlayer(false) - }) - } - return () => sub && sub.remove() - }, [displayVideoPlayer, store]) - - const imageChild = ( - <Pressable onPress={handlePlayButtonPressed} style={styles.playButton}> - <FontAwesomeIcon icon="play" size={24} color="white" /> - </Pressable> - ) - - if (!displayVideoPlayer) { - return ( - <View - style={[styles.extOuter, pal.view, pal.border]} - onLayout={handleOnLayout}> - <ExternalLinkEmbed - link={link} - onImagePress={handlePlayButtonPressed} - imageChild={imageChild} - /> - </View> - ) - } - - const height = (playerDimensions.width / 16) * 9 - const noop = () => {} - - return ( - <TouchableWithoutFeedback onPress={noop}> - <View> - {/* Removing the outter View will make tap events propagate to parents */} - <YoutubePlayer - initialPlayerParams={{ - modestbranding: true, - }} - webViewProps={{ - startInLoadingState: true, - }} - height={height} - videoId={videoId} - webViewStyle={styles.webView} - /> - </View> - </TouchableWithoutFeedback> - ) -} - -const styles = StyleSheet.create({ - extOuter: { - borderWidth: 1, - borderRadius: 8, - marginTop: 4, - }, - playButton: { - position: 'absolute', - alignSelf: 'center', - alignItems: 'center', - top: '44%', - justifyContent: 'center', - backgroundColor: 'black', - padding: 10, - borderRadius: 50, - opacity: 0.8, - }, - webView: { - alignItems: 'center', - alignContent: 'center', - justifyContent: 'center', - }, -}) - -export default YoutubeEmbed diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx index c53de5c1f..a675283b8 100644 --- a/src/view/com/util/PostMeta.tsx +++ b/src/view/com/util/PostMeta.tsx @@ -16,7 +16,6 @@ interface PostMetaOpts { postHref: string timestamp: string did?: string - declarationCid?: string showFollowBtn?: boolean } @@ -34,13 +33,7 @@ export const PostMeta = observer(function (opts: PostMetaOpts) { setDidFollow(true) }, [setDidFollow]) - if ( - opts.showFollowBtn && - !isMe && - (!isFollowing || didFollow) && - opts.did && - opts.declarationCid - ) { + if (opts.showFollowBtn && !isMe && (!isFollowing || didFollow) && opts.did) { // two-liner with follow button return ( <View style={styles.metaTwoLine}> @@ -79,7 +72,6 @@ export const PostMeta = observer(function (opts: PostMetaOpts) { <FollowButton type="default" did={opts.did} - declarationCid={opts.declarationCid} onToggleFollow={onToggleFollow} /> </View> diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index 2e0632521..ff741cd34 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -23,6 +23,7 @@ import {isWeb} from 'platform/detection' function DefaultAvatar({size}: {size: number}) { return ( <Svg + testID="userAvatarFallback" width={size} height={size} viewBox="0 0 24 24" @@ -56,6 +57,7 @@ export function UserAvatar({ const dropdownItems = [ !isWeb && { + testID: 'changeAvatarCameraBtn', label: 'Camera', icon: 'camera' as IconProp, onPress: async () => { @@ -73,6 +75,7 @@ export function UserAvatar({ }, }, { + testID: 'changeAvatarLibraryBtn', label: 'Library', icon: 'image' as IconProp, onPress: async () => { @@ -94,6 +97,7 @@ export function UserAvatar({ }, }, { + testID: 'changeAvatarRemoveBtn', label: 'Remove', icon: ['far', 'trash-can'] as IconProp, onPress: async () => { @@ -104,6 +108,7 @@ export function UserAvatar({ // onSelectNewAvatar is only passed as prop on the EditProfile component return onSelectNewAvatar ? ( <DropdownButton + testID="changeAvatarBtn" type="bare" items={dropdownItems} openToRight @@ -112,6 +117,7 @@ export function UserAvatar({ menuWidth={170}> {avatar ? ( <HighPriorityImage + testID="userAvatarImage" style={{ width: size, height: size, @@ -132,6 +138,7 @@ export function UserAvatar({ </DropdownButton> ) : avatar ? ( <HighPriorityImage + testID="userAvatarImage" style={{width: size, height: size, borderRadius: Math.floor(size / 2)}} resizeMode="stretch" source={{uri: avatar}} diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx index 8317f93ac..56d7e370a 100644 --- a/src/view/com/util/UserBanner.tsx +++ b/src/view/com/util/UserBanner.tsx @@ -33,6 +33,7 @@ export function UserBanner({ const dropdownItems = [ !isWeb && { + testID: 'changeBannerCameraBtn', label: 'Camera', icon: 'camera' as IconProp, onPress: async () => { @@ -51,6 +52,7 @@ export function UserBanner({ }, }, { + testID: 'changeBannerLibraryBtn', label: 'Library', icon: 'image' as IconProp, onPress: async () => { @@ -73,6 +75,7 @@ export function UserBanner({ }, }, { + testID: 'changeBannerRemoveBtn', label: 'Remove', icon: ['far', 'trash-can'] as IconProp, onPress: () => { @@ -84,6 +87,7 @@ export function UserBanner({ // setUserBanner is only passed as prop on the EditProfile component return onSelectNewBanner ? ( <DropdownButton + testID="changeBannerBtn" type="bare" items={dropdownItems} openToRight @@ -91,9 +95,16 @@ export function UserBanner({ bottomOffset={-10} menuWidth={170}> {banner ? ( - <Image style={styles.bannerImage} source={{uri: banner}} /> + <Image + testID="userBannerImage" + style={styles.bannerImage} + source={{uri: banner}} + /> ) : ( - <View style={[styles.bannerImage, styles.defaultBanner]} /> + <View + testID="userBannerFallback" + style={[styles.bannerImage, styles.defaultBanner]} + /> )} <View style={[styles.editButtonContainer, pal.btn]}> <FontAwesomeIcon @@ -106,12 +117,16 @@ export function UserBanner({ </DropdownButton> ) : banner ? ( <Image + testID="userBannerImage" style={styles.bannerImage} resizeMode="cover" source={{uri: banner}} /> ) : ( - <View style={[styles.bannerImage, styles.defaultBanner]} /> + <View + testID="userBannerFallback" + style={[styles.bannerImage, styles.defaultBanner]} + /> ) } diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx index a99282512..ad0a5a1d2 100644 --- a/src/view/com/util/ViewHeader.tsx +++ b/src/view/com/util/ViewHeader.tsx @@ -51,7 +51,7 @@ export const ViewHeader = observer(function ({ return ( <Container hideOnScroll={hideOnScroll || false}> <TouchableOpacity - testID="viewHeaderBackOrMenuBtn" + testID="viewHeaderDrawerBtn" onPress={canGoBack ? onPressBack : onPressMenu} hitSlop={BACK_HITSLOP} style={canGoBack ? styles.backBtn : styles.backBtnWide}> diff --git a/src/view/com/util/ViewSelector.tsx b/src/view/com/util/ViewSelector.tsx index e1280fd82..82351cf08 100644 --- a/src/view/com/util/ViewSelector.tsx +++ b/src/view/com/util/ViewSelector.tsx @@ -47,13 +47,18 @@ export function ViewSelector({ // events // = - const onSwipeEnd = (dx: number) => { - if (dx !== 0) { - setSelectedIndex(clamp(selectedIndex + dx, 0, sections.length)) - } - } - const onPressSelection = (index: number) => - setSelectedIndex(clamp(index, 0, sections.length)) + const onSwipeEnd = React.useCallback( + (dx: number) => { + if (dx !== 0) { + setSelectedIndex(clamp(selectedIndex + dx, 0, sections.length)) + } + }, + [setSelectedIndex, selectedIndex, sections], + ) + const onPressSelection = React.useCallback( + (index: number) => setSelectedIndex(clamp(index, 0, sections.length)), + [setSelectedIndex, sections], + ) useEffect(() => { onSelectView?.(selectedIndex) }, [selectedIndex, onSelectView]) @@ -61,27 +66,33 @@ export function ViewSelector({ // rendering // = - const renderItemInternal = ({item}: {item: any}) => { - if (item === HEADER_ITEM) { - if (renderHeader) { - return renderHeader() + const renderItemInternal = React.useCallback( + ({item}: {item: any}) => { + if (item === HEADER_ITEM) { + if (renderHeader) { + return renderHeader() + } + return <View /> + } else if (item === SELECTOR_ITEM) { + return ( + <Selector + items={sections} + panX={panX} + selectedIndex={selectedIndex} + onSelect={onPressSelection} + /> + ) + } else { + return renderItem(item) } - return <View /> - } else if (item === SELECTOR_ITEM) { - return ( - <Selector - items={sections} - panX={panX} - selectedIndex={selectedIndex} - onSelect={onPressSelection} - /> - ) - } else { - return renderItem(item) - } - } + }, + [sections, panX, selectedIndex, onPressSelection, renderHeader, renderItem], + ) - const data = [HEADER_ITEM, SELECTOR_ITEM, ...items] + const data = React.useMemo( + () => [HEADER_ITEM, SELECTOR_ITEM, ...items], + [items], + ) return ( <HorzSwipe hasPriority diff --git a/src/view/com/util/forms/Button.tsx b/src/view/com/util/forms/Button.tsx index f3f4d1c79..b7c058d2d 100644 --- a/src/view/com/util/forms/Button.tsx +++ b/src/view/com/util/forms/Button.tsx @@ -27,11 +27,13 @@ export function Button({ style, onPress, children, + testID, }: React.PropsWithChildren<{ type?: ButtonType label?: string style?: StyleProp<ViewStyle> onPress?: () => void + testID?: string }>) { const theme = useTheme() const outerStyle = choose<ViewStyle, Record<ButtonType, ViewStyle>>(type, { @@ -107,7 +109,8 @@ export function Button({ return ( <TouchableOpacity style={[outerStyle, styles.outer, style]} - onPress={onPress}> + onPress={onPress} + testID={testID}> {label ? ( <Text type="button" style={[labelStyle]}> {label} diff --git a/src/view/com/util/forms/DropdownButton.tsx b/src/view/com/util/forms/DropdownButton.tsx index d6ae800c6..938c346cd 100644 --- a/src/view/com/util/forms/DropdownButton.tsx +++ b/src/view/com/util/forms/DropdownButton.tsx @@ -24,6 +24,7 @@ const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10} const ESTIMATED_MENU_ITEM_HEIGHT = 52 export interface DropdownItem { + testID?: string icon?: IconProp label: string onPress: () => void @@ -33,6 +34,7 @@ type MaybeDropdownItem = DropdownItem | false | undefined export type DropdownButtonType = ButtonType | 'bare' export function DropdownButton({ + testID, type = 'bare', style, items, @@ -43,6 +45,7 @@ export function DropdownButton({ rightOffset = 0, bottomOffset = 0, }: { + testID?: string type?: DropdownButtonType style?: StyleProp<ViewStyle> items: MaybeDropdownItem[] @@ -90,22 +93,18 @@ export function DropdownButton({ if (type === 'bare') { return ( <TouchableOpacity + testID={testID} style={style} onPress={onPress} hitSlop={HITSLOP} - // Fix an issue where specific references cause runtime error in jest environment - ref={ - typeof process !== 'undefined' && process.env.JEST_WORKER_ID != null - ? null - : ref - }> + ref={ref}> {children} </TouchableOpacity> ) } return ( <View ref={ref}> - <Button onPress={onPress} style={style} label={label}> + <Button testID={testID} onPress={onPress} style={style} label={label}> {children} </Button> </View> @@ -113,6 +112,7 @@ export function DropdownButton({ } export function PostDropdownBtn({ + testID, style, children, itemUri, @@ -123,6 +123,7 @@ export function PostDropdownBtn({ onOpenTranslate, onDeletePost, }: { + testID?: string style?: StyleProp<ViewStyle> children?: React.ReactNode itemUri: string @@ -138,6 +139,7 @@ export function PostDropdownBtn({ const dropdownItems: DropdownItem[] = [ { + testID: 'postDropdownTranslateBtn', icon: 'language', label: 'Translate...', onPress() { @@ -145,6 +147,7 @@ export function PostDropdownBtn({ }, }, { + testID: 'postDropdownCopyTextBtn', icon: ['far', 'paste'], label: 'Copy post text', onPress() { @@ -152,6 +155,7 @@ export function PostDropdownBtn({ }, }, { + testID: 'postDropdownShareBtn', icon: 'share', label: 'Share...', onPress() { @@ -159,6 +163,7 @@ export function PostDropdownBtn({ }, }, { + testID: 'postDropdownReportBtn', icon: 'circle-exclamation', label: 'Report post', onPress() { @@ -171,6 +176,7 @@ export function PostDropdownBtn({ }, isAuthor ? { + testID: 'postDropdownDeleteBtn', icon: ['far', 'trash-can'], label: 'Delete post', onPress() { @@ -186,7 +192,11 @@ export function PostDropdownBtn({ ].filter(Boolean) as DropdownItem[] return ( - <DropdownButton style={style} items={dropdownItems} menuWidth={200}> + <DropdownButton + testID={testID} + style={style} + items={dropdownItems} + menuWidth={200}> {children} </DropdownButton> ) @@ -291,6 +301,7 @@ const DropdownItems = ({ ]}> {items.map((item, index) => ( <TouchableOpacity + testID={item.testID} key={index} style={[styles.menuItem]} onPress={() => onPressItem(index)}> diff --git a/src/view/com/util/forms/RadioButton.tsx b/src/view/com/util/forms/RadioButton.tsx index d6b2bb119..f5696a76d 100644 --- a/src/view/com/util/forms/RadioButton.tsx +++ b/src/view/com/util/forms/RadioButton.tsx @@ -6,12 +6,14 @@ import {useTheme} from 'lib/ThemeContext' import {choose} from 'lib/functions' export function RadioButton({ + testID, type = 'default-light', label, isSelected, style, onPress, }: { + testID?: string type?: ButtonType label: string isSelected: boolean @@ -119,7 +121,7 @@ export function RadioButton({ }, }) return ( - <Button type={type} onPress={onPress} style={style}> + <Button testID={testID} type={type} onPress={onPress} style={style}> <View style={styles.outer}> <View style={[circleStyle, styles.circle]}> {isSelected ? ( diff --git a/src/view/com/util/forms/RadioGroup.tsx b/src/view/com/util/forms/RadioGroup.tsx index 901b0cdd8..071540b73 100644 --- a/src/view/com/util/forms/RadioGroup.tsx +++ b/src/view/com/util/forms/RadioGroup.tsx @@ -10,11 +10,13 @@ export interface RadioGroupItem { } export function RadioGroup({ + testID, type, items, initialSelection = '', onSelect, }: { + testID?: string type?: ButtonType items: RadioGroupItem[] initialSelection?: string @@ -30,6 +32,7 @@ export function RadioGroup({ {items.map((item, i) => ( <RadioButton key={item.key} + testID={testID ? `${testID}-${item.key}` : undefined} style={i !== 0 ? s.mt2 : undefined} type={type} label={item.label} diff --git a/src/view/com/util/images/AutoSizedImage.tsx b/src/view/com/util/images/AutoSizedImage.tsx index 24dbe6a52..ddb09ce39 100644 --- a/src/view/com/util/images/AutoSizedImage.tsx +++ b/src/view/com/util/images/AutoSizedImage.tsx @@ -4,9 +4,9 @@ import { StyleProp, StyleSheet, TouchableOpacity, + View, ViewStyle, } from 'react-native' -// import Image from 'view/com/util/images/Image' import {clamp} from 'lib/numbers' import {useStores} from 'state/index' import {Dim} from 'lib/media/manip' @@ -51,16 +51,24 @@ export function AutoSizedImage({ }) }, [dim, setDim, setAspectRatio, store, uri]) + if (onPress || onLongPress || onPressIn) { + return ( + <TouchableOpacity + onPress={onPress} + onLongPress={onLongPress} + onPressIn={onPressIn} + delayPressIn={DELAY_PRESS_IN} + style={[styles.container, style]}> + <Image style={[styles.image, {aspectRatio}]} source={{uri}} /> + {children} + </TouchableOpacity> + ) + } return ( - <TouchableOpacity - onPress={onPress} - onLongPress={onLongPress} - onPressIn={onPressIn} - delayPressIn={DELAY_PRESS_IN} - style={[styles.container, style]}> + <View style={[styles.container, style]}> <Image style={[styles.image, {aspectRatio}]} source={{uri}} /> {children} - </TouchableOpacity> + </View> ) } diff --git a/src/view/com/util/PostEmbeds/ExternalLinkEmbed.tsx b/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx index e8c63bdb7..a4cbb3e29 100644 --- a/src/view/com/util/PostEmbeds/ExternalLinkEmbed.tsx +++ b/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx @@ -3,25 +3,20 @@ import {Text} from '../text/Text' import {AutoSizedImage} from '../images/AutoSizedImage' import {StyleSheet, View} from 'react-native' import {usePalette} from 'lib/hooks/usePalette' -import {PresentedExternal} from '@atproto/api/dist/client/types/app/bsky/embed/external' +import {AppBskyEmbedExternal} from '@atproto/api' -const ExternalLinkEmbed = ({ +export const ExternalLinkEmbed = ({ link, - onImagePress, imageChild, }: { - link: PresentedExternal - onImagePress?: () => void + link: AppBskyEmbedExternal.ViewExternal imageChild?: React.ReactNode }) => { const pal = usePalette('default') return ( <> {link.thumb ? ( - <AutoSizedImage - uri={link.thumb} - style={styles.extImage} - onPress={onImagePress}> + <AutoSizedImage uri={link.thumb} style={styles.extImage}> {imageChild} </AutoSizedImage> ) : undefined} @@ -65,5 +60,3 @@ const styles = StyleSheet.create({ marginTop: 4, }, }) - -export default ExternalLinkEmbed diff --git a/src/view/com/util/PostEmbeds/QuoteEmbed.tsx b/src/view/com/util/post-embeds/QuoteEmbed.tsx index fee67c9bc..9dc5739a0 100644 --- a/src/view/com/util/PostEmbeds/QuoteEmbed.tsx +++ b/src/view/com/util/post-embeds/QuoteEmbed.tsx @@ -1,13 +1,21 @@ -import {StyleSheet} from 'react-native' import React from 'react' +import {StyleProp, StyleSheet, ViewStyle} from 'react-native' +import {AppBskyEmbedImages, AppBskyEmbedRecordWithMedia} from '@atproto/api' import {AtUri} from '../../../../third-party/uri' import {PostMeta} from '../PostMeta' import {Link} from '../Link' import {Text} from '../text/Text' import {usePalette} from 'lib/hooks/usePalette' import {ComposerOptsQuote} from 'state/models/ui/shell' +import {PostEmbeds} from '.' -const QuoteEmbed = ({quote}: {quote: ComposerOptsQuote}) => { +export function QuoteEmbed({ + quote, + style, +}: { + quote: ComposerOptsQuote + style?: StyleProp<ViewStyle> +}) { const pal = usePalette('default') const itemUrip = new AtUri(quote.uri) const itemHref = `/profile/${quote.author.handle}/post/${itemUrip.rkey}` @@ -16,9 +24,18 @@ const QuoteEmbed = ({quote}: {quote: ComposerOptsQuote}) => { () => quote.text.trim().length === 0, [quote.text], ) + const imagesEmbed = React.useMemo( + () => + quote.embeds?.find( + embed => + AppBskyEmbedImages.isView(embed) || + AppBskyEmbedRecordWithMedia.isView(embed), + ), + [quote.embeds], + ) return ( <Link - style={[styles.container, pal.border]} + style={[styles.container, pal.border, style]} href={itemHref} title={itemTitle}> <PostMeta @@ -37,6 +54,12 @@ const QuoteEmbed = ({quote}: {quote: ComposerOptsQuote}) => { quote.text )} </Text> + {AppBskyEmbedImages.isView(imagesEmbed) && ( + <PostEmbeds embed={imagesEmbed} /> + )} + {AppBskyEmbedRecordWithMedia.isView(imagesEmbed) && ( + <PostEmbeds embed={imagesEmbed.media} /> + )} </Link> ) } @@ -48,7 +71,6 @@ const styles = StyleSheet.create({ borderRadius: 8, paddingVertical: 8, paddingHorizontal: 12, - marginVertical: 8, borderWidth: 1, }, quotePost: { diff --git a/src/view/com/util/post-embeds/YoutubeEmbed.tsx b/src/view/com/util/post-embeds/YoutubeEmbed.tsx new file mode 100644 index 000000000..2ca0750a3 --- /dev/null +++ b/src/view/com/util/post-embeds/YoutubeEmbed.tsx @@ -0,0 +1,55 @@ +import React from 'react' +import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' +import {usePalette} from 'lib/hooks/usePalette' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {ExternalLinkEmbed} from './ExternalLinkEmbed' +import {AppBskyEmbedExternal} from '@atproto/api' +import {Link} from '../Link' + +export const YoutubeEmbed = ({ + link, + style, +}: { + link: AppBskyEmbedExternal.ViewExternal + style?: StyleProp<ViewStyle> +}) => { + const pal = usePalette('default') + + const imageChild = ( + <View style={styles.playButton}> + <FontAwesomeIcon icon="play" size={24} color="white" /> + </View> + ) + + return ( + <Link + style={[styles.extOuter, pal.view, pal.border, style]} + href={link.uri} + noFeedback> + <ExternalLinkEmbed link={link} imageChild={imageChild} /> + </Link> + ) +} + +const styles = StyleSheet.create({ + extOuter: { + borderWidth: 1, + borderRadius: 8, + }, + playButton: { + position: 'absolute', + alignSelf: 'center', + alignItems: 'center', + top: '44%', + justifyContent: 'center', + backgroundColor: 'black', + padding: 10, + borderRadius: 50, + opacity: 0.8, + }, + webView: { + alignItems: 'center', + alignContent: 'center', + justifyContent: 'center', + }, +}) diff --git a/src/view/com/util/PostEmbeds/index.tsx b/src/view/com/util/post-embeds/index.tsx index 02a8aa90e..726bea6e7 100644 --- a/src/view/com/util/PostEmbeds/index.tsx +++ b/src/view/com/util/post-embeds/index.tsx @@ -10,6 +10,7 @@ import { AppBskyEmbedImages, AppBskyEmbedExternal, AppBskyEmbedRecord, + AppBskyEmbedRecordWithMedia, AppBskyFeedPost, } from '@atproto/api' import {Link} from '../Link' @@ -19,15 +20,16 @@ import {ImagesLightbox} from 'state/models/ui/shell' import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' import {saveImageModal} from 'lib/media/manip' -import YoutubeEmbed from './YoutubeEmbed' -import ExternalLinkEmbed from './ExternalLinkEmbed' +import {YoutubeEmbed} from './YoutubeEmbed' +import {ExternalLinkEmbed} from './ExternalLinkEmbed' import {getYoutubeVideoId} from 'lib/strings/url-helpers' import QuoteEmbed from './QuoteEmbed' type Embed = - | AppBskyEmbedRecord.Presented - | AppBskyEmbedImages.Presented - | AppBskyEmbedExternal.Presented + | AppBskyEmbedRecord.View + | AppBskyEmbedImages.View + | AppBskyEmbedExternal.View + | AppBskyEmbedRecordWithMedia.View | {$type: string; [k: string]: unknown} export function PostEmbeds({ @@ -39,11 +41,35 @@ export function PostEmbeds({ }) { const pal = usePalette('default') const store = useStores() - if (AppBskyEmbedRecord.isPresented(embed)) { + + if ( + AppBskyEmbedRecordWithMedia.isView(embed) && + AppBskyEmbedRecord.isViewRecord(embed.record.record) && + AppBskyFeedPost.isRecord(embed.record.record.value) && + AppBskyFeedPost.validateRecord(embed.record.record.value).success + ) { + return ( + <View style={[styles.stackContainer, style]}> + <PostEmbeds embed={embed.media} /> + <QuoteEmbed + quote={{ + author: embed.record.record.author, + cid: embed.record.record.cid, + uri: embed.record.record.uri, + indexedAt: embed.record.record.indexedAt, + text: embed.record.record.value.text, + embeds: embed.record.record.embeds, + }} + /> + </View> + ) + } + + if (AppBskyEmbedRecord.isView(embed)) { if ( - AppBskyEmbedRecord.isPresentedRecord(embed.record) && - AppBskyFeedPost.isRecord(embed.record.record) && - AppBskyFeedPost.validateRecord(embed.record.record).success + AppBskyEmbedRecord.isViewRecord(embed.record) && + AppBskyFeedPost.isRecord(embed.record.value) && + AppBskyFeedPost.validateRecord(embed.record.value).success ) { return ( <QuoteEmbed @@ -51,14 +77,17 @@ export function PostEmbeds({ author: embed.record.author, cid: embed.record.cid, uri: embed.record.uri, - indexedAt: embed.record.record.createdAt, // TODO - text: embed.record.record.text, + indexedAt: embed.record.indexedAt, + text: embed.record.value.text, + embeds: embed.record.embeds, }} + style={style} /> ) } } - if (AppBskyEmbedImages.isPresented(embed)) { + + if (AppBskyEmbedImages.isView(embed)) { if (embed.images.length > 0) { const uris = embed.images.map(img => img.fullsize) const openLightbox = (index: number) => { @@ -129,12 +158,13 @@ export function PostEmbeds({ } } } - if (AppBskyEmbedExternal.isPresented(embed)) { + + if (AppBskyEmbedExternal.isView(embed)) { const link = embed.external const youtubeVideoId = getYoutubeVideoId(link.uri) if (youtubeVideoId) { - return <YoutubeEmbed videoId={youtubeVideoId} link={link} /> + return <YoutubeEmbed link={link} style={style} /> } return ( @@ -150,6 +180,9 @@ export function PostEmbeds({ } const styles = StyleSheet.create({ + stackContainer: { + gap: 6, + }, imagesContainer: { marginTop: 4, }, diff --git a/src/view/com/util/text/RichText.tsx b/src/view/com/util/text/RichText.tsx index d4cf19172..804db002a 100644 --- a/src/view/com/util/text/RichText.tsx +++ b/src/view/com/util/text/RichText.tsx @@ -1,20 +1,22 @@ import React from 'react' import {TextStyle, StyleProp} from 'react-native' +import {RichText as RichTextObj, AppBskyRichtextFacet} from '@atproto/api' import {TextLink} from '../Link' import {Text} from './Text' import {lh} from 'lib/styles' import {toShortUrl} from 'lib/strings/url-helpers' -import {RichText as RichTextObj, Entity} from 'lib/strings/rich-text' import {useTheme, TypographyVariant} from 'lib/ThemeContext' import {usePalette} from 'lib/hooks/usePalette' export function RichText({ + testID, type = 'md', richText, lineHeight = 1.2, style, numberOfLines, }: { + testID?: string type?: TypographyVariant richText?: RichTextObj lineHeight?: number @@ -29,17 +31,24 @@ export function RichText({ return null } - const {text, entities} = richText - if (!entities?.length) { + const {text, facets} = richText + if (!facets?.length) { if (/^\p{Extended_Pictographic}+$/u.test(text) && text.length <= 5) { style = { fontSize: 26, lineHeight: 30, } - return <Text style={[style, pal.text]}>{text}</Text> + return ( + <Text testID={testID} style={[style, pal.text]}> + {text} + </Text> + ) } return ( - <Text type={type} style={[style, pal.text, lineHeightStyle]}> + <Text + testID={testID} + type={type} + style={[style, pal.text, lineHeightStyle]}> {text} </Text> ) @@ -49,40 +58,40 @@ export function RichText({ } else if (!Array.isArray(style)) { style = [style] } - entities.sort(sortByIndex) - const segments = Array.from(toSegments(text, entities)) + const els = [] let key = 0 - for (const segment of segments) { - if (typeof segment === 'string') { - els.push(segment) + for (const segment of richText.segments()) { + const link = segment.link + const mention = segment.mention + if (mention && AppBskyRichtextFacet.validateMention(mention).success) { + els.push( + <TextLink + key={key} + type={type} + text={segment.text} + href={`/profile/${mention.did}`} + style={[style, lineHeightStyle, pal.link]} + />, + ) + } else if (link && AppBskyRichtextFacet.validateLink(link).success) { + els.push( + <TextLink + key={key} + type={type} + text={toShortUrl(segment.text)} + href={link.uri} + style={[style, lineHeightStyle, pal.link]} + />, + ) } else { - if (segment.entity.type === 'mention') { - els.push( - <TextLink - key={key} - type={type} - text={segment.text} - href={`/profile/${segment.entity.value}`} - style={[style, lineHeightStyle, pal.link]} - />, - ) - } else if (segment.entity.type === 'link') { - els.push( - <TextLink - key={key} - type={type} - text={toShortUrl(segment.text)} - href={segment.entity.value} - style={[style, lineHeightStyle, pal.link]} - />, - ) - } + els.push(segment.text) } key++ } return ( <Text + testID={testID} type={type} style={[style, pal.text, lineHeightStyle]} numberOfLines={numberOfLines}> @@ -90,38 +99,3 @@ export function RichText({ </Text> ) } - -function sortByIndex(a: Entity, b: Entity) { - return a.index.start - b.index.start -} - -function* toSegments(text: string, entities: Entity[]) { - let cursor = 0 - let i = 0 - do { - let currEnt = entities[i] - if (cursor < currEnt.index.start) { - yield text.slice(cursor, currEnt.index.start) - } else if (cursor > currEnt.index.start) { - i++ - continue - } - if (currEnt.index.start < currEnt.index.end) { - let subtext = text.slice(currEnt.index.start, currEnt.index.end) - if (!subtext.trim()) { - // dont yield links to empty strings - yield subtext - } else { - yield { - entity: currEnt, - text: subtext, - } - } - } - cursor = currEnt.index.end - i++ - } while (i < entities.length) - if (cursor < text.length) { - yield text.slice(cursor, text.length) - } -} diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index 4f2bc4c15..871aae9c7 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -33,6 +33,7 @@ export const HomeScreen = withAuthRequired((_opts: Props) => { useFocusEffect( React.useCallback(() => { + store.shell.setMinimalShellMode(false) store.shell.setIsDrawerSwipeDisabled(selectedPage > 0) return () => { store.shell.setIsDrawerSwipeDisabled(false) @@ -42,6 +43,7 @@ export const HomeScreen = withAuthRequired((_opts: Props) => { const onPageSelected = React.useCallback( (index: number) => { + store.shell.setMinimalShellMode(false) setSelectedPage(index) store.shell.setIsDrawerSwipeDisabled(index > 0) }, @@ -54,7 +56,13 @@ export const HomeScreen = withAuthRequired((_opts: Props) => { const renderTabBar = React.useCallback( (props: RenderTabBarFnProps) => { - return <FeedsTabBar {...props} onPressSelected={onPressSelected} /> + return ( + <FeedsTabBar + {...props} + testID="homeScreenFeedTabs" + onPressSelected={onPressSelected} + /> + ) }, [onPressSelected], ) @@ -66,27 +74,36 @@ export const HomeScreen = withAuthRequired((_opts: Props) => { const initialPage = store.me.follows.isEmpty ? 1 : 0 return ( <Pager + testID="homeScreen" onPageSelected={onPageSelected} renderTabBar={renderTabBar} tabBarPosition="top" initialPage={initialPage}> <FeedPage key="1" + testID="followingFeedPage" isPageFocused={selectedPage === 0} feed={store.me.mainFeed} renderEmptyState={renderFollowingEmptyState} /> - <FeedPage key="2" isPageFocused={selectedPage === 1} feed={algoFeed} /> + <FeedPage + key="2" + testID="whatshotFeedPage" + isPageFocused={selectedPage === 1} + feed={algoFeed} + /> </Pager> ) }) const FeedPage = observer( ({ + testID, isPageFocused, feed, renderEmptyState, }: { + testID?: string feed: FeedModel isPageFocused: boolean renderEmptyState?: () => JSX.Element @@ -163,9 +180,9 @@ const FeedPage = observer( }, [feed, scrollToTop]) return ( - <View style={s.h100pct}> + <View testID={testID} style={s.h100pct}> <Feed - testID="homeFeed" + testID={testID ? `${testID}-feed` : undefined} key="default" feed={feed} scrollElRef={scrollElRef} diff --git a/src/view/screens/NotFound.tsx b/src/view/screens/NotFound.tsx index 6ab37f117..cb52da58b 100644 --- a/src/view/screens/NotFound.tsx +++ b/src/view/screens/NotFound.tsx @@ -1,16 +1,28 @@ import React from 'react' import {StyleSheet, View} from 'react-native' -import {useNavigation, StackActions} from '@react-navigation/native' +import { + useNavigation, + StackActions, + useFocusEffect, +} from '@react-navigation/native' import {ViewHeader} from '../com/util/ViewHeader' import {Text} from '../com/util/text/Text' import {Button} from 'view/com/util/forms/Button' import {NavigationProp} from 'lib/routes/types' import {usePalette} from 'lib/hooks/usePalette' +import {useStores} from 'state/index' import {s} from 'lib/styles' export const NotFoundScreen = () => { const pal = usePalette('default') const navigation = useNavigation<NavigationProp>() + const store = useStores() + + useFocusEffect( + React.useCallback(() => { + store.shell.setMinimalShellMode(false) + }, [store]), + ) const canGoBack = navigation.canGoBack() const onPressHome = React.useCallback(() => { diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx index 7da563843..e5521c7ac 100644 --- a/src/view/screens/Notifications.tsx +++ b/src/view/screens/Notifications.tsx @@ -72,6 +72,7 @@ export const NotificationsScreen = withAuthRequired( // = useFocusEffect( React.useCallback(() => { + store.shell.setMinimalShellMode(false) store.log.debug('NotificationsScreen: Updating feed') const softResetSub = store.onScreenSoftReset(scrollToTop) store.me.notifications.loadUnreadCount() @@ -86,7 +87,7 @@ export const NotificationsScreen = withAuthRequired( ) return ( - <View style={s.hContentRegion}> + <View testID="notificationsScreen" style={s.hContentRegion}> <ViewHeader title="Notifications" canGoBack={false} /> <Feed view={store.me.notifications} diff --git a/src/view/screens/PostUpvotedBy.tsx b/src/view/screens/PostLikedBy.tsx index 35b55f3c4..fb44f1f9b 100644 --- a/src/view/screens/PostUpvotedBy.tsx +++ b/src/view/screens/PostLikedBy.tsx @@ -4,12 +4,12 @@ import {useFocusEffect} from '@react-navigation/native' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {ViewHeader} from '../com/util/ViewHeader' -import {PostVotedBy as PostLikedByComponent} from '../com/post-thread/PostVotedBy' +import {PostLikedBy as PostLikedByComponent} from '../com/post-thread/PostLikedBy' import {useStores} from 'state/index' import {makeRecordUri} from 'lib/strings/url-helpers' -type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostUpvotedBy'> -export const PostUpvotedByScreen = withAuthRequired(({route}: Props) => { +type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostLikedBy'> +export const PostLikedByScreen = withAuthRequired(({route}: Props) => { const store = useStores() const {name, rkey} = route.params const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) @@ -23,7 +23,7 @@ export const PostUpvotedByScreen = withAuthRequired(({route}: Props) => { return ( <View> <ViewHeader title="Liked by" /> - <PostLikedByComponent uri={uri} direction="up" /> + <PostLikedByComponent uri={uri} /> </View> ) }) diff --git a/src/view/screens/PostThread.tsx b/src/view/screens/PostThread.tsx index ad54126b6..9bfdcc95a 100644 --- a/src/view/screens/PostThread.tsx +++ b/src/view/screens/PostThread.tsx @@ -29,8 +29,8 @@ export const PostThreadScreen = withAuthRequired(({route}: Props) => { useFocusEffect( React.useCallback(() => { - const threadCleanup = view.registerListeners() store.shell.setMinimalShellMode(false) + const threadCleanup = view.registerListeners() if (!view.hasLoaded && !view.isLoading) { view.setup().catch(err => { store.log.error('Failed to fetch thread', err) diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index 65f1fef26..556578e77 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -42,6 +42,7 @@ export const ProfileScreen = withAuthRequired( useFocusEffect( React.useCallback(() => { let aborted = false + store.shell.setMinimalShellMode(false) const feedCleanup = uiState.feed.registerListeners() if (hasSetup) { uiState.update() @@ -57,7 +58,7 @@ export const ProfileScreen = withAuthRequired( aborted = true feedCleanup() } - }, [hasSetup, uiState]), + }, [hasSetup, uiState, store]), ) // events diff --git a/src/view/screens/Search.tsx b/src/view/screens/Search.tsx index 641d144ae..e6947013e 100644 --- a/src/view/screens/Search.tsx +++ b/src/view/screens/Search.tsx @@ -152,6 +152,7 @@ export const SearchScreen = withAuthRequired( {autocompleteView.searchRes.map(item => ( <ProfileCard key={item.did} + testID={`searchAutoCompleteResult-${item.handle}`} handle={item.handle} displayName={item.displayName} avatar={item.avatar} diff --git a/src/view/shell/BottomBar.tsx b/src/view/shell/BottomBar.tsx index bfbd7f0a2..e46eeb991 100644 --- a/src/view/shell/BottomBar.tsx +++ b/src/view/shell/BottomBar.tsx @@ -112,6 +112,7 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => { footerMinimalShellTransform, ]}> <Btn + testID="bottomBarHomeBtn" icon={ isAtHome ? ( <HomeIconSolid @@ -130,6 +131,7 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => { onPress={onPressHome} /> <Btn + testID="bottomBarSearchBtn" icon={ isAtSearch ? ( <MagnifyingGlassIcon2Solid @@ -148,6 +150,7 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => { onPress={onPressSearch} /> <Btn + testID="bottomBarNotificationsBtn" icon={ isAtNotifications ? ( <BellIconSolid @@ -167,6 +170,7 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => { notificationCount={store.me.notifications.unreadCount} /> <Btn + testID="bottomBarProfileBtn" icon={ <View style={styles.ctrlIconSizingWrapper}> <UserIcon @@ -183,11 +187,13 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => { }) function Btn({ + testID, icon, notificationCount, onPress, onLongPress, }: { + testID?: string icon: JSX.Element notificationCount?: number onPress?: (event: GestureResponderEvent) => void @@ -195,6 +201,7 @@ function Btn({ }) { return ( <TouchableOpacity + testID={testID} style={styles.ctrl} onPress={onLongPress ? onPress : undefined} onPressIn={onLongPress ? undefined : onPress} diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx index a33cf8c4e..ccf64c0e6 100644 --- a/src/view/shell/Drawer.tsx +++ b/src/view/shell/Drawer.tsx @@ -162,7 +162,7 @@ export const DrawerContent = observer(() => { return ( <View - testID="menuView" + testID="drawer" style={[ styles.view, theme.colorScheme === 'light' ? pal.view : styles.viewDarkMode, diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx index eec0f8ed4..84242c283 100644 --- a/src/view/shell/index.tsx +++ b/src/view/shell/index.tsx @@ -7,11 +7,9 @@ import {useNavigationState} from '@react-navigation/native' import {useStores} from 'state/index' import {ModalsContainer} from 'view/com/modals/Modal' import {Lightbox} from 'view/com/lightbox/Lightbox' -import {Text} from 'view/com/util/text/Text' import {ErrorBoundary} from 'view/com/util/ErrorBoundary' import {DrawerContent} from './Drawer' import {Composer} from './Composer' -import {s} from 'lib/styles' import {useTheme} from 'lib/ThemeContext' import {usePalette} from 'lib/hooks/usePalette' import {RoutesContainer, TabsNavigator} from '../../Navigation' @@ -72,41 +70,6 @@ const ShellInner = observer(() => { export const Shell: React.FC = observer(() => { const theme = useTheme() const pal = usePalette('default') - const store = useStores() - - if (store.hackUpgradeNeeded) { - return ( - <View style={styles.outerContainer}> - <View style={[s.flexCol, s.p20, s.h100pct]}> - <View style={s.flex1} /> - <View> - <Text type="title-2xl" style={s.pb10}> - Update required - </Text> - <Text style={[s.pb20, s.bold]}> - Please update your app to the latest version. If no update is - available yet, please check the App Store in a day or so. - </Text> - <Text type="title" style={s.pb10}> - What's happening? - </Text> - <Text style={s.pb10}> - We're in the final stages of the AT Protocol's v1 development. To - make sure everything works as well as possible, we're making final - breaking changes to the APIs. - </Text> - <Text> - If we didn't botch this process, a new version of the app should - be available now. - </Text> - </View> - <View style={s.flex1} /> - <View style={s.footerSpacer} /> - </View> - </View> - ) - } - return ( <View testID="mobileShellView" style={[styles.outerContainer, pal.view]}> <StatusBar diff --git a/tsconfig.json b/tsconfig.json index f1e7f443d..409a613d7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,7 @@ "lib/*": ["./src/lib/*"], "platform/*": ["./src/platform/*"], "state/*": ["./src/state/*"], - "view/*": ["./src/view/*"] + "view/*": ["./src/view/*"], } } } diff --git a/web/static/js/intl-segmenter-polyfill.min.js b/web/static/js/intl-segmenter-polyfill.min.js new file mode 100644 index 000000000..bdd9af2d3 --- /dev/null +++ b/web/static/js/intl-segmenter-polyfill.min.js @@ -0,0 +1,2 @@ +var k=Object.create;var M=Object.defineProperty;var m=Object.getOwnPropertyDescriptor;var y=Object.getOwnPropertyNames;var j=Object.getPrototypeOf,F=Object.prototype.hasOwnProperty;var l=(x,E)=>()=>(E||x((E={exports:{}}).exports,E),E.exports);var Z=(x,E,R,i)=>{if(E&&typeof E=="object"||typeof E=="function")for(let T of y(E))!F.call(x,T)&&T!==R&&M(x,T,{get:()=>E[T],enumerable:!(i=m(E,T))||i.enumerable});return x};var J=(x,E,R)=>(R=x!=null?k(j(x)):{},Z(E||!x||!x.__esModule?M(R,"default",{value:x,enumerable:!0}):R,x));var V=l(K=>{"use strict";Object.defineProperty(K,"__esModule",{value:!0});K.EXTENDED_PICTOGRAPHIC=K.CLUSTER_BREAK=void 0;var W;(function(x){x[x.CR=0]="CR",x[x.LF=1]="LF",x[x.CONTROL=2]="CONTROL",x[x.EXTEND=3]="EXTEND",x[x.REGIONAL_INDICATOR=4]="REGIONAL_INDICATOR",x[x.SPACINGMARK=5]="SPACINGMARK",x[x.L=6]="L",x[x.V=7]="V",x[x.T=8]="T",x[x.LV=9]="LV",x[x.LVT=10]="LVT",x[x.OTHER=11]="OTHER",x[x.PREPEND=12]="PREPEND",x[x.E_BASE=13]="E_BASE",x[x.E_MODIFIER=14]="E_MODIFIER",x[x.ZWJ=15]="ZWJ",x[x.GLUE_AFTER_ZWJ=16]="GLUE_AFTER_ZWJ",x[x.E_BASE_GAZ=17]="E_BASE_GAZ"})(W=K.CLUSTER_BREAK||(K.CLUSTER_BREAK={}));K.EXTENDED_PICTOGRAPHIC=101});var O=l(X=>{"use strict";Object.defineProperty(X,"__esModule",{value:!0});var r=V(),u=0,N=1,w=2,q=3,z=4,D=class{static isSurrogate(E,R){return 55296<=E.charCodeAt(R)&&E.charCodeAt(R)<=56319&&56320<=E.charCodeAt(R+1)&&E.charCodeAt(R+1)<=57343}static codePointAt(E,R){R===void 0&&(R=0);let i=E.charCodeAt(R);if(55296<=i&&i<=56319&&R<E.length-1){let T=i,t=E.charCodeAt(R+1);return 56320<=t&&t<=57343?(T-55296)*1024+(t-56320)+65536:T}if(56320<=i&&i<=57343&&R>=1){let T=E.charCodeAt(R-1),t=i;return 55296<=T&&T<=56319?(T-55296)*1024+(t-56320)+65536:t}return i}static shouldBreak(E,R,i,T,t,_){let n=[E].concat(R).concat([i]),S=[T].concat(t).concat([_]),A=n[n.length-2],L=i,B=_,G=n.lastIndexOf(r.CLUSTER_BREAK.REGIONAL_INDICATOR);if(G>0&&n.slice(1,G).every(function(s){return s===r.CLUSTER_BREAK.REGIONAL_INDICATOR})&&[r.CLUSTER_BREAK.PREPEND,r.CLUSTER_BREAK.REGIONAL_INDICATOR].indexOf(A)===-1)return n.filter(function(s){return s===r.CLUSTER_BREAK.REGIONAL_INDICATOR}).length%2===1?q:z;if(A===r.CLUSTER_BREAK.CR&&L===r.CLUSTER_BREAK.LF)return u;if(A===r.CLUSTER_BREAK.CONTROL||A===r.CLUSTER_BREAK.CR||A===r.CLUSTER_BREAK.LF)return N;if(L===r.CLUSTER_BREAK.CONTROL||L===r.CLUSTER_BREAK.CR||L===r.CLUSTER_BREAK.LF)return N;if(A===r.CLUSTER_BREAK.L&&(L===r.CLUSTER_BREAK.L||L===r.CLUSTER_BREAK.V||L===r.CLUSTER_BREAK.LV||L===r.CLUSTER_BREAK.LVT))return u;if((A===r.CLUSTER_BREAK.LV||A===r.CLUSTER_BREAK.V)&&(L===r.CLUSTER_BREAK.V||L===r.CLUSTER_BREAK.T))return u;if((A===r.CLUSTER_BREAK.LVT||A===r.CLUSTER_BREAK.T)&&L===r.CLUSTER_BREAK.T)return u;if(L===r.CLUSTER_BREAK.EXTEND||L===r.CLUSTER_BREAK.ZWJ)return u;if(L===r.CLUSTER_BREAK.SPACINGMARK)return u;if(A===r.CLUSTER_BREAK.PREPEND)return u;let a=S.slice(0,-1).lastIndexOf(r.EXTENDED_PICTOGRAPHIC);return a!==-1&&S[a]===r.EXTENDED_PICTOGRAPHIC&&n.slice(a+1,-2).every(function(s){return s===r.CLUSTER_BREAK.EXTEND})&&A===r.CLUSTER_BREAK.ZWJ&&B===r.EXTENDED_PICTOGRAPHIC?u:R.indexOf(r.CLUSTER_BREAK.REGIONAL_INDICATOR)!==-1?w:A===r.CLUSTER_BREAK.REGIONAL_INDICATOR&&L===r.CLUSTER_BREAK.REGIONAL_INDICATOR?u:N}};X.default=D});var H=l(I=>{"use strict";Object.defineProperty(I,"__esModule",{value:!0});var P=class{constructor(E,R){this._index=0,this._str=E,this._nextBreak=R}[Symbol.iterator](){return this}next(){let E;if((E=this._nextBreak(this._str,this._index))<this._str.length){let R=this._str.slice(this._index,E);return this._index=E,{value:R,done:!1}}if(this._index<this._str.length){let R=this._str.slice(this._index);return this._index=this._str.length,{value:R,done:!1}}return{value:void 0,done:!0}}};I.default=P});var g=l(b=>{"use strict";var h=b&&b.__importDefault||function(x){return x&&x.__esModule?x:{default:x}};Object.defineProperty(b,"__esModule",{value:!0});var f=V(),U=h(O()),Q=h(H()),C=class{static nextBreak(E,R){if(R===void 0&&(R=0),R<0)return 0;if(R>=E.length-1)return E.length;let i=U.default.codePointAt(E,R),T=C.getGraphemeBreakProperty(i),t=C.getEmojiProperty(i),_=[],n=[];for(let S=R+1;S<E.length;S++){if(U.default.isSurrogate(E,S-1))continue;let A=U.default.codePointAt(E,S),L=C.getGraphemeBreakProperty(A),B=C.getEmojiProperty(A);if(U.default.shouldBreak(T,_,L,t,n,B))return S;_.push(L),n.push(B)}return E.length}splitGraphemes(E){let R=[],i=0,T;for(;(T=C.nextBreak(E,i))<E.length;)R.push(E.slice(i,T)),i=T;return i<E.length&&R.push(E.slice(i)),R}iterateGraphemes(E){return new Q.default(E,C.nextBreak)}countGraphemes(E){let R=0,i=0,T;for(;(T=C.nextBreak(E,i))<E.length;)i=T,R++;return i<E.length&&R++,R}static getGraphemeBreakProperty(E){if(E<48905){if(E<44116){if(E<4141){if(E<2818){if(E<2363)if(E<1759){if(E<1471){if(E<127){if(E<11){if(E<10){if(0<=E&&E<=9)return f.CLUSTER_BREAK.CONTROL}else if(E===10)return f.CLUSTER_BREAK.LF}else if(E<13){if(11<=E&&E<=12)return f.CLUSTER_BREAK.CONTROL}else if(E<14){if(E===13)return f.CLUSTER_BREAK.CR}else if(14<=E&&E<=31)return f.CLUSTER_BREAK.CONTROL}else if(E<768){if(E<173){if(127<=E&&E<=159)return f.CLUSTER_BREAK.CONTROL}else if(E===173)return f.CLUSTER_BREAK.CONTROL}else if(E<1155){if(768<=E&&E<=879)return f.CLUSTER_BREAK.EXTEND}else if(E<1425){if(1155<=E&&E<=1161)return f.CLUSTER_BREAK.EXTEND}else if(1425<=E&&E<=1469)return f.CLUSTER_BREAK.EXTEND}else if(E<1552){if(E<1476){if(E<1473){if(E===1471)return f.CLUSTER_BREAK.EXTEND}else if(1473<=E&&E<=1474)return f.CLUSTER_BREAK.EXTEND}else if(E<1479){if(1476<=E&&E<=1477)return f.CLUSTER_BREAK.EXTEND}else if(E<1536){if(E===1479)return f.CLUSTER_BREAK.EXTEND}else if(1536<=E&&E<=1541)return f.CLUSTER_BREAK.PREPEND}else if(E<1648){if(E<1564){if(1552<=E&&E<=1562)return f.CLUSTER_BREAK.EXTEND}else if(E<1611){if(E===1564)return f.CLUSTER_BREAK.CONTROL}else if(1611<=E&&E<=1631)return f.CLUSTER_BREAK.EXTEND}else if(E<1750){if(E===1648)return f.CLUSTER_BREAK.EXTEND}else if(E<1757){if(1750<=E&&E<=1756)return f.CLUSTER_BREAK.EXTEND}else if(E===1757)return f.CLUSTER_BREAK.PREPEND}else if(E<2075){if(E<1840)if(E<1770){if(E<1767){if(1759<=E&&E<=1764)return f.CLUSTER_BREAK.EXTEND}else if(1767<=E&&E<=1768)return f.CLUSTER_BREAK.EXTEND}else if(E<1807){if(1770<=E&&E<=1773)return f.CLUSTER_BREAK.EXTEND}else{if(E===1807)return f.CLUSTER_BREAK.PREPEND;if(E===1809)return f.CLUSTER_BREAK.EXTEND}else if(E<2027){if(E<1958){if(1840<=E&&E<=1866)return f.CLUSTER_BREAK.EXTEND}else if(1958<=E&&E<=1968)return f.CLUSTER_BREAK.EXTEND}else if(E<2045){if(2027<=E&&E<=2035)return f.CLUSTER_BREAK.EXTEND}else if(E<2070){if(E===2045)return f.CLUSTER_BREAK.EXTEND}else if(2070<=E&&E<=2073)return f.CLUSTER_BREAK.EXTEND}else if(E<2200){if(E<2089){if(E<2085){if(2075<=E&&E<=2083)return f.CLUSTER_BREAK.EXTEND}else if(2085<=E&&E<=2087)return f.CLUSTER_BREAK.EXTEND}else if(E<2137){if(2089<=E&&E<=2093)return f.CLUSTER_BREAK.EXTEND}else if(E<2192){if(2137<=E&&E<=2139)return f.CLUSTER_BREAK.EXTEND}else if(2192<=E&&E<=2193)return f.CLUSTER_BREAK.PREPEND}else if(E<2275){if(E<2250){if(2200<=E&&E<=2207)return f.CLUSTER_BREAK.EXTEND}else if(E<2274){if(2250<=E&&E<=2273)return f.CLUSTER_BREAK.EXTEND}else if(E===2274)return f.CLUSTER_BREAK.PREPEND}else if(E<2307){if(2275<=E&&E<=2306)return f.CLUSTER_BREAK.EXTEND}else{if(E===2307)return f.CLUSTER_BREAK.SPACINGMARK;if(E===2362)return f.CLUSTER_BREAK.EXTEND}else if(E<2561){if(E<2434){if(E<2381){if(E<2366){if(E===2363)return f.CLUSTER_BREAK.SPACINGMARK;if(E===2364)return f.CLUSTER_BREAK.EXTEND}else if(E<2369){if(2366<=E&&E<=2368)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<2377){if(2369<=E&&E<=2376)return f.CLUSTER_BREAK.EXTEND}else if(2377<=E&&E<=2380)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<2385){if(E<2382){if(E===2381)return f.CLUSTER_BREAK.EXTEND}else if(2382<=E&&E<=2383)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<2402){if(2385<=E&&E<=2391)return f.CLUSTER_BREAK.EXTEND}else if(E<2433){if(2402<=E&&E<=2403)return f.CLUSTER_BREAK.EXTEND}else if(E===2433)return f.CLUSTER_BREAK.EXTEND}else if(E<2503){if(E<2494){if(E<2492){if(2434<=E&&E<=2435)return f.CLUSTER_BREAK.SPACINGMARK}else if(E===2492)return f.CLUSTER_BREAK.EXTEND}else if(E<2495){if(E===2494)return f.CLUSTER_BREAK.EXTEND}else if(E<2497){if(2495<=E&&E<=2496)return f.CLUSTER_BREAK.SPACINGMARK}else if(2497<=E&&E<=2500)return f.CLUSTER_BREAK.EXTEND}else if(E<2519){if(E<2507){if(2503<=E&&E<=2504)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<2509){if(2507<=E&&E<=2508)return f.CLUSTER_BREAK.SPACINGMARK}else if(E===2509)return f.CLUSTER_BREAK.EXTEND}else if(E<2530){if(E===2519)return f.CLUSTER_BREAK.EXTEND}else if(E<2558){if(2530<=E&&E<=2531)return f.CLUSTER_BREAK.EXTEND}else if(E===2558)return f.CLUSTER_BREAK.EXTEND}else if(E<2691){if(E<2631){if(E<2620){if(E<2563){if(2561<=E&&E<=2562)return f.CLUSTER_BREAK.EXTEND}else if(E===2563)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<2622){if(E===2620)return f.CLUSTER_BREAK.EXTEND}else if(E<2625){if(2622<=E&&E<=2624)return f.CLUSTER_BREAK.SPACINGMARK}else if(2625<=E&&E<=2626)return f.CLUSTER_BREAK.EXTEND}else if(E<2672){if(E<2635){if(2631<=E&&E<=2632)return f.CLUSTER_BREAK.EXTEND}else if(E<2641){if(2635<=E&&E<=2637)return f.CLUSTER_BREAK.EXTEND}else if(E===2641)return f.CLUSTER_BREAK.EXTEND}else if(E<2677){if(2672<=E&&E<=2673)return f.CLUSTER_BREAK.EXTEND}else if(E<2689){if(E===2677)return f.CLUSTER_BREAK.EXTEND}else if(2689<=E&&E<=2690)return f.CLUSTER_BREAK.EXTEND}else if(E<2761){if(E<2750){if(E===2691)return f.CLUSTER_BREAK.SPACINGMARK;if(E===2748)return f.CLUSTER_BREAK.EXTEND}else if(E<2753){if(2750<=E&&E<=2752)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<2759){if(2753<=E&&E<=2757)return f.CLUSTER_BREAK.EXTEND}else if(2759<=E&&E<=2760)return f.CLUSTER_BREAK.EXTEND}else if(E<2786){if(E<2763){if(E===2761)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<2765){if(2763<=E&&E<=2764)return f.CLUSTER_BREAK.SPACINGMARK}else if(E===2765)return f.CLUSTER_BREAK.EXTEND}else if(E<2810){if(2786<=E&&E<=2787)return f.CLUSTER_BREAK.EXTEND}else if(E<2817){if(2810<=E&&E<=2815)return f.CLUSTER_BREAK.EXTEND}else if(E===2817)return f.CLUSTER_BREAK.EXTEND}else if(E<3315){if(E<3076){if(E<2946){if(E<2887){if(E<2878){if(E<2876){if(2818<=E&&E<=2819)return f.CLUSTER_BREAK.SPACINGMARK}else if(E===2876)return f.CLUSTER_BREAK.EXTEND}else if(E<2880){if(2878<=E&&E<=2879)return f.CLUSTER_BREAK.EXTEND}else if(E<2881){if(E===2880)return f.CLUSTER_BREAK.SPACINGMARK}else if(2881<=E&&E<=2884)return f.CLUSTER_BREAK.EXTEND}else if(E<2893){if(E<2891){if(2887<=E&&E<=2888)return f.CLUSTER_BREAK.SPACINGMARK}else if(2891<=E&&E<=2892)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<2901){if(E===2893)return f.CLUSTER_BREAK.EXTEND}else if(E<2914){if(2901<=E&&E<=2903)return f.CLUSTER_BREAK.EXTEND}else if(2914<=E&&E<=2915)return f.CLUSTER_BREAK.EXTEND}else if(E<3014){if(E<3007){if(E===2946||E===3006)return f.CLUSTER_BREAK.EXTEND}else if(E<3008){if(E===3007)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<3009){if(E===3008)return f.CLUSTER_BREAK.EXTEND}else if(3009<=E&&E<=3010)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<3031){if(E<3018){if(3014<=E&&E<=3016)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<3021){if(3018<=E&&E<=3020)return f.CLUSTER_BREAK.SPACINGMARK}else if(E===3021)return f.CLUSTER_BREAK.EXTEND}else if(E<3072){if(E===3031)return f.CLUSTER_BREAK.EXTEND}else if(E<3073){if(E===3072)return f.CLUSTER_BREAK.EXTEND}else if(3073<=E&&E<=3075)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<3262){if(E<3146){if(E<3134){if(E===3076||E===3132)return f.CLUSTER_BREAK.EXTEND}else if(E<3137){if(3134<=E&&E<=3136)return f.CLUSTER_BREAK.EXTEND}else if(E<3142){if(3137<=E&&E<=3140)return f.CLUSTER_BREAK.SPACINGMARK}else if(3142<=E&&E<=3144)return f.CLUSTER_BREAK.EXTEND}else if(E<3201){if(E<3157){if(3146<=E&&E<=3149)return f.CLUSTER_BREAK.EXTEND}else if(E<3170){if(3157<=E&&E<=3158)return f.CLUSTER_BREAK.EXTEND}else if(3170<=E&&E<=3171)return f.CLUSTER_BREAK.EXTEND}else if(E<3202){if(E===3201)return f.CLUSTER_BREAK.EXTEND}else if(E<3260){if(3202<=E&&E<=3203)return f.CLUSTER_BREAK.SPACINGMARK}else if(E===3260)return f.CLUSTER_BREAK.EXTEND}else if(E<3270){if(E<3264){if(E===3262)return f.CLUSTER_BREAK.SPACINGMARK;if(E===3263)return f.CLUSTER_BREAK.EXTEND}else if(E<3266){if(3264<=E&&E<=3265)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<3267){if(E===3266)return f.CLUSTER_BREAK.EXTEND}else if(3267<=E&&E<=3268)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<3276){if(E<3271){if(E===3270)return f.CLUSTER_BREAK.EXTEND}else if(E<3274){if(3271<=E&&E<=3272)return f.CLUSTER_BREAK.SPACINGMARK}else if(3274<=E&&E<=3275)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<3285){if(3276<=E&&E<=3277)return f.CLUSTER_BREAK.EXTEND}else if(E<3298){if(3285<=E&&E<=3286)return f.CLUSTER_BREAK.EXTEND}else if(3298<=E&&E<=3299)return f.CLUSTER_BREAK.EXTEND}else if(E<3551){if(E<3406){if(E<3391){if(E<3330){if(E<3328){if(E===3315)return f.CLUSTER_BREAK.SPACINGMARK}else if(3328<=E&&E<=3329)return f.CLUSTER_BREAK.EXTEND}else if(E<3387){if(3330<=E&&E<=3331)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<3390){if(3387<=E&&E<=3388)return f.CLUSTER_BREAK.EXTEND}else if(E===3390)return f.CLUSTER_BREAK.EXTEND}else if(E<3398){if(E<3393){if(3391<=E&&E<=3392)return f.CLUSTER_BREAK.SPACINGMARK}else if(3393<=E&&E<=3396)return f.CLUSTER_BREAK.EXTEND}else if(E<3402){if(3398<=E&&E<=3400)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<3405){if(3402<=E&&E<=3404)return f.CLUSTER_BREAK.SPACINGMARK}else if(E===3405)return f.CLUSTER_BREAK.EXTEND}else if(E<3530){if(E<3426){if(E===3406)return f.CLUSTER_BREAK.PREPEND;if(E===3415)return f.CLUSTER_BREAK.EXTEND}else if(E<3457){if(3426<=E&&E<=3427)return f.CLUSTER_BREAK.EXTEND}else if(E<3458){if(E===3457)return f.CLUSTER_BREAK.EXTEND}else if(3458<=E&&E<=3459)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<3538){if(E<3535){if(E===3530)return f.CLUSTER_BREAK.EXTEND}else if(E<3536){if(E===3535)return f.CLUSTER_BREAK.EXTEND}else if(3536<=E&&E<=3537)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<3542){if(3538<=E&&E<=3540)return f.CLUSTER_BREAK.EXTEND}else if(E<3544){if(E===3542)return f.CLUSTER_BREAK.EXTEND}else if(3544<=E&&E<=3550)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<3893){if(E<3655){if(E<3633){if(E<3570){if(E===3551)return f.CLUSTER_BREAK.EXTEND}else if(3570<=E&&E<=3571)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<3635){if(E===3633)return f.CLUSTER_BREAK.EXTEND}else if(E<3636){if(E===3635)return f.CLUSTER_BREAK.SPACINGMARK}else if(3636<=E&&E<=3642)return f.CLUSTER_BREAK.EXTEND}else if(E<3764)if(E<3761){if(3655<=E&&E<=3662)return f.CLUSTER_BREAK.EXTEND}else{if(E===3761)return f.CLUSTER_BREAK.EXTEND;if(E===3763)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<3784){if(3764<=E&&E<=3772)return f.CLUSTER_BREAK.EXTEND}else if(E<3864){if(3784<=E&&E<=3790)return f.CLUSTER_BREAK.EXTEND}else if(3864<=E&&E<=3865)return f.CLUSTER_BREAK.EXTEND}else if(E<3967){if(E<3897){if(E===3893||E===3895)return f.CLUSTER_BREAK.EXTEND}else if(E<3902){if(E===3897)return f.CLUSTER_BREAK.EXTEND}else if(E<3953){if(3902<=E&&E<=3903)return f.CLUSTER_BREAK.SPACINGMARK}else if(3953<=E&&E<=3966)return f.CLUSTER_BREAK.EXTEND}else if(E<3981){if(E<3968){if(E===3967)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<3974){if(3968<=E&&E<=3972)return f.CLUSTER_BREAK.EXTEND}else if(3974<=E&&E<=3975)return f.CLUSTER_BREAK.EXTEND}else if(E<3993){if(3981<=E&&E<=3991)return f.CLUSTER_BREAK.EXTEND}else if(E<4038){if(3993<=E&&E<=4028)return f.CLUSTER_BREAK.EXTEND}else if(E===4038)return f.CLUSTER_BREAK.EXTEND}else if(E<7204){if(E<6448){if(E<5938){if(E<4226){if(E<4157){if(E<4146){if(E<4145){if(4141<=E&&E<=4144)return f.CLUSTER_BREAK.EXTEND}else if(E===4145)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<4153){if(4146<=E&&E<=4151)return f.CLUSTER_BREAK.EXTEND}else if(E<4155){if(4153<=E&&E<=4154)return f.CLUSTER_BREAK.EXTEND}else if(4155<=E&&E<=4156)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<4184){if(E<4182){if(4157<=E&&E<=4158)return f.CLUSTER_BREAK.EXTEND}else if(4182<=E&&E<=4183)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<4190){if(4184<=E&&E<=4185)return f.CLUSTER_BREAK.EXTEND}else if(E<4209){if(4190<=E&&E<=4192)return f.CLUSTER_BREAK.EXTEND}else if(4209<=E&&E<=4212)return f.CLUSTER_BREAK.EXTEND}else if(E<4352){if(E<4229){if(E===4226)return f.CLUSTER_BREAK.EXTEND;if(E===4228)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<4237){if(4229<=E&&E<=4230)return f.CLUSTER_BREAK.EXTEND}else if(E===4237||E===4253)return f.CLUSTER_BREAK.EXTEND}else if(E<4957){if(E<4448){if(4352<=E&&E<=4447)return f.CLUSTER_BREAK.L}else if(E<4520){if(4448<=E&&E<=4519)return f.CLUSTER_BREAK.V}else if(4520<=E&&E<=4607)return f.CLUSTER_BREAK.T}else if(E<5906){if(4957<=E&&E<=4959)return f.CLUSTER_BREAK.EXTEND}else if(E<5909){if(5906<=E&&E<=5908)return f.CLUSTER_BREAK.EXTEND}else if(E===5909)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<6089){if(E<6070){if(E<5970){if(E<5940){if(5938<=E&&E<=5939)return f.CLUSTER_BREAK.EXTEND}else if(E===5940)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<6002){if(5970<=E&&E<=5971)return f.CLUSTER_BREAK.EXTEND}else if(E<6068){if(6002<=E&&E<=6003)return f.CLUSTER_BREAK.EXTEND}else if(6068<=E&&E<=6069)return f.CLUSTER_BREAK.EXTEND}else if(E<6078){if(E<6071){if(E===6070)return f.CLUSTER_BREAK.SPACINGMARK}else if(6071<=E&&E<=6077)return f.CLUSTER_BREAK.EXTEND}else if(E<6086){if(6078<=E&&E<=6085)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<6087){if(E===6086)return f.CLUSTER_BREAK.EXTEND}else if(6087<=E&&E<=6088)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<6277)if(E<6155){if(E<6109){if(6089<=E&&E<=6099)return f.CLUSTER_BREAK.EXTEND}else if(E===6109)return f.CLUSTER_BREAK.EXTEND}else if(E<6158){if(6155<=E&&E<=6157)return f.CLUSTER_BREAK.EXTEND}else{if(E===6158)return f.CLUSTER_BREAK.CONTROL;if(E===6159)return f.CLUSTER_BREAK.EXTEND}else if(E<6435){if(E<6313){if(6277<=E&&E<=6278)return f.CLUSTER_BREAK.EXTEND}else if(E<6432){if(E===6313)return f.CLUSTER_BREAK.EXTEND}else if(6432<=E&&E<=6434)return f.CLUSTER_BREAK.EXTEND}else if(E<6439){if(6435<=E&&E<=6438)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<6441){if(6439<=E&&E<=6440)return f.CLUSTER_BREAK.EXTEND}else if(6441<=E&&E<=6443)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<6971){if(E<6744)if(E<6681){if(E<6451){if(E<6450){if(6448<=E&&E<=6449)return f.CLUSTER_BREAK.SPACINGMARK}else if(E===6450)return f.CLUSTER_BREAK.EXTEND}else if(E<6457){if(6451<=E&&E<=6456)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<6679){if(6457<=E&&E<=6459)return f.CLUSTER_BREAK.EXTEND}else if(6679<=E&&E<=6680)return f.CLUSTER_BREAK.EXTEND}else if(E<6741){if(E<6683){if(6681<=E&&E<=6682)return f.CLUSTER_BREAK.SPACINGMARK}else if(E===6683)return f.CLUSTER_BREAK.EXTEND}else if(E<6742){if(E===6741)return f.CLUSTER_BREAK.SPACINGMARK}else{if(E===6742)return f.CLUSTER_BREAK.EXTEND;if(E===6743)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<6771){if(E<6754){if(E<6752){if(6744<=E&&E<=6750)return f.CLUSTER_BREAK.EXTEND}else if(E===6752)return f.CLUSTER_BREAK.EXTEND}else if(E<6757){if(E===6754)return f.CLUSTER_BREAK.EXTEND}else if(E<6765){if(6757<=E&&E<=6764)return f.CLUSTER_BREAK.EXTEND}else if(6765<=E&&E<=6770)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<6912){if(E<6783){if(6771<=E&&E<=6780)return f.CLUSTER_BREAK.EXTEND}else if(E<6832){if(E===6783)return f.CLUSTER_BREAK.EXTEND}else if(6832<=E&&E<=6862)return f.CLUSTER_BREAK.EXTEND}else if(E<6916){if(6912<=E&&E<=6915)return f.CLUSTER_BREAK.EXTEND}else if(E<6964){if(E===6916)return f.CLUSTER_BREAK.SPACINGMARK}else if(6964<=E&&E<=6970)return f.CLUSTER_BREAK.EXTEND}else if(E<7080){if(E<7019){if(E<6973){if(E===6971)return f.CLUSTER_BREAK.SPACINGMARK;if(E===6972)return f.CLUSTER_BREAK.EXTEND}else if(E<6978){if(6973<=E&&E<=6977)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<6979){if(E===6978)return f.CLUSTER_BREAK.EXTEND}else if(6979<=E&&E<=6980)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<7073){if(E<7040){if(7019<=E&&E<=7027)return f.CLUSTER_BREAK.EXTEND}else if(E<7042){if(7040<=E&&E<=7041)return f.CLUSTER_BREAK.EXTEND}else if(E===7042)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<7074){if(E===7073)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<7078){if(7074<=E&&E<=7077)return f.CLUSTER_BREAK.EXTEND}else if(7078<=E&&E<=7079)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<7144)if(E<7083){if(E<7082){if(7080<=E&&E<=7081)return f.CLUSTER_BREAK.EXTEND}else if(E===7082)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<7142){if(7083<=E&&E<=7085)return f.CLUSTER_BREAK.EXTEND}else{if(E===7142)return f.CLUSTER_BREAK.EXTEND;if(E===7143)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<7150){if(E<7146){if(7144<=E&&E<=7145)return f.CLUSTER_BREAK.EXTEND}else if(E<7149){if(7146<=E&&E<=7148)return f.CLUSTER_BREAK.SPACINGMARK}else if(E===7149)return f.CLUSTER_BREAK.EXTEND}else if(E<7151){if(E===7150)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<7154){if(7151<=E&&E<=7153)return f.CLUSTER_BREAK.EXTEND}else if(7154<=E&&E<=7155)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<43346){if(E<11647){if(E<7415){if(E<7380){if(E<7220){if(E<7212){if(7204<=E&&E<=7211)return f.CLUSTER_BREAK.SPACINGMARK}else if(7212<=E&&E<=7219)return f.CLUSTER_BREAK.EXTEND}else if(E<7222){if(7220<=E&&E<=7221)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<7376){if(7222<=E&&E<=7223)return f.CLUSTER_BREAK.EXTEND}else if(7376<=E&&E<=7378)return f.CLUSTER_BREAK.EXTEND}else if(E<7394){if(E<7393){if(7380<=E&&E<=7392)return f.CLUSTER_BREAK.EXTEND}else if(E===7393)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<7405){if(7394<=E&&E<=7400)return f.CLUSTER_BREAK.EXTEND}else if(E===7405||E===7412)return f.CLUSTER_BREAK.EXTEND}else if(E<8205)if(E<7616){if(E<7416){if(E===7415)return f.CLUSTER_BREAK.SPACINGMARK}else if(7416<=E&&E<=7417)return f.CLUSTER_BREAK.EXTEND}else if(E<8203){if(7616<=E&&E<=7679)return f.CLUSTER_BREAK.EXTEND}else{if(E===8203)return f.CLUSTER_BREAK.CONTROL;if(E===8204)return f.CLUSTER_BREAK.EXTEND}else if(E<8288){if(E<8206){if(E===8205)return f.CLUSTER_BREAK.ZWJ}else if(E<8232){if(8206<=E&&E<=8207)return f.CLUSTER_BREAK.CONTROL}else if(8232<=E&&E<=8238)return f.CLUSTER_BREAK.CONTROL}else if(E<8400){if(8288<=E&&E<=8303)return f.CLUSTER_BREAK.CONTROL}else if(E<11503){if(8400<=E&&E<=8432)return f.CLUSTER_BREAK.EXTEND}else if(11503<=E&&E<=11505)return f.CLUSTER_BREAK.EXTEND}else if(E<43043){if(E<42612){if(E<12330){if(E<11744){if(E===11647)return f.CLUSTER_BREAK.EXTEND}else if(11744<=E&&E<=11775)return f.CLUSTER_BREAK.EXTEND}else if(E<12441){if(12330<=E&&E<=12335)return f.CLUSTER_BREAK.EXTEND}else if(E<42607){if(12441<=E&&E<=12442)return f.CLUSTER_BREAK.EXTEND}else if(42607<=E&&E<=42610)return f.CLUSTER_BREAK.EXTEND}else if(E<43010){if(E<42654){if(42612<=E&&E<=42621)return f.CLUSTER_BREAK.EXTEND}else if(E<42736){if(42654<=E&&E<=42655)return f.CLUSTER_BREAK.EXTEND}else if(42736<=E&&E<=42737)return f.CLUSTER_BREAK.EXTEND}else if(E<43014){if(E===43010)return f.CLUSTER_BREAK.EXTEND}else if(E===43014||E===43019)return f.CLUSTER_BREAK.EXTEND}else if(E<43188){if(E<43047){if(E<43045){if(43043<=E&&E<=43044)return f.CLUSTER_BREAK.SPACINGMARK}else if(43045<=E&&E<=43046)return f.CLUSTER_BREAK.EXTEND}else if(E<43052){if(E===43047)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<43136){if(E===43052)return f.CLUSTER_BREAK.EXTEND}else if(43136<=E&&E<=43137)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<43263){if(E<43204){if(43188<=E&&E<=43203)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<43232){if(43204<=E&&E<=43205)return f.CLUSTER_BREAK.EXTEND}else if(43232<=E&&E<=43249)return f.CLUSTER_BREAK.EXTEND}else if(E<43302){if(E===43263)return f.CLUSTER_BREAK.EXTEND}else if(E<43335){if(43302<=E&&E<=43309)return f.CLUSTER_BREAK.EXTEND}else if(43335<=E&&E<=43345)return f.CLUSTER_BREAK.EXTEND}else if(E<43698){if(E<43493){if(E<43444)if(E<43392){if(E<43360){if(43346<=E&&E<=43347)return f.CLUSTER_BREAK.SPACINGMARK}else if(43360<=E&&E<=43388)return f.CLUSTER_BREAK.L}else if(E<43395){if(43392<=E&&E<=43394)return f.CLUSTER_BREAK.EXTEND}else{if(E===43395)return f.CLUSTER_BREAK.SPACINGMARK;if(E===43443)return f.CLUSTER_BREAK.EXTEND}else if(E<43450){if(E<43446){if(43444<=E&&E<=43445)return f.CLUSTER_BREAK.SPACINGMARK}else if(43446<=E&&E<=43449)return f.CLUSTER_BREAK.EXTEND}else if(E<43452){if(43450<=E&&E<=43451)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<43454){if(43452<=E&&E<=43453)return f.CLUSTER_BREAK.EXTEND}else if(43454<=E&&E<=43456)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<43573){if(E<43567){if(E<43561){if(E===43493)return f.CLUSTER_BREAK.EXTEND}else if(43561<=E&&E<=43566)return f.CLUSTER_BREAK.EXTEND}else if(E<43569){if(43567<=E&&E<=43568)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<43571){if(43569<=E&&E<=43570)return f.CLUSTER_BREAK.EXTEND}else if(43571<=E&&E<=43572)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<43597){if(E<43587){if(43573<=E&&E<=43574)return f.CLUSTER_BREAK.EXTEND}else if(E===43587||E===43596)return f.CLUSTER_BREAK.EXTEND}else if(E<43644){if(E===43597)return f.CLUSTER_BREAK.SPACINGMARK}else if(E===43644||E===43696)return f.CLUSTER_BREAK.EXTEND}else if(E<44006){if(E<43756)if(E<43710){if(E<43703){if(43698<=E&&E<=43700)return f.CLUSTER_BREAK.EXTEND}else if(43703<=E&&E<=43704)return f.CLUSTER_BREAK.EXTEND}else if(E<43713){if(43710<=E&&E<=43711)return f.CLUSTER_BREAK.EXTEND}else{if(E===43713)return f.CLUSTER_BREAK.EXTEND;if(E===43755)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<43766){if(E<43758){if(43756<=E&&E<=43757)return f.CLUSTER_BREAK.EXTEND}else if(E<43765){if(43758<=E&&E<=43759)return f.CLUSTER_BREAK.SPACINGMARK}else if(E===43765)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<44003){if(E===43766)return f.CLUSTER_BREAK.EXTEND}else if(E<44005){if(44003<=E&&E<=44004)return f.CLUSTER_BREAK.SPACINGMARK}else if(E===44005)return f.CLUSTER_BREAK.EXTEND}else if(E<44032)if(E<44009){if(E<44008){if(44006<=E&&E<=44007)return f.CLUSTER_BREAK.SPACINGMARK}else if(E===44008)return f.CLUSTER_BREAK.EXTEND}else if(E<44012){if(44009<=E&&E<=44010)return f.CLUSTER_BREAK.SPACINGMARK}else{if(E===44012)return f.CLUSTER_BREAK.SPACINGMARK;if(E===44013)return f.CLUSTER_BREAK.EXTEND}else if(E<44061){if(E<44033){if(E===44032)return f.CLUSTER_BREAK.LV}else if(E<44060){if(44033<=E&&E<=44059)return f.CLUSTER_BREAK.LVT}else if(E===44060)return f.CLUSTER_BREAK.LV}else if(E<44088){if(44061<=E&&E<=44087)return f.CLUSTER_BREAK.LVT}else if(E<44089){if(E===44088)return f.CLUSTER_BREAK.LV}else if(44089<=E&&E<=44115)return f.CLUSTER_BREAK.LVT}else if(E<46497){if(E<45293){if(E<44704){if(E<44397){if(E<44256){if(E<44173){if(E<44144){if(E<44117){if(E===44116)return f.CLUSTER_BREAK.LV}else if(44117<=E&&E<=44143)return f.CLUSTER_BREAK.LVT}else if(E<44145){if(E===44144)return f.CLUSTER_BREAK.LV}else if(E<44172){if(44145<=E&&E<=44171)return f.CLUSTER_BREAK.LVT}else if(E===44172)return f.CLUSTER_BREAK.LV}else if(E<44201){if(E<44200){if(44173<=E&&E<=44199)return f.CLUSTER_BREAK.LVT}else if(E===44200)return f.CLUSTER_BREAK.LV}else if(E<44228){if(44201<=E&&E<=44227)return f.CLUSTER_BREAK.LVT}else if(E<44229){if(E===44228)return f.CLUSTER_BREAK.LV}else if(44229<=E&&E<=44255)return f.CLUSTER_BREAK.LVT}else if(E<44313){if(E<44284){if(E<44257){if(E===44256)return f.CLUSTER_BREAK.LV}else if(44257<=E&&E<=44283)return f.CLUSTER_BREAK.LVT}else if(E<44285){if(E===44284)return f.CLUSTER_BREAK.LV}else if(E<44312){if(44285<=E&&E<=44311)return f.CLUSTER_BREAK.LVT}else if(E===44312)return f.CLUSTER_BREAK.LV}else if(E<44368){if(E<44340){if(44313<=E&&E<=44339)return f.CLUSTER_BREAK.LVT}else if(E<44341){if(E===44340)return f.CLUSTER_BREAK.LV}else if(44341<=E&&E<=44367)return f.CLUSTER_BREAK.LVT}else if(E<44369){if(E===44368)return f.CLUSTER_BREAK.LV}else if(E<44396){if(44369<=E&&E<=44395)return f.CLUSTER_BREAK.LVT}else if(E===44396)return f.CLUSTER_BREAK.LV}else if(E<44537){if(E<44480){if(E<44425){if(E<44424){if(44397<=E&&E<=44423)return f.CLUSTER_BREAK.LVT}else if(E===44424)return f.CLUSTER_BREAK.LV}else if(E<44452){if(44425<=E&&E<=44451)return f.CLUSTER_BREAK.LVT}else if(E<44453){if(E===44452)return f.CLUSTER_BREAK.LV}else if(44453<=E&&E<=44479)return f.CLUSTER_BREAK.LVT}else if(E<44508){if(E<44481){if(E===44480)return f.CLUSTER_BREAK.LV}else if(44481<=E&&E<=44507)return f.CLUSTER_BREAK.LVT}else if(E<44509){if(E===44508)return f.CLUSTER_BREAK.LV}else if(E<44536){if(44509<=E&&E<=44535)return f.CLUSTER_BREAK.LVT}else if(E===44536)return f.CLUSTER_BREAK.LV}else if(E<44620){if(E<44565){if(E<44564){if(44537<=E&&E<=44563)return f.CLUSTER_BREAK.LVT}else if(E===44564)return f.CLUSTER_BREAK.LV}else if(E<44592){if(44565<=E&&E<=44591)return f.CLUSTER_BREAK.LVT}else if(E<44593){if(E===44592)return f.CLUSTER_BREAK.LV}else if(44593<=E&&E<=44619)return f.CLUSTER_BREAK.LVT}else if(E<44649){if(E<44621){if(E===44620)return f.CLUSTER_BREAK.LV}else if(E<44648){if(44621<=E&&E<=44647)return f.CLUSTER_BREAK.LVT}else if(E===44648)return f.CLUSTER_BREAK.LV}else if(E<44676){if(44649<=E&&E<=44675)return f.CLUSTER_BREAK.LVT}else if(E<44677){if(E===44676)return f.CLUSTER_BREAK.LV}else if(44677<=E&&E<=44703)return f.CLUSTER_BREAK.LVT}else if(E<44985){if(E<44844){if(E<44761){if(E<44732){if(E<44705){if(E===44704)return f.CLUSTER_BREAK.LV}else if(44705<=E&&E<=44731)return f.CLUSTER_BREAK.LVT}else if(E<44733){if(E===44732)return f.CLUSTER_BREAK.LV}else if(E<44760){if(44733<=E&&E<=44759)return f.CLUSTER_BREAK.LVT}else if(E===44760)return f.CLUSTER_BREAK.LV}else if(E<44789){if(E<44788){if(44761<=E&&E<=44787)return f.CLUSTER_BREAK.LVT}else if(E===44788)return f.CLUSTER_BREAK.LV}else if(E<44816){if(44789<=E&&E<=44815)return f.CLUSTER_BREAK.LVT}else if(E<44817){if(E===44816)return f.CLUSTER_BREAK.LV}else if(44817<=E&&E<=44843)return f.CLUSTER_BREAK.LVT}else if(E<44901){if(E<44872){if(E<44845){if(E===44844)return f.CLUSTER_BREAK.LV}else if(44845<=E&&E<=44871)return f.CLUSTER_BREAK.LVT}else if(E<44873){if(E===44872)return f.CLUSTER_BREAK.LV}else if(E<44900){if(44873<=E&&E<=44899)return f.CLUSTER_BREAK.LVT}else if(E===44900)return f.CLUSTER_BREAK.LV}else if(E<44956){if(E<44928){if(44901<=E&&E<=44927)return f.CLUSTER_BREAK.LVT}else if(E<44929){if(E===44928)return f.CLUSTER_BREAK.LV}else if(44929<=E&&E<=44955)return f.CLUSTER_BREAK.LVT}else if(E<44957){if(E===44956)return f.CLUSTER_BREAK.LV}else if(E<44984){if(44957<=E&&E<=44983)return f.CLUSTER_BREAK.LVT}else if(E===44984)return f.CLUSTER_BREAK.LV}else if(E<45152){if(E<45068){if(E<45013){if(E<45012){if(44985<=E&&E<=45011)return f.CLUSTER_BREAK.LVT}else if(E===45012)return f.CLUSTER_BREAK.LV}else if(E<45040){if(45013<=E&&E<=45039)return f.CLUSTER_BREAK.LVT}else if(E<45041){if(E===45040)return f.CLUSTER_BREAK.LV}else if(45041<=E&&E<=45067)return f.CLUSTER_BREAK.LVT}else if(E<45097){if(E<45069){if(E===45068)return f.CLUSTER_BREAK.LV}else if(E<45096){if(45069<=E&&E<=45095)return f.CLUSTER_BREAK.LVT}else if(E===45096)return f.CLUSTER_BREAK.LV}else if(E<45124){if(45097<=E&&E<=45123)return f.CLUSTER_BREAK.LVT}else if(E<45125){if(E===45124)return f.CLUSTER_BREAK.LV}else if(45125<=E&&E<=45151)return f.CLUSTER_BREAK.LVT}else if(E<45209){if(E<45180){if(E<45153){if(E===45152)return f.CLUSTER_BREAK.LV}else if(45153<=E&&E<=45179)return f.CLUSTER_BREAK.LVT}else if(E<45181){if(E===45180)return f.CLUSTER_BREAK.LV}else if(E<45208){if(45181<=E&&E<=45207)return f.CLUSTER_BREAK.LVT}else if(E===45208)return f.CLUSTER_BREAK.LV}else if(E<45264){if(E<45236){if(45209<=E&&E<=45235)return f.CLUSTER_BREAK.LVT}else if(E<45237){if(E===45236)return f.CLUSTER_BREAK.LV}else if(45237<=E&&E<=45263)return f.CLUSTER_BREAK.LVT}else if(E<45265){if(E===45264)return f.CLUSTER_BREAK.LV}else if(E<45292){if(45265<=E&&E<=45291)return f.CLUSTER_BREAK.LVT}else if(E===45292)return f.CLUSTER_BREAK.LV}else if(E<45908){if(E<45600){if(E<45433){if(E<45376){if(E<45321){if(E<45320){if(45293<=E&&E<=45319)return f.CLUSTER_BREAK.LVT}else if(E===45320)return f.CLUSTER_BREAK.LV}else if(E<45348){if(45321<=E&&E<=45347)return f.CLUSTER_BREAK.LVT}else if(E<45349){if(E===45348)return f.CLUSTER_BREAK.LV}else if(45349<=E&&E<=45375)return f.CLUSTER_BREAK.LVT}else if(E<45404){if(E<45377){if(E===45376)return f.CLUSTER_BREAK.LV}else if(45377<=E&&E<=45403)return f.CLUSTER_BREAK.LVT}else if(E<45405){if(E===45404)return f.CLUSTER_BREAK.LV}else if(E<45432){if(45405<=E&&E<=45431)return f.CLUSTER_BREAK.LVT}else if(E===45432)return f.CLUSTER_BREAK.LV}else if(E<45516){if(E<45461){if(E<45460){if(45433<=E&&E<=45459)return f.CLUSTER_BREAK.LVT}else if(E===45460)return f.CLUSTER_BREAK.LV}else if(E<45488){if(45461<=E&&E<=45487)return f.CLUSTER_BREAK.LVT}else if(E<45489){if(E===45488)return f.CLUSTER_BREAK.LV}else if(45489<=E&&E<=45515)return f.CLUSTER_BREAK.LVT}else if(E<45545){if(E<45517){if(E===45516)return f.CLUSTER_BREAK.LV}else if(E<45544){if(45517<=E&&E<=45543)return f.CLUSTER_BREAK.LVT}else if(E===45544)return f.CLUSTER_BREAK.LV}else if(E<45572){if(45545<=E&&E<=45571)return f.CLUSTER_BREAK.LVT}else if(E<45573){if(E===45572)return f.CLUSTER_BREAK.LV}else if(45573<=E&&E<=45599)return f.CLUSTER_BREAK.LVT}else if(E<45741){if(E<45657){if(E<45628){if(E<45601){if(E===45600)return f.CLUSTER_BREAK.LV}else if(45601<=E&&E<=45627)return f.CLUSTER_BREAK.LVT}else if(E<45629){if(E===45628)return f.CLUSTER_BREAK.LV}else if(E<45656){if(45629<=E&&E<=45655)return f.CLUSTER_BREAK.LVT}else if(E===45656)return f.CLUSTER_BREAK.LV}else if(E<45712){if(E<45684){if(45657<=E&&E<=45683)return f.CLUSTER_BREAK.LVT}else if(E<45685){if(E===45684)return f.CLUSTER_BREAK.LV}else if(45685<=E&&E<=45711)return f.CLUSTER_BREAK.LVT}else if(E<45713){if(E===45712)return f.CLUSTER_BREAK.LV}else if(E<45740){if(45713<=E&&E<=45739)return f.CLUSTER_BREAK.LVT}else if(E===45740)return f.CLUSTER_BREAK.LV}else if(E<45824){if(E<45769){if(E<45768){if(45741<=E&&E<=45767)return f.CLUSTER_BREAK.LVT}else if(E===45768)return f.CLUSTER_BREAK.LV}else if(E<45796){if(45769<=E&&E<=45795)return f.CLUSTER_BREAK.LVT}else if(E<45797){if(E===45796)return f.CLUSTER_BREAK.LV}else if(45797<=E&&E<=45823)return f.CLUSTER_BREAK.LVT}else if(E<45853){if(E<45825){if(E===45824)return f.CLUSTER_BREAK.LV}else if(E<45852){if(45825<=E&&E<=45851)return f.CLUSTER_BREAK.LVT}else if(E===45852)return f.CLUSTER_BREAK.LV}else if(E<45880){if(45853<=E&&E<=45879)return f.CLUSTER_BREAK.LVT}else if(E<45881){if(E===45880)return f.CLUSTER_BREAK.LV}else if(45881<=E&&E<=45907)return f.CLUSTER_BREAK.LVT}else if(E<46189){if(E<46048){if(E<45965){if(E<45936){if(E<45909){if(E===45908)return f.CLUSTER_BREAK.LV}else if(45909<=E&&E<=45935)return f.CLUSTER_BREAK.LVT}else if(E<45937){if(E===45936)return f.CLUSTER_BREAK.LV}else if(E<45964){if(45937<=E&&E<=45963)return f.CLUSTER_BREAK.LVT}else if(E===45964)return f.CLUSTER_BREAK.LV}else if(E<45993){if(E<45992){if(45965<=E&&E<=45991)return f.CLUSTER_BREAK.LVT}else if(E===45992)return f.CLUSTER_BREAK.LV}else if(E<46020){if(45993<=E&&E<=46019)return f.CLUSTER_BREAK.LVT}else if(E<46021){if(E===46020)return f.CLUSTER_BREAK.LV}else if(46021<=E&&E<=46047)return f.CLUSTER_BREAK.LVT}else if(E<46105){if(E<46076){if(E<46049){if(E===46048)return f.CLUSTER_BREAK.LV}else if(46049<=E&&E<=46075)return f.CLUSTER_BREAK.LVT}else if(E<46077){if(E===46076)return f.CLUSTER_BREAK.LV}else if(E<46104){if(46077<=E&&E<=46103)return f.CLUSTER_BREAK.LVT}else if(E===46104)return f.CLUSTER_BREAK.LV}else if(E<46160){if(E<46132){if(46105<=E&&E<=46131)return f.CLUSTER_BREAK.LVT}else if(E<46133){if(E===46132)return f.CLUSTER_BREAK.LV}else if(46133<=E&&E<=46159)return f.CLUSTER_BREAK.LVT}else if(E<46161){if(E===46160)return f.CLUSTER_BREAK.LV}else if(E<46188){if(46161<=E&&E<=46187)return f.CLUSTER_BREAK.LVT}else if(E===46188)return f.CLUSTER_BREAK.LV}else if(E<46356){if(E<46272){if(E<46217){if(E<46216){if(46189<=E&&E<=46215)return f.CLUSTER_BREAK.LVT}else if(E===46216)return f.CLUSTER_BREAK.LV}else if(E<46244){if(46217<=E&&E<=46243)return f.CLUSTER_BREAK.LVT}else if(E<46245){if(E===46244)return f.CLUSTER_BREAK.LV}else if(46245<=E&&E<=46271)return f.CLUSTER_BREAK.LVT}else if(E<46301){if(E<46273){if(E===46272)return f.CLUSTER_BREAK.LV}else if(E<46300){if(46273<=E&&E<=46299)return f.CLUSTER_BREAK.LVT}else if(E===46300)return f.CLUSTER_BREAK.LV}else if(E<46328){if(46301<=E&&E<=46327)return f.CLUSTER_BREAK.LVT}else if(E<46329){if(E===46328)return f.CLUSTER_BREAK.LV}else if(46329<=E&&E<=46355)return f.CLUSTER_BREAK.LVT}else if(E<46413){if(E<46384){if(E<46357){if(E===46356)return f.CLUSTER_BREAK.LV}else if(46357<=E&&E<=46383)return f.CLUSTER_BREAK.LVT}else if(E<46385){if(E===46384)return f.CLUSTER_BREAK.LV}else if(E<46412){if(46385<=E&&E<=46411)return f.CLUSTER_BREAK.LVT}else if(E===46412)return f.CLUSTER_BREAK.LV}else if(E<46468){if(E<46440){if(46413<=E&&E<=46439)return f.CLUSTER_BREAK.LVT}else if(E<46441){if(E===46440)return f.CLUSTER_BREAK.LV}else if(46441<=E&&E<=46467)return f.CLUSTER_BREAK.LVT}else if(E<46469){if(E===46468)return f.CLUSTER_BREAK.LV}else if(E<46496){if(46469<=E&&E<=46495)return f.CLUSTER_BREAK.LVT}else if(E===46496)return f.CLUSTER_BREAK.LV}else if(E<47701){if(E<47112){if(E<46804){if(E<46637){if(E<46580){if(E<46525){if(E<46524){if(46497<=E&&E<=46523)return f.CLUSTER_BREAK.LVT}else if(E===46524)return f.CLUSTER_BREAK.LV}else if(E<46552){if(46525<=E&&E<=46551)return f.CLUSTER_BREAK.LVT}else if(E<46553){if(E===46552)return f.CLUSTER_BREAK.LV}else if(46553<=E&&E<=46579)return f.CLUSTER_BREAK.LVT}else if(E<46608){if(E<46581){if(E===46580)return f.CLUSTER_BREAK.LV}else if(46581<=E&&E<=46607)return f.CLUSTER_BREAK.LVT}else if(E<46609){if(E===46608)return f.CLUSTER_BREAK.LV}else if(E<46636){if(46609<=E&&E<=46635)return f.CLUSTER_BREAK.LVT}else if(E===46636)return f.CLUSTER_BREAK.LV}else if(E<46720){if(E<46665){if(E<46664){if(46637<=E&&E<=46663)return f.CLUSTER_BREAK.LVT}else if(E===46664)return f.CLUSTER_BREAK.LV}else if(E<46692){if(46665<=E&&E<=46691)return f.CLUSTER_BREAK.LVT}else if(E<46693){if(E===46692)return f.CLUSTER_BREAK.LV}else if(46693<=E&&E<=46719)return f.CLUSTER_BREAK.LVT}else if(E<46749){if(E<46721){if(E===46720)return f.CLUSTER_BREAK.LV}else if(E<46748){if(46721<=E&&E<=46747)return f.CLUSTER_BREAK.LVT}else if(E===46748)return f.CLUSTER_BREAK.LV}else if(E<46776){if(46749<=E&&E<=46775)return f.CLUSTER_BREAK.LVT}else if(E<46777){if(E===46776)return f.CLUSTER_BREAK.LV}else if(46777<=E&&E<=46803)return f.CLUSTER_BREAK.LVT}else if(E<46945){if(E<46861){if(E<46832){if(E<46805){if(E===46804)return f.CLUSTER_BREAK.LV}else if(46805<=E&&E<=46831)return f.CLUSTER_BREAK.LVT}else if(E<46833){if(E===46832)return f.CLUSTER_BREAK.LV}else if(E<46860){if(46833<=E&&E<=46859)return f.CLUSTER_BREAK.LVT}else if(E===46860)return f.CLUSTER_BREAK.LV}else if(E<46916){if(E<46888){if(46861<=E&&E<=46887)return f.CLUSTER_BREAK.LVT}else if(E<46889){if(E===46888)return f.CLUSTER_BREAK.LV}else if(46889<=E&&E<=46915)return f.CLUSTER_BREAK.LVT}else if(E<46917){if(E===46916)return f.CLUSTER_BREAK.LV}else if(E<46944){if(46917<=E&&E<=46943)return f.CLUSTER_BREAK.LVT}else if(E===46944)return f.CLUSTER_BREAK.LV}else if(E<47028){if(E<46973){if(E<46972){if(46945<=E&&E<=46971)return f.CLUSTER_BREAK.LVT}else if(E===46972)return f.CLUSTER_BREAK.LV}else if(E<47e3){if(46973<=E&&E<=46999)return f.CLUSTER_BREAK.LVT}else if(E<47001){if(E===47e3)return f.CLUSTER_BREAK.LV}else if(47001<=E&&E<=47027)return f.CLUSTER_BREAK.LVT}else if(E<47057){if(E<47029){if(E===47028)return f.CLUSTER_BREAK.LV}else if(E<47056){if(47029<=E&&E<=47055)return f.CLUSTER_BREAK.LVT}else if(E===47056)return f.CLUSTER_BREAK.LV}else if(E<47084){if(47057<=E&&E<=47083)return f.CLUSTER_BREAK.LVT}else if(E<47085){if(E===47084)return f.CLUSTER_BREAK.LV}else if(47085<=E&&E<=47111)return f.CLUSTER_BREAK.LVT}else if(E<47393){if(E<47252){if(E<47169){if(E<47140){if(E<47113){if(E===47112)return f.CLUSTER_BREAK.LV}else if(47113<=E&&E<=47139)return f.CLUSTER_BREAK.LVT}else if(E<47141){if(E===47140)return f.CLUSTER_BREAK.LV}else if(E<47168){if(47141<=E&&E<=47167)return f.CLUSTER_BREAK.LVT}else if(E===47168)return f.CLUSTER_BREAK.LV}else if(E<47197){if(E<47196){if(47169<=E&&E<=47195)return f.CLUSTER_BREAK.LVT}else if(E===47196)return f.CLUSTER_BREAK.LV}else if(E<47224){if(47197<=E&&E<=47223)return f.CLUSTER_BREAK.LVT}else if(E<47225){if(E===47224)return f.CLUSTER_BREAK.LV}else if(47225<=E&&E<=47251)return f.CLUSTER_BREAK.LVT}else if(E<47309){if(E<47280){if(E<47253){if(E===47252)return f.CLUSTER_BREAK.LV}else if(47253<=E&&E<=47279)return f.CLUSTER_BREAK.LVT}else if(E<47281){if(E===47280)return f.CLUSTER_BREAK.LV}else if(E<47308){if(47281<=E&&E<=47307)return f.CLUSTER_BREAK.LVT}else if(E===47308)return f.CLUSTER_BREAK.LV}else if(E<47364){if(E<47336){if(47309<=E&&E<=47335)return f.CLUSTER_BREAK.LVT}else if(E<47337){if(E===47336)return f.CLUSTER_BREAK.LV}else if(47337<=E&&E<=47363)return f.CLUSTER_BREAK.LVT}else if(E<47365){if(E===47364)return f.CLUSTER_BREAK.LV}else if(E<47392){if(47365<=E&&E<=47391)return f.CLUSTER_BREAK.LVT}else if(E===47392)return f.CLUSTER_BREAK.LV}else if(E<47560){if(E<47476){if(E<47421){if(E<47420){if(47393<=E&&E<=47419)return f.CLUSTER_BREAK.LVT}else if(E===47420)return f.CLUSTER_BREAK.LV}else if(E<47448){if(47421<=E&&E<=47447)return f.CLUSTER_BREAK.LVT}else if(E<47449){if(E===47448)return f.CLUSTER_BREAK.LV}else if(47449<=E&&E<=47475)return f.CLUSTER_BREAK.LVT}else if(E<47505){if(E<47477){if(E===47476)return f.CLUSTER_BREAK.LV}else if(E<47504){if(47477<=E&&E<=47503)return f.CLUSTER_BREAK.LVT}else if(E===47504)return f.CLUSTER_BREAK.LV}else if(E<47532){if(47505<=E&&E<=47531)return f.CLUSTER_BREAK.LVT}else if(E<47533){if(E===47532)return f.CLUSTER_BREAK.LV}else if(47533<=E&&E<=47559)return f.CLUSTER_BREAK.LVT}else if(E<47617){if(E<47588){if(E<47561){if(E===47560)return f.CLUSTER_BREAK.LV}else if(47561<=E&&E<=47587)return f.CLUSTER_BREAK.LVT}else if(E<47589){if(E===47588)return f.CLUSTER_BREAK.LV}else if(E<47616){if(47589<=E&&E<=47615)return f.CLUSTER_BREAK.LVT}else if(E===47616)return f.CLUSTER_BREAK.LV}else if(E<47672){if(E<47644){if(47617<=E&&E<=47643)return f.CLUSTER_BREAK.LVT}else if(E<47645){if(E===47644)return f.CLUSTER_BREAK.LV}else if(47645<=E&&E<=47671)return f.CLUSTER_BREAK.LVT}else if(E<47673){if(E===47672)return f.CLUSTER_BREAK.LV}else if(E<47700){if(47673<=E&&E<=47699)return f.CLUSTER_BREAK.LVT}else if(E===47700)return f.CLUSTER_BREAK.LV}else if(E<48316){if(E<48008){if(E<47841){if(E<47784){if(E<47729){if(E<47728){if(47701<=E&&E<=47727)return f.CLUSTER_BREAK.LVT}else if(E===47728)return f.CLUSTER_BREAK.LV}else if(E<47756){if(47729<=E&&E<=47755)return f.CLUSTER_BREAK.LVT}else if(E<47757){if(E===47756)return f.CLUSTER_BREAK.LV}else if(47757<=E&&E<=47783)return f.CLUSTER_BREAK.LVT}else if(E<47812){if(E<47785){if(E===47784)return f.CLUSTER_BREAK.LV}else if(47785<=E&&E<=47811)return f.CLUSTER_BREAK.LVT}else if(E<47813){if(E===47812)return f.CLUSTER_BREAK.LV}else if(E<47840){if(47813<=E&&E<=47839)return f.CLUSTER_BREAK.LVT}else if(E===47840)return f.CLUSTER_BREAK.LV}else if(E<47924){if(E<47869){if(E<47868){if(47841<=E&&E<=47867)return f.CLUSTER_BREAK.LVT}else if(E===47868)return f.CLUSTER_BREAK.LV}else if(E<47896){if(47869<=E&&E<=47895)return f.CLUSTER_BREAK.LVT}else if(E<47897){if(E===47896)return f.CLUSTER_BREAK.LV}else if(47897<=E&&E<=47923)return f.CLUSTER_BREAK.LVT}else if(E<47953){if(E<47925){if(E===47924)return f.CLUSTER_BREAK.LV}else if(E<47952){if(47925<=E&&E<=47951)return f.CLUSTER_BREAK.LVT}else if(E===47952)return f.CLUSTER_BREAK.LV}else if(E<47980){if(47953<=E&&E<=47979)return f.CLUSTER_BREAK.LVT}else if(E<47981){if(E===47980)return f.CLUSTER_BREAK.LV}else if(47981<=E&&E<=48007)return f.CLUSTER_BREAK.LVT}else if(E<48149){if(E<48065){if(E<48036){if(E<48009){if(E===48008)return f.CLUSTER_BREAK.LV}else if(48009<=E&&E<=48035)return f.CLUSTER_BREAK.LVT}else if(E<48037){if(E===48036)return f.CLUSTER_BREAK.LV}else if(E<48064){if(48037<=E&&E<=48063)return f.CLUSTER_BREAK.LVT}else if(E===48064)return f.CLUSTER_BREAK.LV}else if(E<48120){if(E<48092){if(48065<=E&&E<=48091)return f.CLUSTER_BREAK.LVT}else if(E<48093){if(E===48092)return f.CLUSTER_BREAK.LV}else if(48093<=E&&E<=48119)return f.CLUSTER_BREAK.LVT}else if(E<48121){if(E===48120)return f.CLUSTER_BREAK.LV}else if(E<48148){if(48121<=E&&E<=48147)return f.CLUSTER_BREAK.LVT}else if(E===48148)return f.CLUSTER_BREAK.LV}else if(E<48232){if(E<48177){if(E<48176){if(48149<=E&&E<=48175)return f.CLUSTER_BREAK.LVT}else if(E===48176)return f.CLUSTER_BREAK.LV}else if(E<48204){if(48177<=E&&E<=48203)return f.CLUSTER_BREAK.LVT}else if(E<48205){if(E===48204)return f.CLUSTER_BREAK.LV}else if(48205<=E&&E<=48231)return f.CLUSTER_BREAK.LVT}else if(E<48261){if(E<48233){if(E===48232)return f.CLUSTER_BREAK.LV}else if(E<48260){if(48233<=E&&E<=48259)return f.CLUSTER_BREAK.LVT}else if(E===48260)return f.CLUSTER_BREAK.LV}else if(E<48288){if(48261<=E&&E<=48287)return f.CLUSTER_BREAK.LVT}else if(E<48289){if(E===48288)return f.CLUSTER_BREAK.LV}else if(48289<=E&&E<=48315)return f.CLUSTER_BREAK.LVT}else if(E<48597){if(E<48456){if(E<48373){if(E<48344){if(E<48317){if(E===48316)return f.CLUSTER_BREAK.LV}else if(48317<=E&&E<=48343)return f.CLUSTER_BREAK.LVT}else if(E<48345){if(E===48344)return f.CLUSTER_BREAK.LV}else if(E<48372){if(48345<=E&&E<=48371)return f.CLUSTER_BREAK.LVT}else if(E===48372)return f.CLUSTER_BREAK.LV}else if(E<48401){if(E<48400){if(48373<=E&&E<=48399)return f.CLUSTER_BREAK.LVT}else if(E===48400)return f.CLUSTER_BREAK.LV}else if(E<48428){if(48401<=E&&E<=48427)return f.CLUSTER_BREAK.LVT}else if(E<48429){if(E===48428)return f.CLUSTER_BREAK.LV}else if(48429<=E&&E<=48455)return f.CLUSTER_BREAK.LVT}else if(E<48513){if(E<48484){if(E<48457){if(E===48456)return f.CLUSTER_BREAK.LV}else if(48457<=E&&E<=48483)return f.CLUSTER_BREAK.LVT}else if(E<48485){if(E===48484)return f.CLUSTER_BREAK.LV}else if(E<48512){if(48485<=E&&E<=48511)return f.CLUSTER_BREAK.LVT}else if(E===48512)return f.CLUSTER_BREAK.LV}else if(E<48568){if(E<48540){if(48513<=E&&E<=48539)return f.CLUSTER_BREAK.LVT}else if(E<48541){if(E===48540)return f.CLUSTER_BREAK.LV}else if(48541<=E&&E<=48567)return f.CLUSTER_BREAK.LVT}else if(E<48569){if(E===48568)return f.CLUSTER_BREAK.LV}else if(E<48596){if(48569<=E&&E<=48595)return f.CLUSTER_BREAK.LVT}else if(E===48596)return f.CLUSTER_BREAK.LV}else if(E<48764){if(E<48680){if(E<48625){if(E<48624){if(48597<=E&&E<=48623)return f.CLUSTER_BREAK.LVT}else if(E===48624)return f.CLUSTER_BREAK.LV}else if(E<48652){if(48625<=E&&E<=48651)return f.CLUSTER_BREAK.LVT}else if(E<48653){if(E===48652)return f.CLUSTER_BREAK.LV}else if(48653<=E&&E<=48679)return f.CLUSTER_BREAK.LVT}else if(E<48709){if(E<48681){if(E===48680)return f.CLUSTER_BREAK.LV}else if(E<48708){if(48681<=E&&E<=48707)return f.CLUSTER_BREAK.LVT}else if(E===48708)return f.CLUSTER_BREAK.LV}else if(E<48736){if(48709<=E&&E<=48735)return f.CLUSTER_BREAK.LVT}else if(E<48737){if(E===48736)return f.CLUSTER_BREAK.LV}else if(48737<=E&&E<=48763)return f.CLUSTER_BREAK.LVT}else if(E<48821){if(E<48792){if(E<48765){if(E===48764)return f.CLUSTER_BREAK.LV}else if(48765<=E&&E<=48791)return f.CLUSTER_BREAK.LVT}else if(E<48793){if(E===48792)return f.CLUSTER_BREAK.LV}else if(E<48820){if(48793<=E&&E<=48819)return f.CLUSTER_BREAK.LVT}else if(E===48820)return f.CLUSTER_BREAK.LV}else if(E<48876){if(E<48848){if(48821<=E&&E<=48847)return f.CLUSTER_BREAK.LVT}else if(E<48849){if(E===48848)return f.CLUSTER_BREAK.LV}else if(48849<=E&&E<=48875)return f.CLUSTER_BREAK.LVT}else if(E<48877){if(E===48876)return f.CLUSTER_BREAK.LV}else if(E<48904){if(48877<=E&&E<=48903)return f.CLUSTER_BREAK.LVT}else if(E===48904)return f.CLUSTER_BREAK.LV}else if(E<53720){if(E<51312){if(E<50108){if(E<49493){if(E<49212){if(E<49045){if(E<48988){if(E<48933){if(E<48932){if(48905<=E&&E<=48931)return f.CLUSTER_BREAK.LVT}else if(E===48932)return f.CLUSTER_BREAK.LV}else if(E<48960){if(48933<=E&&E<=48959)return f.CLUSTER_BREAK.LVT}else if(E<48961){if(E===48960)return f.CLUSTER_BREAK.LV}else if(48961<=E&&E<=48987)return f.CLUSTER_BREAK.LVT}else if(E<49016){if(E<48989){if(E===48988)return f.CLUSTER_BREAK.LV}else if(48989<=E&&E<=49015)return f.CLUSTER_BREAK.LVT}else if(E<49017){if(E===49016)return f.CLUSTER_BREAK.LV}else if(E<49044){if(49017<=E&&E<=49043)return f.CLUSTER_BREAK.LVT}else if(E===49044)return f.CLUSTER_BREAK.LV}else if(E<49128){if(E<49073){if(E<49072){if(49045<=E&&E<=49071)return f.CLUSTER_BREAK.LVT}else if(E===49072)return f.CLUSTER_BREAK.LV}else if(E<49100){if(49073<=E&&E<=49099)return f.CLUSTER_BREAK.LVT}else if(E<49101){if(E===49100)return f.CLUSTER_BREAK.LV}else if(49101<=E&&E<=49127)return f.CLUSTER_BREAK.LVT}else if(E<49157){if(E<49129){if(E===49128)return f.CLUSTER_BREAK.LV}else if(E<49156){if(49129<=E&&E<=49155)return f.CLUSTER_BREAK.LVT}else if(E===49156)return f.CLUSTER_BREAK.LV}else if(E<49184){if(49157<=E&&E<=49183)return f.CLUSTER_BREAK.LVT}else if(E<49185){if(E===49184)return f.CLUSTER_BREAK.LV}else if(49185<=E&&E<=49211)return f.CLUSTER_BREAK.LVT}else if(E<49352){if(E<49269){if(E<49240){if(E<49213){if(E===49212)return f.CLUSTER_BREAK.LV}else if(49213<=E&&E<=49239)return f.CLUSTER_BREAK.LVT}else if(E<49241){if(E===49240)return f.CLUSTER_BREAK.LV}else if(E<49268){if(49241<=E&&E<=49267)return f.CLUSTER_BREAK.LVT}else if(E===49268)return f.CLUSTER_BREAK.LV}else if(E<49297){if(E<49296){if(49269<=E&&E<=49295)return f.CLUSTER_BREAK.LVT}else if(E===49296)return f.CLUSTER_BREAK.LV}else if(E<49324){if(49297<=E&&E<=49323)return f.CLUSTER_BREAK.LVT}else if(E<49325){if(E===49324)return f.CLUSTER_BREAK.LV}else if(49325<=E&&E<=49351)return f.CLUSTER_BREAK.LVT}else if(E<49409){if(E<49380){if(E<49353){if(E===49352)return f.CLUSTER_BREAK.LV}else if(49353<=E&&E<=49379)return f.CLUSTER_BREAK.LVT}else if(E<49381){if(E===49380)return f.CLUSTER_BREAK.LV}else if(E<49408){if(49381<=E&&E<=49407)return f.CLUSTER_BREAK.LVT}else if(E===49408)return f.CLUSTER_BREAK.LV}else if(E<49464){if(E<49436){if(49409<=E&&E<=49435)return f.CLUSTER_BREAK.LVT}else if(E<49437){if(E===49436)return f.CLUSTER_BREAK.LV}else if(49437<=E&&E<=49463)return f.CLUSTER_BREAK.LVT}else if(E<49465){if(E===49464)return f.CLUSTER_BREAK.LV}else if(E<49492){if(49465<=E&&E<=49491)return f.CLUSTER_BREAK.LVT}else if(E===49492)return f.CLUSTER_BREAK.LV}else if(E<49800){if(E<49633){if(E<49576){if(E<49521){if(E<49520){if(49493<=E&&E<=49519)return f.CLUSTER_BREAK.LVT}else if(E===49520)return f.CLUSTER_BREAK.LV}else if(E<49548){if(49521<=E&&E<=49547)return f.CLUSTER_BREAK.LVT}else if(E<49549){if(E===49548)return f.CLUSTER_BREAK.LV}else if(49549<=E&&E<=49575)return f.CLUSTER_BREAK.LVT}else if(E<49604){if(E<49577){if(E===49576)return f.CLUSTER_BREAK.LV}else if(49577<=E&&E<=49603)return f.CLUSTER_BREAK.LVT}else if(E<49605){if(E===49604)return f.CLUSTER_BREAK.LV}else if(E<49632){if(49605<=E&&E<=49631)return f.CLUSTER_BREAK.LVT}else if(E===49632)return f.CLUSTER_BREAK.LV}else if(E<49716){if(E<49661){if(E<49660){if(49633<=E&&E<=49659)return f.CLUSTER_BREAK.LVT}else if(E===49660)return f.CLUSTER_BREAK.LV}else if(E<49688){if(49661<=E&&E<=49687)return f.CLUSTER_BREAK.LVT}else if(E<49689){if(E===49688)return f.CLUSTER_BREAK.LV}else if(49689<=E&&E<=49715)return f.CLUSTER_BREAK.LVT}else if(E<49745){if(E<49717){if(E===49716)return f.CLUSTER_BREAK.LV}else if(E<49744){if(49717<=E&&E<=49743)return f.CLUSTER_BREAK.LVT}else if(E===49744)return f.CLUSTER_BREAK.LV}else if(E<49772){if(49745<=E&&E<=49771)return f.CLUSTER_BREAK.LVT}else if(E<49773){if(E===49772)return f.CLUSTER_BREAK.LV}else if(49773<=E&&E<=49799)return f.CLUSTER_BREAK.LVT}else if(E<49941){if(E<49857){if(E<49828){if(E<49801){if(E===49800)return f.CLUSTER_BREAK.LV}else if(49801<=E&&E<=49827)return f.CLUSTER_BREAK.LVT}else if(E<49829){if(E===49828)return f.CLUSTER_BREAK.LV}else if(E<49856){if(49829<=E&&E<=49855)return f.CLUSTER_BREAK.LVT}else if(E===49856)return f.CLUSTER_BREAK.LV}else if(E<49912){if(E<49884){if(49857<=E&&E<=49883)return f.CLUSTER_BREAK.LVT}else if(E<49885){if(E===49884)return f.CLUSTER_BREAK.LV}else if(49885<=E&&E<=49911)return f.CLUSTER_BREAK.LVT}else if(E<49913){if(E===49912)return f.CLUSTER_BREAK.LV}else if(E<49940){if(49913<=E&&E<=49939)return f.CLUSTER_BREAK.LVT}else if(E===49940)return f.CLUSTER_BREAK.LV}else if(E<50024){if(E<49969){if(E<49968){if(49941<=E&&E<=49967)return f.CLUSTER_BREAK.LVT}else if(E===49968)return f.CLUSTER_BREAK.LV}else if(E<49996){if(49969<=E&&E<=49995)return f.CLUSTER_BREAK.LVT}else if(E<49997){if(E===49996)return f.CLUSTER_BREAK.LV}else if(49997<=E&&E<=50023)return f.CLUSTER_BREAK.LVT}else if(E<50053){if(E<50025){if(E===50024)return f.CLUSTER_BREAK.LV}else if(E<50052){if(50025<=E&&E<=50051)return f.CLUSTER_BREAK.LVT}else if(E===50052)return f.CLUSTER_BREAK.LV}else if(E<50080){if(50053<=E&&E<=50079)return f.CLUSTER_BREAK.LVT}else if(E<50081){if(E===50080)return f.CLUSTER_BREAK.LV}else if(50081<=E&&E<=50107)return f.CLUSTER_BREAK.LVT}else if(E<50697){if(E<50389){if(E<50248){if(E<50165){if(E<50136){if(E<50109){if(E===50108)return f.CLUSTER_BREAK.LV}else if(50109<=E&&E<=50135)return f.CLUSTER_BREAK.LVT}else if(E<50137){if(E===50136)return f.CLUSTER_BREAK.LV}else if(E<50164){if(50137<=E&&E<=50163)return f.CLUSTER_BREAK.LVT}else if(E===50164)return f.CLUSTER_BREAK.LV}else if(E<50193){if(E<50192){if(50165<=E&&E<=50191)return f.CLUSTER_BREAK.LVT}else if(E===50192)return f.CLUSTER_BREAK.LV}else if(E<50220){if(50193<=E&&E<=50219)return f.CLUSTER_BREAK.LVT}else if(E<50221){if(E===50220)return f.CLUSTER_BREAK.LV}else if(50221<=E&&E<=50247)return f.CLUSTER_BREAK.LVT}else if(E<50305){if(E<50276){if(E<50249){if(E===50248)return f.CLUSTER_BREAK.LV}else if(50249<=E&&E<=50275)return f.CLUSTER_BREAK.LVT}else if(E<50277){if(E===50276)return f.CLUSTER_BREAK.LV}else if(E<50304){if(50277<=E&&E<=50303)return f.CLUSTER_BREAK.LVT}else if(E===50304)return f.CLUSTER_BREAK.LV}else if(E<50360){if(E<50332){if(50305<=E&&E<=50331)return f.CLUSTER_BREAK.LVT}else if(E<50333){if(E===50332)return f.CLUSTER_BREAK.LV}else if(50333<=E&&E<=50359)return f.CLUSTER_BREAK.LVT}else if(E<50361){if(E===50360)return f.CLUSTER_BREAK.LV}else if(E<50388){if(50361<=E&&E<=50387)return f.CLUSTER_BREAK.LVT}else if(E===50388)return f.CLUSTER_BREAK.LV}else if(E<50556){if(E<50472){if(E<50417){if(E<50416){if(50389<=E&&E<=50415)return f.CLUSTER_BREAK.LVT}else if(E===50416)return f.CLUSTER_BREAK.LV}else if(E<50444){if(50417<=E&&E<=50443)return f.CLUSTER_BREAK.LVT}else if(E<50445){if(E===50444)return f.CLUSTER_BREAK.LV}else if(50445<=E&&E<=50471)return f.CLUSTER_BREAK.LVT}else if(E<50501){if(E<50473){if(E===50472)return f.CLUSTER_BREAK.LV}else if(E<50500){if(50473<=E&&E<=50499)return f.CLUSTER_BREAK.LVT}else if(E===50500)return f.CLUSTER_BREAK.LV}else if(E<50528){if(50501<=E&&E<=50527)return f.CLUSTER_BREAK.LVT}else if(E<50529){if(E===50528)return f.CLUSTER_BREAK.LV}else if(50529<=E&&E<=50555)return f.CLUSTER_BREAK.LVT}else if(E<50613){if(E<50584){if(E<50557){if(E===50556)return f.CLUSTER_BREAK.LV}else if(50557<=E&&E<=50583)return f.CLUSTER_BREAK.LVT}else if(E<50585){if(E===50584)return f.CLUSTER_BREAK.LV}else if(E<50612){if(50585<=E&&E<=50611)return f.CLUSTER_BREAK.LVT}else if(E===50612)return f.CLUSTER_BREAK.LV}else if(E<50668){if(E<50640){if(50613<=E&&E<=50639)return f.CLUSTER_BREAK.LVT}else if(E<50641){if(E===50640)return f.CLUSTER_BREAK.LV}else if(50641<=E&&E<=50667)return f.CLUSTER_BREAK.LVT}else if(E<50669){if(E===50668)return f.CLUSTER_BREAK.LV}else if(E<50696){if(50669<=E&&E<=50695)return f.CLUSTER_BREAK.LVT}else if(E===50696)return f.CLUSTER_BREAK.LV}else if(E<51004){if(E<50837){if(E<50780){if(E<50725){if(E<50724){if(50697<=E&&E<=50723)return f.CLUSTER_BREAK.LVT}else if(E===50724)return f.CLUSTER_BREAK.LV}else if(E<50752){if(50725<=E&&E<=50751)return f.CLUSTER_BREAK.LVT}else if(E<50753){if(E===50752)return f.CLUSTER_BREAK.LV}else if(50753<=E&&E<=50779)return f.CLUSTER_BREAK.LVT}else if(E<50808){if(E<50781){if(E===50780)return f.CLUSTER_BREAK.LV}else if(50781<=E&&E<=50807)return f.CLUSTER_BREAK.LVT}else if(E<50809){if(E===50808)return f.CLUSTER_BREAK.LV}else if(E<50836){if(50809<=E&&E<=50835)return f.CLUSTER_BREAK.LVT}else if(E===50836)return f.CLUSTER_BREAK.LV}else if(E<50920){if(E<50865){if(E<50864){if(50837<=E&&E<=50863)return f.CLUSTER_BREAK.LVT}else if(E===50864)return f.CLUSTER_BREAK.LV}else if(E<50892){if(50865<=E&&E<=50891)return f.CLUSTER_BREAK.LVT}else if(E<50893){if(E===50892)return f.CLUSTER_BREAK.LV}else if(50893<=E&&E<=50919)return f.CLUSTER_BREAK.LVT}else if(E<50949){if(E<50921){if(E===50920)return f.CLUSTER_BREAK.LV}else if(E<50948){if(50921<=E&&E<=50947)return f.CLUSTER_BREAK.LVT}else if(E===50948)return f.CLUSTER_BREAK.LV}else if(E<50976){if(50949<=E&&E<=50975)return f.CLUSTER_BREAK.LVT}else if(E<50977){if(E===50976)return f.CLUSTER_BREAK.LV}else if(50977<=E&&E<=51003)return f.CLUSTER_BREAK.LVT}else if(E<51145){if(E<51061){if(E<51032){if(E<51005){if(E===51004)return f.CLUSTER_BREAK.LV}else if(51005<=E&&E<=51031)return f.CLUSTER_BREAK.LVT}else if(E<51033){if(E===51032)return f.CLUSTER_BREAK.LV}else if(E<51060){if(51033<=E&&E<=51059)return f.CLUSTER_BREAK.LVT}else if(E===51060)return f.CLUSTER_BREAK.LV}else if(E<51116){if(E<51088){if(51061<=E&&E<=51087)return f.CLUSTER_BREAK.LVT}else if(E<51089){if(E===51088)return f.CLUSTER_BREAK.LV}else if(51089<=E&&E<=51115)return f.CLUSTER_BREAK.LVT}else if(E<51117){if(E===51116)return f.CLUSTER_BREAK.LV}else if(E<51144){if(51117<=E&&E<=51143)return f.CLUSTER_BREAK.LVT}else if(E===51144)return f.CLUSTER_BREAK.LV}else if(E<51228){if(E<51173){if(E<51172){if(51145<=E&&E<=51171)return f.CLUSTER_BREAK.LVT}else if(E===51172)return f.CLUSTER_BREAK.LV}else if(E<51200){if(51173<=E&&E<=51199)return f.CLUSTER_BREAK.LVT}else if(E<51201){if(E===51200)return f.CLUSTER_BREAK.LV}else if(51201<=E&&E<=51227)return f.CLUSTER_BREAK.LVT}else if(E<51257){if(E<51229){if(E===51228)return f.CLUSTER_BREAK.LV}else if(E<51256){if(51229<=E&&E<=51255)return f.CLUSTER_BREAK.LVT}else if(E===51256)return f.CLUSTER_BREAK.LV}else if(E<51284){if(51257<=E&&E<=51283)return f.CLUSTER_BREAK.LVT}else if(E<51285){if(E===51284)return f.CLUSTER_BREAK.LV}else if(51285<=E&&E<=51311)return f.CLUSTER_BREAK.LVT}else if(E<52516){if(E<51901){if(E<51593){if(E<51452){if(E<51369){if(E<51340){if(E<51313){if(E===51312)return f.CLUSTER_BREAK.LV}else if(51313<=E&&E<=51339)return f.CLUSTER_BREAK.LVT}else if(E<51341){if(E===51340)return f.CLUSTER_BREAK.LV}else if(E<51368){if(51341<=E&&E<=51367)return f.CLUSTER_BREAK.LVT}else if(E===51368)return f.CLUSTER_BREAK.LV}else if(E<51397){if(E<51396){if(51369<=E&&E<=51395)return f.CLUSTER_BREAK.LVT}else if(E===51396)return f.CLUSTER_BREAK.LV}else if(E<51424){if(51397<=E&&E<=51423)return f.CLUSTER_BREAK.LVT}else if(E<51425){if(E===51424)return f.CLUSTER_BREAK.LV}else if(51425<=E&&E<=51451)return f.CLUSTER_BREAK.LVT}else if(E<51509){if(E<51480){if(E<51453){if(E===51452)return f.CLUSTER_BREAK.LV}else if(51453<=E&&E<=51479)return f.CLUSTER_BREAK.LVT}else if(E<51481){if(E===51480)return f.CLUSTER_BREAK.LV}else if(E<51508){if(51481<=E&&E<=51507)return f.CLUSTER_BREAK.LVT}else if(E===51508)return f.CLUSTER_BREAK.LV}else if(E<51564){if(E<51536){if(51509<=E&&E<=51535)return f.CLUSTER_BREAK.LVT}else if(E<51537){if(E===51536)return f.CLUSTER_BREAK.LV}else if(51537<=E&&E<=51563)return f.CLUSTER_BREAK.LVT}else if(E<51565){if(E===51564)return f.CLUSTER_BREAK.LV}else if(E<51592){if(51565<=E&&E<=51591)return f.CLUSTER_BREAK.LVT}else if(E===51592)return f.CLUSTER_BREAK.LV}else if(E<51760){if(E<51676){if(E<51621){if(E<51620){if(51593<=E&&E<=51619)return f.CLUSTER_BREAK.LVT}else if(E===51620)return f.CLUSTER_BREAK.LV}else if(E<51648){if(51621<=E&&E<=51647)return f.CLUSTER_BREAK.LVT}else if(E<51649){if(E===51648)return f.CLUSTER_BREAK.LV}else if(51649<=E&&E<=51675)return f.CLUSTER_BREAK.LVT}else if(E<51705){if(E<51677){if(E===51676)return f.CLUSTER_BREAK.LV}else if(E<51704){if(51677<=E&&E<=51703)return f.CLUSTER_BREAK.LVT}else if(E===51704)return f.CLUSTER_BREAK.LV}else if(E<51732){if(51705<=E&&E<=51731)return f.CLUSTER_BREAK.LVT}else if(E<51733){if(E===51732)return f.CLUSTER_BREAK.LV}else if(51733<=E&&E<=51759)return f.CLUSTER_BREAK.LVT}else if(E<51817){if(E<51788){if(E<51761){if(E===51760)return f.CLUSTER_BREAK.LV}else if(51761<=E&&E<=51787)return f.CLUSTER_BREAK.LVT}else if(E<51789){if(E===51788)return f.CLUSTER_BREAK.LV}else if(E<51816){if(51789<=E&&E<=51815)return f.CLUSTER_BREAK.LVT}else if(E===51816)return f.CLUSTER_BREAK.LV}else if(E<51872){if(E<51844){if(51817<=E&&E<=51843)return f.CLUSTER_BREAK.LVT}else if(E<51845){if(E===51844)return f.CLUSTER_BREAK.LV}else if(51845<=E&&E<=51871)return f.CLUSTER_BREAK.LVT}else if(E<51873){if(E===51872)return f.CLUSTER_BREAK.LV}else if(E<51900){if(51873<=E&&E<=51899)return f.CLUSTER_BREAK.LVT}else if(E===51900)return f.CLUSTER_BREAK.LV}else if(E<52208){if(E<52041){if(E<51984){if(E<51929){if(E<51928){if(51901<=E&&E<=51927)return f.CLUSTER_BREAK.LVT}else if(E===51928)return f.CLUSTER_BREAK.LV}else if(E<51956){if(51929<=E&&E<=51955)return f.CLUSTER_BREAK.LVT}else if(E<51957){if(E===51956)return f.CLUSTER_BREAK.LV}else if(51957<=E&&E<=51983)return f.CLUSTER_BREAK.LVT}else if(E<52012){if(E<51985){if(E===51984)return f.CLUSTER_BREAK.LV}else if(51985<=E&&E<=52011)return f.CLUSTER_BREAK.LVT}else if(E<52013){if(E===52012)return f.CLUSTER_BREAK.LV}else if(E<52040){if(52013<=E&&E<=52039)return f.CLUSTER_BREAK.LVT}else if(E===52040)return f.CLUSTER_BREAK.LV}else if(E<52124){if(E<52069){if(E<52068){if(52041<=E&&E<=52067)return f.CLUSTER_BREAK.LVT}else if(E===52068)return f.CLUSTER_BREAK.LV}else if(E<52096){if(52069<=E&&E<=52095)return f.CLUSTER_BREAK.LVT}else if(E<52097){if(E===52096)return f.CLUSTER_BREAK.LV}else if(52097<=E&&E<=52123)return f.CLUSTER_BREAK.LVT}else if(E<52153){if(E<52125){if(E===52124)return f.CLUSTER_BREAK.LV}else if(E<52152){if(52125<=E&&E<=52151)return f.CLUSTER_BREAK.LVT}else if(E===52152)return f.CLUSTER_BREAK.LV}else if(E<52180){if(52153<=E&&E<=52179)return f.CLUSTER_BREAK.LVT}else if(E<52181){if(E===52180)return f.CLUSTER_BREAK.LV}else if(52181<=E&&E<=52207)return f.CLUSTER_BREAK.LVT}else if(E<52349){if(E<52265){if(E<52236){if(E<52209){if(E===52208)return f.CLUSTER_BREAK.LV}else if(52209<=E&&E<=52235)return f.CLUSTER_BREAK.LVT}else if(E<52237){if(E===52236)return f.CLUSTER_BREAK.LV}else if(E<52264){if(52237<=E&&E<=52263)return f.CLUSTER_BREAK.LVT}else if(E===52264)return f.CLUSTER_BREAK.LV}else if(E<52320){if(E<52292){if(52265<=E&&E<=52291)return f.CLUSTER_BREAK.LVT}else if(E<52293){if(E===52292)return f.CLUSTER_BREAK.LV}else if(52293<=E&&E<=52319)return f.CLUSTER_BREAK.LVT}else if(E<52321){if(E===52320)return f.CLUSTER_BREAK.LV}else if(E<52348){if(52321<=E&&E<=52347)return f.CLUSTER_BREAK.LVT}else if(E===52348)return f.CLUSTER_BREAK.LV}else if(E<52432){if(E<52377){if(E<52376){if(52349<=E&&E<=52375)return f.CLUSTER_BREAK.LVT}else if(E===52376)return f.CLUSTER_BREAK.LV}else if(E<52404){if(52377<=E&&E<=52403)return f.CLUSTER_BREAK.LVT}else if(E<52405){if(E===52404)return f.CLUSTER_BREAK.LV}else if(52405<=E&&E<=52431)return f.CLUSTER_BREAK.LVT}else if(E<52461){if(E<52433){if(E===52432)return f.CLUSTER_BREAK.LV}else if(E<52460){if(52433<=E&&E<=52459)return f.CLUSTER_BREAK.LVT}else if(E===52460)return f.CLUSTER_BREAK.LV}else if(E<52488){if(52461<=E&&E<=52487)return f.CLUSTER_BREAK.LVT}else if(E<52489){if(E===52488)return f.CLUSTER_BREAK.LV}else if(52489<=E&&E<=52515)return f.CLUSTER_BREAK.LVT}else if(E<53105){if(E<52797){if(E<52656){if(E<52573){if(E<52544){if(E<52517){if(E===52516)return f.CLUSTER_BREAK.LV}else if(52517<=E&&E<=52543)return f.CLUSTER_BREAK.LVT}else if(E<52545){if(E===52544)return f.CLUSTER_BREAK.LV}else if(E<52572){if(52545<=E&&E<=52571)return f.CLUSTER_BREAK.LVT}else if(E===52572)return f.CLUSTER_BREAK.LV}else if(E<52601){if(E<52600){if(52573<=E&&E<=52599)return f.CLUSTER_BREAK.LVT}else if(E===52600)return f.CLUSTER_BREAK.LV}else if(E<52628){if(52601<=E&&E<=52627)return f.CLUSTER_BREAK.LVT}else if(E<52629){if(E===52628)return f.CLUSTER_BREAK.LV}else if(52629<=E&&E<=52655)return f.CLUSTER_BREAK.LVT}else if(E<52713){if(E<52684){if(E<52657){if(E===52656)return f.CLUSTER_BREAK.LV}else if(52657<=E&&E<=52683)return f.CLUSTER_BREAK.LVT}else if(E<52685){if(E===52684)return f.CLUSTER_BREAK.LV}else if(E<52712){if(52685<=E&&E<=52711)return f.CLUSTER_BREAK.LVT}else if(E===52712)return f.CLUSTER_BREAK.LV}else if(E<52768){if(E<52740){if(52713<=E&&E<=52739)return f.CLUSTER_BREAK.LVT}else if(E<52741){if(E===52740)return f.CLUSTER_BREAK.LV}else if(52741<=E&&E<=52767)return f.CLUSTER_BREAK.LVT}else if(E<52769){if(E===52768)return f.CLUSTER_BREAK.LV}else if(E<52796){if(52769<=E&&E<=52795)return f.CLUSTER_BREAK.LVT}else if(E===52796)return f.CLUSTER_BREAK.LV}else if(E<52964){if(E<52880){if(E<52825){if(E<52824){if(52797<=E&&E<=52823)return f.CLUSTER_BREAK.LVT}else if(E===52824)return f.CLUSTER_BREAK.LV}else if(E<52852){if(52825<=E&&E<=52851)return f.CLUSTER_BREAK.LVT}else if(E<52853){if(E===52852)return f.CLUSTER_BREAK.LV}else if(52853<=E&&E<=52879)return f.CLUSTER_BREAK.LVT}else if(E<52909){if(E<52881){if(E===52880)return f.CLUSTER_BREAK.LV}else if(E<52908){if(52881<=E&&E<=52907)return f.CLUSTER_BREAK.LVT}else if(E===52908)return f.CLUSTER_BREAK.LV}else if(E<52936){if(52909<=E&&E<=52935)return f.CLUSTER_BREAK.LVT}else if(E<52937){if(E===52936)return f.CLUSTER_BREAK.LV}else if(52937<=E&&E<=52963)return f.CLUSTER_BREAK.LVT}else if(E<53021){if(E<52992){if(E<52965){if(E===52964)return f.CLUSTER_BREAK.LV}else if(52965<=E&&E<=52991)return f.CLUSTER_BREAK.LVT}else if(E<52993){if(E===52992)return f.CLUSTER_BREAK.LV}else if(E<53020){if(52993<=E&&E<=53019)return f.CLUSTER_BREAK.LVT}else if(E===53020)return f.CLUSTER_BREAK.LV}else if(E<53076){if(E<53048){if(53021<=E&&E<=53047)return f.CLUSTER_BREAK.LVT}else if(E<53049){if(E===53048)return f.CLUSTER_BREAK.LV}else if(53049<=E&&E<=53075)return f.CLUSTER_BREAK.LVT}else if(E<53077){if(E===53076)return f.CLUSTER_BREAK.LV}else if(E<53104){if(53077<=E&&E<=53103)return f.CLUSTER_BREAK.LVT}else if(E===53104)return f.CLUSTER_BREAK.LV}else if(E<53412){if(E<53245){if(E<53188){if(E<53133){if(E<53132){if(53105<=E&&E<=53131)return f.CLUSTER_BREAK.LVT}else if(E===53132)return f.CLUSTER_BREAK.LV}else if(E<53160){if(53133<=E&&E<=53159)return f.CLUSTER_BREAK.LVT}else if(E<53161){if(E===53160)return f.CLUSTER_BREAK.LV}else if(53161<=E&&E<=53187)return f.CLUSTER_BREAK.LVT}else if(E<53216){if(E<53189){if(E===53188)return f.CLUSTER_BREAK.LV}else if(53189<=E&&E<=53215)return f.CLUSTER_BREAK.LVT}else if(E<53217){if(E===53216)return f.CLUSTER_BREAK.LV}else if(E<53244){if(53217<=E&&E<=53243)return f.CLUSTER_BREAK.LVT}else if(E===53244)return f.CLUSTER_BREAK.LV}else if(E<53328){if(E<53273){if(E<53272){if(53245<=E&&E<=53271)return f.CLUSTER_BREAK.LVT}else if(E===53272)return f.CLUSTER_BREAK.LV}else if(E<53300){if(53273<=E&&E<=53299)return f.CLUSTER_BREAK.LVT}else if(E<53301){if(E===53300)return f.CLUSTER_BREAK.LV}else if(53301<=E&&E<=53327)return f.CLUSTER_BREAK.LVT}else if(E<53357){if(E<53329){if(E===53328)return f.CLUSTER_BREAK.LV}else if(E<53356){if(53329<=E&&E<=53355)return f.CLUSTER_BREAK.LVT}else if(E===53356)return f.CLUSTER_BREAK.LV}else if(E<53384){if(53357<=E&&E<=53383)return f.CLUSTER_BREAK.LVT}else if(E<53385){if(E===53384)return f.CLUSTER_BREAK.LV}else if(53385<=E&&E<=53411)return f.CLUSTER_BREAK.LVT}else if(E<53553){if(E<53469){if(E<53440){if(E<53413){if(E===53412)return f.CLUSTER_BREAK.LV}else if(53413<=E&&E<=53439)return f.CLUSTER_BREAK.LVT}else if(E<53441){if(E===53440)return f.CLUSTER_BREAK.LV}else if(E<53468){if(53441<=E&&E<=53467)return f.CLUSTER_BREAK.LVT}else if(E===53468)return f.CLUSTER_BREAK.LV}else if(E<53524){if(E<53496){if(53469<=E&&E<=53495)return f.CLUSTER_BREAK.LVT}else if(E<53497){if(E===53496)return f.CLUSTER_BREAK.LV}else if(53497<=E&&E<=53523)return f.CLUSTER_BREAK.LVT}else if(E<53525){if(E===53524)return f.CLUSTER_BREAK.LV}else if(E<53552){if(53525<=E&&E<=53551)return f.CLUSTER_BREAK.LVT}else if(E===53552)return f.CLUSTER_BREAK.LV}else if(E<53636){if(E<53581){if(E<53580){if(53553<=E&&E<=53579)return f.CLUSTER_BREAK.LVT}else if(E===53580)return f.CLUSTER_BREAK.LV}else if(E<53608){if(53581<=E&&E<=53607)return f.CLUSTER_BREAK.LVT}else if(E<53609){if(E===53608)return f.CLUSTER_BREAK.LV}else if(53609<=E&&E<=53635)return f.CLUSTER_BREAK.LVT}else if(E<53665){if(E<53637){if(E===53636)return f.CLUSTER_BREAK.LV}else if(E<53664){if(53637<=E&&E<=53663)return f.CLUSTER_BREAK.LVT}else if(E===53664)return f.CLUSTER_BREAK.LV}else if(E<53692){if(53665<=E&&E<=53691)return f.CLUSTER_BREAK.LVT}else if(E<53693){if(E===53692)return f.CLUSTER_BREAK.LV}else if(53693<=E&&E<=53719)return f.CLUSTER_BREAK.LVT}else if(E<70459){if(E<54897){if(E<54308){if(E<54001){if(E<53860){if(E<53777){if(E<53748){if(E<53721){if(E===53720)return f.CLUSTER_BREAK.LV}else if(53721<=E&&E<=53747)return f.CLUSTER_BREAK.LVT}else if(E<53749){if(E===53748)return f.CLUSTER_BREAK.LV}else if(E<53776){if(53749<=E&&E<=53775)return f.CLUSTER_BREAK.LVT}else if(E===53776)return f.CLUSTER_BREAK.LV}else if(E<53805){if(E<53804){if(53777<=E&&E<=53803)return f.CLUSTER_BREAK.LVT}else if(E===53804)return f.CLUSTER_BREAK.LV}else if(E<53832){if(53805<=E&&E<=53831)return f.CLUSTER_BREAK.LVT}else if(E<53833){if(E===53832)return f.CLUSTER_BREAK.LV}else if(53833<=E&&E<=53859)return f.CLUSTER_BREAK.LVT}else if(E<53917){if(E<53888){if(E<53861){if(E===53860)return f.CLUSTER_BREAK.LV}else if(53861<=E&&E<=53887)return f.CLUSTER_BREAK.LVT}else if(E<53889){if(E===53888)return f.CLUSTER_BREAK.LV}else if(E<53916){if(53889<=E&&E<=53915)return f.CLUSTER_BREAK.LVT}else if(E===53916)return f.CLUSTER_BREAK.LV}else if(E<53972){if(E<53944){if(53917<=E&&E<=53943)return f.CLUSTER_BREAK.LVT}else if(E<53945){if(E===53944)return f.CLUSTER_BREAK.LV}else if(53945<=E&&E<=53971)return f.CLUSTER_BREAK.LVT}else if(E<53973){if(E===53972)return f.CLUSTER_BREAK.LV}else if(E<54e3){if(53973<=E&&E<=53999)return f.CLUSTER_BREAK.LVT}else if(E===54e3)return f.CLUSTER_BREAK.LV}else if(E<54141){if(E<54084){if(E<54029){if(E<54028){if(54001<=E&&E<=54027)return f.CLUSTER_BREAK.LVT}else if(E===54028)return f.CLUSTER_BREAK.LV}else if(E<54056){if(54029<=E&&E<=54055)return f.CLUSTER_BREAK.LVT}else if(E<54057){if(E===54056)return f.CLUSTER_BREAK.LV}else if(54057<=E&&E<=54083)return f.CLUSTER_BREAK.LVT}else if(E<54112){if(E<54085){if(E===54084)return f.CLUSTER_BREAK.LV}else if(54085<=E&&E<=54111)return f.CLUSTER_BREAK.LVT}else if(E<54113){if(E===54112)return f.CLUSTER_BREAK.LV}else if(E<54140){if(54113<=E&&E<=54139)return f.CLUSTER_BREAK.LVT}else if(E===54140)return f.CLUSTER_BREAK.LV}else if(E<54224){if(E<54169){if(E<54168){if(54141<=E&&E<=54167)return f.CLUSTER_BREAK.LVT}else if(E===54168)return f.CLUSTER_BREAK.LV}else if(E<54196){if(54169<=E&&E<=54195)return f.CLUSTER_BREAK.LVT}else if(E<54197){if(E===54196)return f.CLUSTER_BREAK.LV}else if(54197<=E&&E<=54223)return f.CLUSTER_BREAK.LVT}else if(E<54253){if(E<54225){if(E===54224)return f.CLUSTER_BREAK.LV}else if(E<54252){if(54225<=E&&E<=54251)return f.CLUSTER_BREAK.LVT}else if(E===54252)return f.CLUSTER_BREAK.LV}else if(E<54280){if(54253<=E&&E<=54279)return f.CLUSTER_BREAK.LVT}else if(E<54281){if(E===54280)return f.CLUSTER_BREAK.LV}else if(54281<=E&&E<=54307)return f.CLUSTER_BREAK.LVT}else if(E<54589){if(E<54448){if(E<54365){if(E<54336){if(E<54309){if(E===54308)return f.CLUSTER_BREAK.LV}else if(54309<=E&&E<=54335)return f.CLUSTER_BREAK.LVT}else if(E<54337){if(E===54336)return f.CLUSTER_BREAK.LV}else if(E<54364){if(54337<=E&&E<=54363)return f.CLUSTER_BREAK.LVT}else if(E===54364)return f.CLUSTER_BREAK.LV}else if(E<54393){if(E<54392){if(54365<=E&&E<=54391)return f.CLUSTER_BREAK.LVT}else if(E===54392)return f.CLUSTER_BREAK.LV}else if(E<54420){if(54393<=E&&E<=54419)return f.CLUSTER_BREAK.LVT}else if(E<54421){if(E===54420)return f.CLUSTER_BREAK.LV}else if(54421<=E&&E<=54447)return f.CLUSTER_BREAK.LVT}else if(E<54505){if(E<54476){if(E<54449){if(E===54448)return f.CLUSTER_BREAK.LV}else if(54449<=E&&E<=54475)return f.CLUSTER_BREAK.LVT}else if(E<54477){if(E===54476)return f.CLUSTER_BREAK.LV}else if(E<54504){if(54477<=E&&E<=54503)return f.CLUSTER_BREAK.LVT}else if(E===54504)return f.CLUSTER_BREAK.LV}else if(E<54560){if(E<54532){if(54505<=E&&E<=54531)return f.CLUSTER_BREAK.LVT}else if(E<54533){if(E===54532)return f.CLUSTER_BREAK.LV}else if(54533<=E&&E<=54559)return f.CLUSTER_BREAK.LVT}else if(E<54561){if(E===54560)return f.CLUSTER_BREAK.LV}else if(E<54588){if(54561<=E&&E<=54587)return f.CLUSTER_BREAK.LVT}else if(E===54588)return f.CLUSTER_BREAK.LV}else if(E<54756){if(E<54672){if(E<54617){if(E<54616){if(54589<=E&&E<=54615)return f.CLUSTER_BREAK.LVT}else if(E===54616)return f.CLUSTER_BREAK.LV}else if(E<54644){if(54617<=E&&E<=54643)return f.CLUSTER_BREAK.LVT}else if(E<54645){if(E===54644)return f.CLUSTER_BREAK.LV}else if(54645<=E&&E<=54671)return f.CLUSTER_BREAK.LVT}else if(E<54701){if(E<54673){if(E===54672)return f.CLUSTER_BREAK.LV}else if(E<54700){if(54673<=E&&E<=54699)return f.CLUSTER_BREAK.LVT}else if(E===54700)return f.CLUSTER_BREAK.LV}else if(E<54728){if(54701<=E&&E<=54727)return f.CLUSTER_BREAK.LVT}else if(E<54729){if(E===54728)return f.CLUSTER_BREAK.LV}else if(54729<=E&&E<=54755)return f.CLUSTER_BREAK.LVT}else if(E<54813){if(E<54784){if(E<54757){if(E===54756)return f.CLUSTER_BREAK.LV}else if(54757<=E&&E<=54783)return f.CLUSTER_BREAK.LVT}else if(E<54785){if(E===54784)return f.CLUSTER_BREAK.LV}else if(E<54812){if(54785<=E&&E<=54811)return f.CLUSTER_BREAK.LVT}else if(E===54812)return f.CLUSTER_BREAK.LV}else if(E<54868){if(E<54840){if(54813<=E&&E<=54839)return f.CLUSTER_BREAK.LVT}else if(E<54841){if(E===54840)return f.CLUSTER_BREAK.LV}else if(54841<=E&&E<=54867)return f.CLUSTER_BREAK.LVT}else if(E<54869){if(E===54868)return f.CLUSTER_BREAK.LV}else if(E<54896){if(54869<=E&&E<=54895)return f.CLUSTER_BREAK.LVT}else if(E===54896)return f.CLUSTER_BREAK.LV}else if(E<69632){if(E<55216){if(E<55037){if(E<54980){if(E<54925){if(E<54924){if(54897<=E&&E<=54923)return f.CLUSTER_BREAK.LVT}else if(E===54924)return f.CLUSTER_BREAK.LV}else if(E<54952){if(54925<=E&&E<=54951)return f.CLUSTER_BREAK.LVT}else if(E<54953){if(E===54952)return f.CLUSTER_BREAK.LV}else if(54953<=E&&E<=54979)return f.CLUSTER_BREAK.LVT}else if(E<55008){if(E<54981){if(E===54980)return f.CLUSTER_BREAK.LV}else if(54981<=E&&E<=55007)return f.CLUSTER_BREAK.LVT}else if(E<55009){if(E===55008)return f.CLUSTER_BREAK.LV}else if(E<55036){if(55009<=E&&E<=55035)return f.CLUSTER_BREAK.LVT}else if(E===55036)return f.CLUSTER_BREAK.LV}else if(E<55120){if(E<55065){if(E<55064){if(55037<=E&&E<=55063)return f.CLUSTER_BREAK.LVT}else if(E===55064)return f.CLUSTER_BREAK.LV}else if(E<55092){if(55065<=E&&E<=55091)return f.CLUSTER_BREAK.LVT}else if(E<55093){if(E===55092)return f.CLUSTER_BREAK.LV}else if(55093<=E&&E<=55119)return f.CLUSTER_BREAK.LVT}else if(E<55149){if(E<55121){if(E===55120)return f.CLUSTER_BREAK.LV}else if(E<55148){if(55121<=E&&E<=55147)return f.CLUSTER_BREAK.LVT}else if(E===55148)return f.CLUSTER_BREAK.LV}else if(E<55176){if(55149<=E&&E<=55175)return f.CLUSTER_BREAK.LVT}else if(E<55177){if(E===55176)return f.CLUSTER_BREAK.LV}else if(55177<=E&&E<=55203)return f.CLUSTER_BREAK.LVT}else if(E<68097){if(E<65279){if(E<64286){if(E<55243){if(55216<=E&&E<=55238)return f.CLUSTER_BREAK.V}else if(55243<=E&&E<=55291)return f.CLUSTER_BREAK.T}else if(E<65024){if(E===64286)return f.CLUSTER_BREAK.EXTEND}else if(E<65056){if(65024<=E&&E<=65039)return f.CLUSTER_BREAK.EXTEND}else if(65056<=E&&E<=65071)return f.CLUSTER_BREAK.EXTEND}else if(E<66045){if(E<65438){if(E===65279)return f.CLUSTER_BREAK.CONTROL}else if(E<65520){if(65438<=E&&E<=65439)return f.CLUSTER_BREAK.EXTEND}else if(65520<=E&&E<=65531)return f.CLUSTER_BREAK.CONTROL}else if(E<66272){if(E===66045)return f.CLUSTER_BREAK.EXTEND}else if(E<66422){if(E===66272)return f.CLUSTER_BREAK.EXTEND}else if(66422<=E&&E<=66426)return f.CLUSTER_BREAK.EXTEND}else if(E<68325){if(E<68108){if(E<68101){if(68097<=E&&E<=68099)return f.CLUSTER_BREAK.EXTEND}else if(68101<=E&&E<=68102)return f.CLUSTER_BREAK.EXTEND}else if(E<68152){if(68108<=E&&E<=68111)return f.CLUSTER_BREAK.EXTEND}else if(E<68159){if(68152<=E&&E<=68154)return f.CLUSTER_BREAK.EXTEND}else if(E===68159)return f.CLUSTER_BREAK.EXTEND}else if(E<69373){if(E<68900){if(68325<=E&&E<=68326)return f.CLUSTER_BREAK.EXTEND}else if(E<69291){if(68900<=E&&E<=68903)return f.CLUSTER_BREAK.EXTEND}else if(69291<=E&&E<=69292)return f.CLUSTER_BREAK.EXTEND}else if(E<69446){if(69373<=E&&E<=69375)return f.CLUSTER_BREAK.EXTEND}else if(E<69506){if(69446<=E&&E<=69456)return f.CLUSTER_BREAK.EXTEND}else if(69506<=E&&E<=69509)return f.CLUSTER_BREAK.EXTEND}else if(E<70016){if(E<69815){if(E<69747){if(E<69634){if(E===69632)return f.CLUSTER_BREAK.SPACINGMARK;if(E===69633)return f.CLUSTER_BREAK.EXTEND}else if(E<69688){if(E===69634)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<69744){if(69688<=E&&E<=69702)return f.CLUSTER_BREAK.EXTEND}else if(E===69744)return f.CLUSTER_BREAK.EXTEND}else if(E<69762){if(E<69759){if(69747<=E&&E<=69748)return f.CLUSTER_BREAK.EXTEND}else if(69759<=E&&E<=69761)return f.CLUSTER_BREAK.EXTEND}else if(E<69808){if(E===69762)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<69811){if(69808<=E&&E<=69810)return f.CLUSTER_BREAK.SPACINGMARK}else if(69811<=E&&E<=69814)return f.CLUSTER_BREAK.EXTEND}else if(E<69888)if(E<69821){if(E<69817){if(69815<=E&&E<=69816)return f.CLUSTER_BREAK.SPACINGMARK}else if(69817<=E&&E<=69818)return f.CLUSTER_BREAK.EXTEND}else if(E<69826){if(E===69821)return f.CLUSTER_BREAK.PREPEND}else{if(E===69826)return f.CLUSTER_BREAK.EXTEND;if(E===69837)return f.CLUSTER_BREAK.PREPEND}else if(E<69933){if(E<69927){if(69888<=E&&E<=69890)return f.CLUSTER_BREAK.EXTEND}else if(E<69932){if(69927<=E&&E<=69931)return f.CLUSTER_BREAK.EXTEND}else if(E===69932)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<69957){if(69933<=E&&E<=69940)return f.CLUSTER_BREAK.EXTEND}else if(E<70003){if(69957<=E&&E<=69958)return f.CLUSTER_BREAK.SPACINGMARK}else if(E===70003)return f.CLUSTER_BREAK.EXTEND}else if(E<70194){if(E<70082){if(E<70067){if(E<70018){if(70016<=E&&E<=70017)return f.CLUSTER_BREAK.EXTEND}else if(E===70018)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<70070){if(70067<=E&&E<=70069)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<70079){if(70070<=E&&E<=70078)return f.CLUSTER_BREAK.EXTEND}else if(70079<=E&&E<=70080)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<70095){if(E<70089){if(70082<=E&&E<=70083)return f.CLUSTER_BREAK.PREPEND}else if(E<70094){if(70089<=E&&E<=70092)return f.CLUSTER_BREAK.EXTEND}else if(E===70094)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<70188){if(E===70095)return f.CLUSTER_BREAK.EXTEND}else if(E<70191){if(70188<=E&&E<=70190)return f.CLUSTER_BREAK.SPACINGMARK}else if(70191<=E&&E<=70193)return f.CLUSTER_BREAK.EXTEND}else if(E<70209){if(E<70197){if(E<70196){if(70194<=E&&E<=70195)return f.CLUSTER_BREAK.SPACINGMARK}else if(E===70196)return f.CLUSTER_BREAK.EXTEND}else if(E<70198){if(E===70197)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<70206){if(70198<=E&&E<=70199)return f.CLUSTER_BREAK.EXTEND}else if(E===70206)return f.CLUSTER_BREAK.EXTEND}else if(E<70371){if(E<70367){if(E===70209)return f.CLUSTER_BREAK.EXTEND}else if(E<70368){if(E===70367)return f.CLUSTER_BREAK.EXTEND}else if(70368<=E&&E<=70370)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<70400){if(70371<=E&&E<=70378)return f.CLUSTER_BREAK.EXTEND}else if(E<70402){if(70400<=E&&E<=70401)return f.CLUSTER_BREAK.EXTEND}else if(70402<=E&&E<=70403)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<72343){if(E<71339){if(E<70841){if(E<70512){if(E<70471){if(E<70463){if(E<70462){if(70459<=E&&E<=70460)return f.CLUSTER_BREAK.EXTEND}else if(E===70462)return f.CLUSTER_BREAK.EXTEND}else if(E<70464){if(E===70463)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<70465){if(E===70464)return f.CLUSTER_BREAK.EXTEND}else if(70465<=E&&E<=70468)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<70487){if(E<70475){if(70471<=E&&E<=70472)return f.CLUSTER_BREAK.SPACINGMARK}else if(70475<=E&&E<=70477)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<70498){if(E===70487)return f.CLUSTER_BREAK.EXTEND}else if(E<70502){if(70498<=E&&E<=70499)return f.CLUSTER_BREAK.SPACINGMARK}else if(70502<=E&&E<=70508)return f.CLUSTER_BREAK.EXTEND}else if(E<70725){if(E<70712){if(E<70709){if(70512<=E&&E<=70516)return f.CLUSTER_BREAK.EXTEND}else if(70709<=E&&E<=70711)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<70720){if(70712<=E&&E<=70719)return f.CLUSTER_BREAK.EXTEND}else if(E<70722){if(70720<=E&&E<=70721)return f.CLUSTER_BREAK.SPACINGMARK}else if(70722<=E&&E<=70724)return f.CLUSTER_BREAK.EXTEND}else if(E<70832){if(E<70726){if(E===70725)return f.CLUSTER_BREAK.SPACINGMARK}else if(E===70726||E===70750)return f.CLUSTER_BREAK.EXTEND}else if(E<70833){if(E===70832)return f.CLUSTER_BREAK.EXTEND}else if(E<70835){if(70833<=E&&E<=70834)return f.CLUSTER_BREAK.SPACINGMARK}else if(70835<=E&&E<=70840)return f.CLUSTER_BREAK.EXTEND}else if(E<71096){if(E<70847)if(E<70843){if(E===70841)return f.CLUSTER_BREAK.SPACINGMARK;if(E===70842)return f.CLUSTER_BREAK.EXTEND}else if(E<70845){if(70843<=E&&E<=70844)return f.CLUSTER_BREAK.SPACINGMARK}else{if(E===70845)return f.CLUSTER_BREAK.EXTEND;if(E===70846)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<71087){if(E<70849){if(70847<=E&&E<=70848)return f.CLUSTER_BREAK.EXTEND}else if(E<70850){if(E===70849)return f.CLUSTER_BREAK.SPACINGMARK}else if(70850<=E&&E<=70851)return f.CLUSTER_BREAK.EXTEND}else if(E<71088){if(E===71087)return f.CLUSTER_BREAK.EXTEND}else if(E<71090){if(71088<=E&&E<=71089)return f.CLUSTER_BREAK.SPACINGMARK}else if(71090<=E&&E<=71093)return f.CLUSTER_BREAK.EXTEND}else if(E<71216){if(E<71102){if(E<71100){if(71096<=E&&E<=71099)return f.CLUSTER_BREAK.SPACINGMARK}else if(71100<=E&&E<=71101)return f.CLUSTER_BREAK.EXTEND}else if(E<71103){if(E===71102)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<71132){if(71103<=E&&E<=71104)return f.CLUSTER_BREAK.EXTEND}else if(71132<=E&&E<=71133)return f.CLUSTER_BREAK.EXTEND}else if(E<71229){if(E<71219){if(71216<=E&&E<=71218)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<71227){if(71219<=E&&E<=71226)return f.CLUSTER_BREAK.EXTEND}else if(71227<=E&&E<=71228)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<71230){if(E===71229)return f.CLUSTER_BREAK.EXTEND}else if(E<71231){if(E===71230)return f.CLUSTER_BREAK.SPACINGMARK}else if(71231<=E&&E<=71232)return f.CLUSTER_BREAK.EXTEND}else if(E<71999)if(E<71463){if(E<71350){if(E<71341){if(E===71339)return f.CLUSTER_BREAK.EXTEND;if(E===71340)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<71342){if(E===71341)return f.CLUSTER_BREAK.EXTEND}else if(E<71344){if(71342<=E&&E<=71343)return f.CLUSTER_BREAK.SPACINGMARK}else if(71344<=E&&E<=71349)return f.CLUSTER_BREAK.EXTEND}else if(E<71453){if(E===71350)return f.CLUSTER_BREAK.SPACINGMARK;if(E===71351)return f.CLUSTER_BREAK.EXTEND}else if(E<71458){if(71453<=E&&E<=71455)return f.CLUSTER_BREAK.EXTEND}else if(E<71462){if(71458<=E&&E<=71461)return f.CLUSTER_BREAK.EXTEND}else if(E===71462)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<71984){if(E<71727){if(E<71724){if(71463<=E&&E<=71467)return f.CLUSTER_BREAK.EXTEND}else if(71724<=E&&E<=71726)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<71736){if(71727<=E&&E<=71735)return f.CLUSTER_BREAK.EXTEND}else if(E<71737){if(E===71736)return f.CLUSTER_BREAK.SPACINGMARK}else if(71737<=E&&E<=71738)return f.CLUSTER_BREAK.EXTEND}else if(E<71995){if(E<71985){if(E===71984)return f.CLUSTER_BREAK.EXTEND}else if(E<71991){if(71985<=E&&E<=71989)return f.CLUSTER_BREAK.SPACINGMARK}else if(71991<=E&&E<=71992)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<71997){if(71995<=E&&E<=71996)return f.CLUSTER_BREAK.EXTEND}else{if(E===71997)return f.CLUSTER_BREAK.SPACINGMARK;if(E===71998)return f.CLUSTER_BREAK.EXTEND}else if(E<72193)if(E<72145)if(E<72001){if(E===71999)return f.CLUSTER_BREAK.PREPEND;if(E===72e3)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<72002){if(E===72001)return f.CLUSTER_BREAK.PREPEND}else{if(E===72002)return f.CLUSTER_BREAK.SPACINGMARK;if(E===72003)return f.CLUSTER_BREAK.EXTEND}else if(E<72156){if(E<72148){if(72145<=E&&E<=72147)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<72154){if(72148<=E&&E<=72151)return f.CLUSTER_BREAK.EXTEND}else if(72154<=E&&E<=72155)return f.CLUSTER_BREAK.EXTEND}else if(E<72160){if(72156<=E&&E<=72159)return f.CLUSTER_BREAK.SPACINGMARK}else{if(E===72160)return f.CLUSTER_BREAK.EXTEND;if(E===72164)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<72263){if(E<72249){if(E<72243){if(72193<=E&&E<=72202)return f.CLUSTER_BREAK.EXTEND}else if(72243<=E&&E<=72248)return f.CLUSTER_BREAK.EXTEND}else if(E<72250){if(E===72249)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<72251){if(E===72250)return f.CLUSTER_BREAK.PREPEND}else if(72251<=E&&E<=72254)return f.CLUSTER_BREAK.EXTEND}else if(E<72281){if(E<72273){if(E===72263)return f.CLUSTER_BREAK.EXTEND}else if(E<72279){if(72273<=E&&E<=72278)return f.CLUSTER_BREAK.EXTEND}else if(72279<=E&&E<=72280)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<72324){if(72281<=E&&E<=72283)return f.CLUSTER_BREAK.EXTEND}else if(E<72330){if(72324<=E&&E<=72329)return f.CLUSTER_BREAK.PREPEND}else if(72330<=E&&E<=72342)return f.CLUSTER_BREAK.EXTEND}else if(E<94033){if(E<73104){if(E<72881){if(E<72766){if(E<72751){if(E<72344){if(E===72343)return f.CLUSTER_BREAK.SPACINGMARK}else if(72344<=E&&E<=72345)return f.CLUSTER_BREAK.EXTEND}else if(E<72752){if(E===72751)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<72760){if(72752<=E&&E<=72758)return f.CLUSTER_BREAK.EXTEND}else if(72760<=E&&E<=72765)return f.CLUSTER_BREAK.EXTEND}else if(E<72850){if(E===72766)return f.CLUSTER_BREAK.SPACINGMARK;if(E===72767)return f.CLUSTER_BREAK.EXTEND}else if(E<72873){if(72850<=E&&E<=72871)return f.CLUSTER_BREAK.EXTEND}else if(E<72874){if(E===72873)return f.CLUSTER_BREAK.SPACINGMARK}else if(72874<=E&&E<=72880)return f.CLUSTER_BREAK.EXTEND}else if(E<73018){if(E<72884){if(E<72882){if(E===72881)return f.CLUSTER_BREAK.SPACINGMARK}else if(72882<=E&&E<=72883)return f.CLUSTER_BREAK.EXTEND}else if(E<72885){if(E===72884)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<73009){if(72885<=E&&E<=72886)return f.CLUSTER_BREAK.EXTEND}else if(73009<=E&&E<=73014)return f.CLUSTER_BREAK.EXTEND}else if(E<73030){if(E<73020){if(E===73018)return f.CLUSTER_BREAK.EXTEND}else if(E<73023){if(73020<=E&&E<=73021)return f.CLUSTER_BREAK.EXTEND}else if(73023<=E&&E<=73029)return f.CLUSTER_BREAK.EXTEND}else if(E<73031){if(E===73030)return f.CLUSTER_BREAK.PREPEND}else if(E<73098){if(E===73031)return f.CLUSTER_BREAK.EXTEND}else if(73098<=E&&E<=73102)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<73526){if(E<73459)if(E<73109){if(E<73107){if(73104<=E&&E<=73105)return f.CLUSTER_BREAK.EXTEND}else if(73107<=E&&E<=73108)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<73110){if(E===73109)return f.CLUSTER_BREAK.EXTEND}else{if(E===73110)return f.CLUSTER_BREAK.SPACINGMARK;if(E===73111)return f.CLUSTER_BREAK.EXTEND}else if(E<73474){if(E<73461){if(73459<=E&&E<=73460)return f.CLUSTER_BREAK.EXTEND}else if(E<73472){if(73461<=E&&E<=73462)return f.CLUSTER_BREAK.SPACINGMARK}else if(73472<=E&&E<=73473)return f.CLUSTER_BREAK.EXTEND}else if(E<73475){if(E===73474)return f.CLUSTER_BREAK.PREPEND}else if(E<73524){if(E===73475)return f.CLUSTER_BREAK.SPACINGMARK}else if(73524<=E&&E<=73525)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<78896)if(E<73536){if(E<73534){if(73526<=E&&E<=73530)return f.CLUSTER_BREAK.EXTEND}else if(73534<=E&&E<=73535)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<73537){if(E===73536)return f.CLUSTER_BREAK.EXTEND}else{if(E===73537)return f.CLUSTER_BREAK.SPACINGMARK;if(E===73538)return f.CLUSTER_BREAK.EXTEND}else if(E<92912){if(E<78912){if(78896<=E&&E<=78911)return f.CLUSTER_BREAK.CONTROL}else if(E<78919){if(E===78912)return f.CLUSTER_BREAK.EXTEND}else if(78919<=E&&E<=78933)return f.CLUSTER_BREAK.EXTEND}else if(E<92976){if(92912<=E&&E<=92916)return f.CLUSTER_BREAK.EXTEND}else if(E<94031){if(92976<=E&&E<=92982)return f.CLUSTER_BREAK.EXTEND}else if(E===94031)return f.CLUSTER_BREAK.EXTEND}else if(E<121476){if(E<119143)if(E<113824){if(E<94180){if(E<94095){if(94033<=E&&E<=94087)return f.CLUSTER_BREAK.SPACINGMARK}else if(94095<=E&&E<=94098)return f.CLUSTER_BREAK.EXTEND}else if(E<94192){if(E===94180)return f.CLUSTER_BREAK.EXTEND}else if(E<113821){if(94192<=E&&E<=94193)return f.CLUSTER_BREAK.SPACINGMARK}else if(113821<=E&&E<=113822)return f.CLUSTER_BREAK.EXTEND}else if(E<118576){if(E<118528){if(113824<=E&&E<=113827)return f.CLUSTER_BREAK.CONTROL}else if(118528<=E&&E<=118573)return f.CLUSTER_BREAK.EXTEND}else if(E<119141){if(118576<=E&&E<=118598)return f.CLUSTER_BREAK.EXTEND}else{if(E===119141)return f.CLUSTER_BREAK.EXTEND;if(E===119142)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<119173){if(E<119150){if(E<119149){if(119143<=E&&E<=119145)return f.CLUSTER_BREAK.EXTEND}else if(E===119149)return f.CLUSTER_BREAK.SPACINGMARK}else if(E<119155){if(119150<=E&&E<=119154)return f.CLUSTER_BREAK.EXTEND}else if(E<119163){if(119155<=E&&E<=119162)return f.CLUSTER_BREAK.CONTROL}else if(119163<=E&&E<=119170)return f.CLUSTER_BREAK.EXTEND}else if(E<121344){if(E<119210){if(119173<=E&&E<=119179)return f.CLUSTER_BREAK.EXTEND}else if(E<119362){if(119210<=E&&E<=119213)return f.CLUSTER_BREAK.EXTEND}else if(119362<=E&&E<=119364)return f.CLUSTER_BREAK.EXTEND}else if(E<121403){if(121344<=E&&E<=121398)return f.CLUSTER_BREAK.EXTEND}else if(E<121461){if(121403<=E&&E<=121452)return f.CLUSTER_BREAK.EXTEND}else if(E===121461)return f.CLUSTER_BREAK.EXTEND}else if(E<123628){if(E<122907){if(E<121505){if(E<121499){if(E===121476)return f.CLUSTER_BREAK.EXTEND}else if(121499<=E&&E<=121503)return f.CLUSTER_BREAK.EXTEND}else if(E<122880){if(121505<=E&&E<=121519)return f.CLUSTER_BREAK.EXTEND}else if(E<122888){if(122880<=E&&E<=122886)return f.CLUSTER_BREAK.EXTEND}else if(122888<=E&&E<=122904)return f.CLUSTER_BREAK.EXTEND}else if(E<123023){if(E<122915){if(122907<=E&&E<=122913)return f.CLUSTER_BREAK.EXTEND}else if(E<122918){if(122915<=E&&E<=122916)return f.CLUSTER_BREAK.EXTEND}else if(122918<=E&&E<=122922)return f.CLUSTER_BREAK.EXTEND}else if(E<123184){if(E===123023)return f.CLUSTER_BREAK.EXTEND}else if(E<123566){if(123184<=E&&E<=123190)return f.CLUSTER_BREAK.EXTEND}else if(E===123566)return f.CLUSTER_BREAK.EXTEND}else if(E<127995){if(E<125136){if(E<124140){if(123628<=E&&E<=123631)return f.CLUSTER_BREAK.EXTEND}else if(124140<=E&&E<=124143)return f.CLUSTER_BREAK.EXTEND}else if(E<125252){if(125136<=E&&E<=125142)return f.CLUSTER_BREAK.EXTEND}else if(E<127462){if(125252<=E&&E<=125258)return f.CLUSTER_BREAK.EXTEND}else if(127462<=E&&E<=127487)return f.CLUSTER_BREAK.REGIONAL_INDICATOR}else if(E<917632){if(E<917504){if(127995<=E&&E<=127999)return f.CLUSTER_BREAK.EXTEND}else if(E<917536){if(917504<=E&&E<=917535)return f.CLUSTER_BREAK.CONTROL}else if(917536<=E&&E<=917631)return f.CLUSTER_BREAK.EXTEND}else if(E<917760){if(917632<=E&&E<=917759)return f.CLUSTER_BREAK.CONTROL}else if(E<918e3){if(917760<=E&&E<=917999)return f.CLUSTER_BREAK.EXTEND}else if(918e3<=E&&E<=921599)return f.CLUSTER_BREAK.CONTROL;return f.CLUSTER_BREAK.OTHER}static getEmojiProperty(E){if(E<10160){if(E<9728){if(E<9e3){if(E<8482){if(E<8252){if(E===169||E===174)return f.EXTENDED_PICTOGRAPHIC}else if(E===8252||E===8265)return f.EXTENDED_PICTOGRAPHIC}else if(E<8596){if(E===8482||E===8505)return f.EXTENDED_PICTOGRAPHIC}else if(E<8617){if(8596<=E&&E<=8601)return f.EXTENDED_PICTOGRAPHIC}else if(E<8986){if(8617<=E&&E<=8618)return f.EXTENDED_PICTOGRAPHIC}else if(8986<=E&&E<=8987)return f.EXTENDED_PICTOGRAPHIC}else if(E<9410){if(E<9167){if(E===9e3||E===9096)return f.EXTENDED_PICTOGRAPHIC}else if(E<9193){if(E===9167)return f.EXTENDED_PICTOGRAPHIC}else if(E<9208){if(9193<=E&&E<=9203)return f.EXTENDED_PICTOGRAPHIC}else if(9208<=E&&E<=9210)return f.EXTENDED_PICTOGRAPHIC}else if(E<9654){if(E<9642){if(E===9410)return f.EXTENDED_PICTOGRAPHIC}else if(9642<=E&&E<=9643)return f.EXTENDED_PICTOGRAPHIC}else if(E<9664){if(E===9654)return f.EXTENDED_PICTOGRAPHIC}else if(E<9723){if(E===9664)return f.EXTENDED_PICTOGRAPHIC}else if(9723<=E&&E<=9726)return f.EXTENDED_PICTOGRAPHIC}else if(E<10035){if(E<10004){if(E<9748){if(E<9735){if(9728<=E&&E<=9733)return f.EXTENDED_PICTOGRAPHIC}else if(9735<=E&&E<=9746)return f.EXTENDED_PICTOGRAPHIC}else if(E<9872){if(9748<=E&&E<=9861)return f.EXTENDED_PICTOGRAPHIC}else if(E<9992){if(9872<=E&&E<=9989)return f.EXTENDED_PICTOGRAPHIC}else if(9992<=E&&E<=10002)return f.EXTENDED_PICTOGRAPHIC}else if(E<10013){if(E===10004||E===10006)return f.EXTENDED_PICTOGRAPHIC}else if(E<10017){if(E===10013)return f.EXTENDED_PICTOGRAPHIC}else if(E===10017||E===10024)return f.EXTENDED_PICTOGRAPHIC}else if(E<10067){if(E<10055){if(E<10052){if(10035<=E&&E<=10036)return f.EXTENDED_PICTOGRAPHIC}else if(E===10052)return f.EXTENDED_PICTOGRAPHIC}else if(E<10060){if(E===10055)return f.EXTENDED_PICTOGRAPHIC}else if(E===10060||E===10062)return f.EXTENDED_PICTOGRAPHIC}else if(E<10083){if(E<10071){if(10067<=E&&E<=10069)return f.EXTENDED_PICTOGRAPHIC}else if(E===10071)return f.EXTENDED_PICTOGRAPHIC}else if(E<10133){if(10083<=E&&E<=10087)return f.EXTENDED_PICTOGRAPHIC}else if(E<10145){if(10133<=E&&E<=10135)return f.EXTENDED_PICTOGRAPHIC}else if(E===10145)return f.EXTENDED_PICTOGRAPHIC}else if(E<127489){if(E<12951){if(E<11035){if(E<10548){if(E===10160||E===10175)return f.EXTENDED_PICTOGRAPHIC}else if(E<11013){if(10548<=E&&E<=10549)return f.EXTENDED_PICTOGRAPHIC}else if(11013<=E&&E<=11015)return f.EXTENDED_PICTOGRAPHIC}else if(E<11093){if(E<11088){if(11035<=E&&E<=11036)return f.EXTENDED_PICTOGRAPHIC}else if(E===11088)return f.EXTENDED_PICTOGRAPHIC}else if(E<12336){if(E===11093)return f.EXTENDED_PICTOGRAPHIC}else if(E===12336||E===12349)return f.EXTENDED_PICTOGRAPHIC}else if(E<127340){if(E<126976){if(E===12951||E===12953)return f.EXTENDED_PICTOGRAPHIC}else if(E<127245){if(126976<=E&&E<=127231)return f.EXTENDED_PICTOGRAPHIC}else if(E<127279){if(127245<=E&&E<=127247)return f.EXTENDED_PICTOGRAPHIC}else if(E===127279)return f.EXTENDED_PICTOGRAPHIC}else if(E<127374){if(E<127358){if(127340<=E&&E<=127345)return f.EXTENDED_PICTOGRAPHIC}else if(127358<=E&&E<=127359)return f.EXTENDED_PICTOGRAPHIC}else if(E<127377){if(E===127374)return f.EXTENDED_PICTOGRAPHIC}else if(E<127405){if(127377<=E&&E<=127386)return f.EXTENDED_PICTOGRAPHIC}else if(127405<=E&&E<=127461)return f.EXTENDED_PICTOGRAPHIC}else if(E<128981){if(E<127561){if(E<127535){if(E<127514){if(127489<=E&&E<=127503)return f.EXTENDED_PICTOGRAPHIC}else if(E===127514)return f.EXTENDED_PICTOGRAPHIC}else if(E<127538){if(E===127535)return f.EXTENDED_PICTOGRAPHIC}else if(E<127548){if(127538<=E&&E<=127546)return f.EXTENDED_PICTOGRAPHIC}else if(127548<=E&&E<=127551)return f.EXTENDED_PICTOGRAPHIC}else if(E<128326){if(E<128e3){if(127561<=E&&E<=127994)return f.EXTENDED_PICTOGRAPHIC}else if(128e3<=E&&E<=128317)return f.EXTENDED_PICTOGRAPHIC}else if(E<128640){if(128326<=E&&E<=128591)return f.EXTENDED_PICTOGRAPHIC}else if(E<128884){if(128640<=E&&E<=128767)return f.EXTENDED_PICTOGRAPHIC}else if(128884<=E&&E<=128895)return f.EXTENDED_PICTOGRAPHIC}else if(E<129198){if(E<129096){if(E<129036){if(128981<=E&&E<=129023)return f.EXTENDED_PICTOGRAPHIC}else if(129036<=E&&E<=129039)return f.EXTENDED_PICTOGRAPHIC}else if(E<129114){if(129096<=E&&E<=129103)return f.EXTENDED_PICTOGRAPHIC}else if(E<129160){if(129114<=E&&E<=129119)return f.EXTENDED_PICTOGRAPHIC}else if(129160<=E&&E<=129167)return f.EXTENDED_PICTOGRAPHIC}else if(E<129340){if(E<129292){if(129198<=E&&E<=129279)return f.EXTENDED_PICTOGRAPHIC}else if(129292<=E&&E<=129338)return f.EXTENDED_PICTOGRAPHIC}else if(E<129351){if(129340<=E&&E<=129349)return f.EXTENDED_PICTOGRAPHIC}else if(E<130048){if(129351<=E&&E<=129791)return f.EXTENDED_PICTOGRAPHIC}else if(130048<=E&&E<=131069)return f.EXTENDED_PICTOGRAPHIC;return f.CLUSTER_BREAK.OTHER}};b.default=C});var p=l(e=>{"use strict";var Y=e&&e.__importDefault||function(x){return x&&x.__esModule?x:{default:x}};Object.defineProperty(e,"__esModule",{value:!0});var $=Y(g());e.default=$.default});var v=J(p()),c=new v.default;globalThis.Intl=globalThis.Intl||{};globalThis.Intl.Segmenter=globalThis.Intl.Segmenter||class{constructor(){}segment=c.iterateGraphemes}; + diff --git a/yarn.lock b/yarn.lock index e6976c269..111e800f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19,12 +19,15 @@ jsonpointer "^5.0.0" leven "^3.1.0" -"@atproto/api@0.1.3": - version "0.1.3" - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.1.3.tgz#4aa9ea7caad624a7eda7d22e03f076e4b0fb68fb" - integrity sha512-jEtE0Afxnkvth7/dZKYx9Gv1IpO2Jlmb8KzgRVPnyYyolI2GI4VTNs7mxxO/44cs8vKu2PN2zW+64XuaIY1JBA== +"@atproto/api@*", "@atproto/api@0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.2.0.tgz#4a60f8f1de91105ad93526d69abcf011bbeaa3be" + integrity sha512-AntqYOVrMalBJapnNBV0akh/PWcsKdWq8zfuvv8hZW/jwOkJTVPTRFOP2OHJFcfz4WezytX43ml/L2kSG9z4+Q== dependencies: + "@atproto/common-web" "*" + "@atproto/uri" "*" "@atproto/xrpc" "*" + tlds "^1.234.0" typed-emitter "^2.1.0" "@atproto/auth@*": @@ -37,6 +40,15 @@ "@ucans/core" "0.11.0" uint8arrays "3.0.0" +"@atproto/common-web@*": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.1.0.tgz#5529fa66f9533aa00cfd13f0a25757df7b26bd3d" + integrity sha512-qD6xF60hvH+cP++fk/mt+0S9cxs94KsK+rNWypNlgnlp7r9By4ltXwtDSR/DNTA8mwDeularUno4VbTd2IWIzA== + dependencies: + multiformats "^9.6.4" + uint8arrays "3.0.0" + zod "^3.14.2" + "@atproto/common@*": version "0.1.1" resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.1.1.tgz#ec33a3b4995c91d3ad2e90fc4cdbc65284ceff84" @@ -47,7 +59,17 @@ pino "^8.6.1" zod "^3.14.2" -"@atproto/crypto@*": +"@atproto/common@0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.1.0.tgz#4216a8fef5b985ab62ac21252a0f8ca0f4a0f210" + integrity sha512-OB5tWE2R19jwiMIs2IjQieH5KTUuMb98XGCn9h3xuu6NanwjlmbCYMv08fMYwIp3UQ6jcq//84cDT3Bu6fJD+A== + dependencies: + "@ipld/dag-cbor" "^7.0.3" + multiformats "^9.6.4" + pino "^8.6.1" + zod "^3.14.2" + +"@atproto/crypto@*", "@atproto/crypto@0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@atproto/crypto/-/crypto-0.1.0.tgz#bc73a479f9dbe06fa025301c182d7f7ab01bc568" integrity sha512-9xgFEPtsCiJEPt9o3HtJT30IdFTGw5cQRSJVIy5CFhqBA4vDLcdXiRDLCjkzHEVbtNCsHUW6CrlfOgbeLPcmcg== @@ -68,14 +90,14 @@ axios "^0.24.0" did-resolver "^4.0.0" -"@atproto/handle@*": - version "0.0.1" - resolved "https://registry.yarnpkg.com/@atproto/handle/-/handle-0.0.1.tgz#783f88aaef1f57920deb61da8d72e5191cd9d515" - integrity sha512-foWqpzyVufo6/LxHeqBqoz9KhoLIGpIQ3zqYXlJWX4YD6OlFq3RfrGYbJrqHIiHhe1xgm6GIgEax8V6QIEbATA== +"@atproto/identifier@*": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@atproto/identifier/-/identifier-0.1.0.tgz#6b600c8a3da08d9a7d5eab076f8b7064457dde75" + integrity sha512-3LV7+4E6S0k8Rru7NBkyDF6Zf6NHVUXVS9d4l9fiXWMC49ghZMjq0vPmz80xjG1rRuFdJFbpRf4ApFciGxLIyQ== dependencies: - "@sideway/address" "^5.0.0" + "@atproto/common-web" "*" -"@atproto/lexicon@*", "@atproto/lexicon@^0.0.4": +"@atproto/lexicon@*": version "0.0.4" resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.0.4.tgz#f0a6688ad54adb2ec4a8d1f11fcbf45e96203c4b" integrity sha512-00lqIKJetVlxQzNmEhrFzZeT9k+zGPBsHwtYpG7rH4vZ211i5WiDkmQcBwwFs2g/qCBt+nVq0dlgl3JhCLJXQg== @@ -89,20 +111,21 @@ resolved "https://registry.yarnpkg.com/@atproto/nsid/-/nsid-0.0.1.tgz#0cdc00cefe8f0b1385f352b9f57b3ad37fff09a4" integrity sha512-t5M6/CzWBVYoBbIvfKDpqPj/+ZmyoK9ydZSStcTXosJ27XXwOPhz0VDUGKK2SM9G5Y7TPes8S5KTAU0UdVYFCw== -"@atproto/pds@^0.0.3": - version "0.0.3" - resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.0.3.tgz#118a1d51687664f085f8e1c19ae3ac1646dc69b2" - integrity sha512-l5iGJNyQs73V/mQWkcg4NXNGbnfXfv+Yg3g8nqwtun409iAAeYZimA1Tt0JV/hyq+Oz3antG0VvLQQEtNVUpVQ== +"@atproto/pds@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.1.0.tgz#8014269c12a322b14618e0991c534979a4b145d7" + integrity sha512-f1KPONxim674owWcTsR8S5r57+b7evg+zy+jkcTX00BB0fO6PchDL6sTQQc1x3u2QZArHDSUUUgoHt4IWwsfkw== dependencies: + "@atproto/api" "*" "@atproto/common" "*" "@atproto/crypto" "*" "@atproto/did-resolver" "*" - "@atproto/handle" "*" + "@atproto/identifier" "*" "@atproto/lexicon" "*" - "@atproto/plc" "*" "@atproto/repo" "*" "@atproto/uri" "*" "@atproto/xrpc-server" "*" + "@did-plc/lib" "^0.0.1" better-sqlite3 "^7.6.2" bytes "^3.1.2" cors "^2.8.5" @@ -126,29 +149,6 @@ typed-emitter "^2.1.0" uint8arrays "3.0.0" -"@atproto/plc@*": - version "0.0.1" - resolved "https://registry.yarnpkg.com/@atproto/plc/-/plc-0.0.1.tgz#713de881fd2b803a0f1afbee57735de8382a8ed3" - integrity sha512-9JM027ioAb6rG+2F/p89DJlIXBOH85rGWXFcG3dImZJ8SalFqRZ0/7gtdFN387IZ/HNAWRmmFaAxMEmJ9NgKpQ== - dependencies: - "@atproto/common" "*" - "@atproto/crypto" "*" - "@ipld/dag-cbor" "^7.0.3" - async-mutex "^0.4.0" - axios "^0.27.2" - better-sqlite3 "^7.6.2" - cors "^2.8.5" - dotenv "^16.0.2" - express "^4.17.2" - express-async-errors "^3.1.1" - http-terminator "^3.2.0" - kysely "^0.22.0" - pg "^8.8.0" - pino "^8.6.1" - pino-http "^8.2.1" - uint8arrays "3.0.0" - zod "^3.14.2" - "@atproto/repo@*": version "0.0.1" resolved "https://registry.yarnpkg.com/@atproto/repo/-/repo-0.0.1.tgz#41c63943a7e6a0942fc3e721c05d8c836c2fcfc2" @@ -180,7 +180,7 @@ mime-types "^2.1.35" zod "^3.14.2" -"@atproto/xrpc@*", "@atproto/xrpc@^0.0.4": +"@atproto/xrpc@*": version "0.0.4" resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.0.4.tgz#d7dd45cdb21e29b9715ca30eb18320548f293413" integrity sha512-Hxh+GgZx21Zvlb2RMlSlJDd3r3GR0vAS6OOZPW2xzWiVHsetb9ZlFB6D0AeAPj2R+U2UUkmdUR8G3U/nkgnQFA== @@ -1314,6 +1314,13 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + "@csstools/normalize.css@*": version "12.0.0" resolved "https://registry.yarnpkg.com/@csstools/normalize.css/-/normalize.css-12.0.0.tgz#a9583a75c3f150667771f30b60d9f059473e62c4" @@ -1425,6 +1432,38 @@ resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.1.1.tgz#c9c61d9fe5ca5ac664e1153bb0aa0eba1c6d6308" integrity sha512-jwx+WCqszn53YHOfvFMJJRd/B2GqkCBt+1MJSG6o5/s8+ytHMvDZXsJgUEWLk12UnLd7HYKac4BYU5i/Ron1Cw== +"@did-plc/lib@*", "@did-plc/lib@^0.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@did-plc/lib/-/lib-0.0.1.tgz#5fd78c71901168ac05c5650af3a376c76461991c" + integrity sha512-RkY5w9DbYMco3SjeepqIiMveqz35exjlVDipCs2gz9AXF4/cp9hvmrp9zUWEw2vny+FjV8vGEN7QpaXWaO6nhg== + dependencies: + "@atproto/common" "0.1.0" + "@atproto/crypto" "0.1.0" + "@ipld/dag-cbor" "^7.0.3" + axios "^1.3.4" + multiformats "^9.6.4" + uint8arrays "3.0.0" + zod "^3.14.2" + +"@did-plc/server@^0.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@did-plc/server/-/server-0.0.1.tgz#8d1ba701f3b2b952b7c8fe03ef3118bb0cba077c" + integrity sha512-GtxxHcOrOQ6fNI1ufq3Zqjc2PtWqPZOdsuzlwtxiH9XibUGwDkb0GmaBHyU5GiOxOKZEW1GspZ8mreBA6XOlTQ== + dependencies: + "@atproto/common" "0.1.0" + "@atproto/crypto" "0.1.0" + "@did-plc/lib" "*" + axios "^1.3.4" + cors "^2.8.5" + express "^4.18.2" + express-async-errors "^3.1.1" + http-terminator "^3.2.0" + kysely "^0.23.4" + multiformats "^9.6.4" + pg "^8.9.0" + pino "^8.11.0" + pino-http "^8.3.3" + "@discoveryjs/json-ext@^0.5.0": version "0.5.7" resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" @@ -1953,11 +1992,6 @@ resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.1.2.tgz#6fc464307cbe3c8ca5064549b806360d84457b04" integrity sha512-9anpBMM9mEgZN4wr2v8wHJI2/u5TnnggewRN6OlvXTTnuVyoY19X6rOv9XTqKRw6dcGKwZsBi8n0kDE2I5i4VA== -"@hapi/hoek@^10.0.0": - version "10.0.1" - resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-10.0.1.tgz#ee9da297fabc557e1c040a0f44ee89c266ccc306" - integrity sha512-CvlW7jmOhWzuqOqiJQ3rQVLMcREh0eel4IBnxDx2FAcK8g7qoJRQK4L1CPBASoCY6y8e6zuCy3f2g+HWdkzcMw== - "@hapi/hoek@^9.0.0": version "9.3.0" resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" @@ -2459,7 +2493,7 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" -"@jridgewell/resolve-uri@3.1.0": +"@jridgewell/resolve-uri@3.1.0", "@jridgewell/resolve-uri@^3.0.3": version "3.1.0" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== @@ -2482,6 +2516,14 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.15", "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": version "0.3.17" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985" @@ -2644,10 +2686,10 @@ dependencies: serve-static "^1.13.1" -"@react-native-community/cli-doctor@^10.1.1": - version "10.2.1" - resolved "https://registry.yarnpkg.com/@react-native-community/cli-doctor/-/cli-doctor-10.2.1.tgz#b6b7a3f0f9cef1a05f1adc6393eb29c6f8f2972c" - integrity sha512-IwhdSD+mtgWdxg2eMr0fpkn08XN7r70DC1riGSmqK/DXNyWBzIZlCkDN+/TwlaUEsiFk6LQTjgCiqZSMpmDrsg== +"@react-native-community/cli-doctor@^10.2.0": + version "10.2.2" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-doctor/-/cli-doctor-10.2.2.tgz#b1893604fa9fc8971064e7c00042350f96868bfe" + integrity sha512-49Ep2aQOF0PkbAR/TcyMjOm9XwBa8VQr+/Zzf4SJeYwiYLCT1NZRAVAVjYRXl0xqvq5S5mAGZZShS4AQl4WsZw== dependencies: "@react-native-community/cli-config" "^10.1.1" "@react-native-community/cli-platform-ios" "^10.2.1" @@ -2666,7 +2708,7 @@ sudo-prompt "^9.0.0" wcwidth "^1.0.1" -"@react-native-community/cli-hermes@^10.1.3": +"@react-native-community/cli-hermes@^10.2.0": version "10.2.0" resolved "https://registry.yarnpkg.com/@react-native-community/cli-hermes/-/cli-hermes-10.2.0.tgz#cc252f435b149f74260bc918ce22fdf58033a87e" integrity sha512-urfmvNeR8IiO/Sd92UU3xPO+/qI2lwCWQnxOkWaU/i2EITFekE47MD6MZrfVulRVYRi5cuaFqKZO/ccOdOB/vQ== @@ -2677,18 +2719,7 @@ hermes-profile-transformer "^0.0.6" ip "^1.1.5" -"@react-native-community/cli-platform-android@10.1.3": - version "10.1.3" - resolved "https://registry.yarnpkg.com/@react-native-community/cli-platform-android/-/cli-platform-android-10.1.3.tgz#8380799cd4d3f9a0ca568b0f5b4ae9e462ce3669" - integrity sha512-8YZEpBL6yd9l4CIoFcLOgrV8x2GDujdqrdWrNsNERDAbsiFwqAQvfjyyb57GAZVuEPEJCoqUlGlMCwOh3XQb9A== - dependencies: - "@react-native-community/cli-tools" "^10.1.1" - chalk "^4.1.2" - execa "^1.0.0" - glob "^7.1.3" - logkitty "^0.7.1" - -"@react-native-community/cli-platform-android@^10.2.0": +"@react-native-community/cli-platform-android@10.2.0", "@react-native-community/cli-platform-android@^10.2.0": version "10.2.0" resolved "https://registry.yarnpkg.com/@react-native-community/cli-platform-android/-/cli-platform-android-10.2.0.tgz#0bc689270a5f1d9aaf9e723181d43ca4dbfffdef" integrity sha512-CBenYwGxwFdObZTn1lgxWtMGA5ms2G/ALQhkS+XTAD7KHDrCxFF9yT/fnAjFZKM6vX/1TqGI1RflruXih3kAhw== @@ -2699,14 +2730,15 @@ glob "^7.1.3" logkitty "^0.7.1" -"@react-native-community/cli-platform-ios@10.1.1": - version "10.1.1" - resolved "https://registry.yarnpkg.com/@react-native-community/cli-platform-ios/-/cli-platform-ios-10.1.1.tgz#39ed6810117d8e7330d3aa4d85818fb6ae358785" - integrity sha512-EB9/L8j1LqrqyfJtLRixU+d8FIP6Pr83rEgUgXgya/u8wk3h/bvX70w+Ff2skwjdPLr5dLUQ/n5KFX4r3bsNmA== +"@react-native-community/cli-platform-ios@10.2.0": + version "10.2.0" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-platform-ios/-/cli-platform-ios-10.2.0.tgz#be21c0e3bbf17358d540cc23e5556bf679f6322e" + integrity sha512-hIPK3iL/mL+0ChXmQ9uqqzNOKA48H+TAzg+hrxQLll/6dNMxDeK9/wZpktcsh8w+CyhqzKqVernGcQs7tPeKGw== dependencies: "@react-native-community/cli-tools" "^10.1.1" chalk "^4.1.2" execa "^1.0.0" + fast-xml-parser "^4.0.12" glob "^7.1.3" ora "^5.4.1" @@ -2722,21 +2754,21 @@ glob "^7.1.3" ora "^5.4.1" -"@react-native-community/cli-plugin-metro@^10.1.1": - version "10.2.0" - resolved "https://registry.yarnpkg.com/@react-native-community/cli-plugin-metro/-/cli-plugin-metro-10.2.0.tgz#83cabbc04c80f7e94f88ed998b72c7d572c6f094" - integrity sha512-9eiJrKYuauEDkQLCrjJUh7tS9T0oaMQqVUSSSuyDG6du7HQcfaR4mSf21wK75jvhKiwcQLpsFmMdctAb+0v+Cg== +"@react-native-community/cli-plugin-metro@^10.2.0": + version "10.2.2" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-plugin-metro/-/cli-plugin-metro-10.2.2.tgz#766914e3c8007dfe52b253544c4f6cd8549919ac" + integrity sha512-sTGjZlD3OGqbF9v1ajwUIXhGmjw9NyJ/14Lo0sg7xH8Pv4qUd5ZvQ6+DWYrQn3IKFUMfGFWYyL81ovLuPylrpw== dependencies: "@react-native-community/cli-server-api" "^10.1.1" "@react-native-community/cli-tools" "^10.1.1" chalk "^4.1.2" execa "^1.0.0" - metro "0.73.8" - metro-config "0.73.8" - metro-core "0.73.8" - metro-react-native-babel-transformer "0.73.8" - metro-resolver "0.73.8" - metro-runtime "0.73.8" + metro "0.73.9" + metro-config "0.73.9" + metro-core "0.73.9" + metro-react-native-babel-transformer "0.73.9" + metro-resolver "0.73.9" + metro-runtime "0.73.9" readline "^1.3.0" "@react-native-community/cli-server-api@^10.1.1": @@ -2776,17 +2808,17 @@ dependencies: joi "^17.2.1" -"@react-native-community/cli@10.1.3": - version "10.1.3" - resolved "https://registry.yarnpkg.com/@react-native-community/cli/-/cli-10.1.3.tgz#ad610c46da9fc7c717272024ec757dc646726506" - integrity sha512-kzh6bYLGN1q1q0IiczKSP1LTrovFeVzppYRTKohPI9VdyZwp7b5JOgaQMB/Ijtwm3MxBDrZgV9AveH/eUmUcKQ== +"@react-native-community/cli@10.2.0": + version "10.2.0" + resolved "https://registry.yarnpkg.com/@react-native-community/cli/-/cli-10.2.0.tgz#bcb65bb3dcb03b0fc4e49619d51e12d23396b301" + integrity sha512-QH7AFBz5FX2zTZRH/o3XehHrZ0aZZEL5Sh+23nSEFgSj3bLFfvjjZhuoiRSAo7iiBdvAoXrfxQ8TXgg4Xf/7fw== dependencies: "@react-native-community/cli-clean" "^10.1.1" "@react-native-community/cli-config" "^10.1.1" "@react-native-community/cli-debugger-ui" "^10.0.0" - "@react-native-community/cli-doctor" "^10.1.1" - "@react-native-community/cli-hermes" "^10.1.3" - "@react-native-community/cli-plugin-metro" "^10.1.1" + "@react-native-community/cli-doctor" "^10.2.0" + "@react-native-community/cli-hermes" "^10.2.0" + "@react-native-community/cli-plugin-metro" "^10.2.0" "@react-native-community/cli-server-api" "^10.1.1" "@react-native-community/cli-tools" "^10.1.1" "@react-native-community/cli-types" "^10.0.0" @@ -3019,13 +3051,6 @@ dependencies: "@hapi/hoek" "^9.0.0" -"@sideway/address@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@sideway/address/-/address-5.0.0.tgz#015f191a4a29e2b2f9ad1aabe7465c3088241536" - integrity sha512-IEZ3Gi972M1yubSPhcpzpVTT/Vb46F9L0W+K/GhqvWv6aAvVbNNVsYFekXWEemHHFfTVrxFcURrzsPGPPKkxKQ== - dependencies: - "@hapi/hoek" "^10.0.0" - "@sideway/formula@^3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" @@ -3314,6 +3339,26 @@ resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== +"@tsconfig/node10@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" + integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" + integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== + "@tsconfig/react-native@^2.0.3": version "2.0.3" resolved "https://registry.yarnpkg.com/@tsconfig/react-native/-/react-native-2.0.3.tgz#79ad8efc6d3729152da6cb23725b6c364a7349b2" @@ -4098,7 +4143,7 @@ acorn-walk@^7.0.0, acorn-walk@^7.1.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== -acorn-walk@^8.0.2: +acorn-walk@^8.0.2, acorn-walk@^8.1.1: version "8.2.0" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== @@ -4108,7 +4153,7 @@ acorn@^7.0.0, acorn@^7.1.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.1.0, acorn@^8.2.4, acorn@^8.5.0, acorn@^8.7.1, acorn@^8.8.0, acorn@^8.8.1: +acorn@^8.1.0, acorn@^8.2.4, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.1, acorn@^8.8.0, acorn@^8.8.1: version "8.8.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== @@ -4280,6 +4325,11 @@ arg@4.1.0: resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.0.tgz#583c518199419e0037abb74062c37f8519e575f0" integrity sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg== +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + arg@^5.0.2: version "5.0.2" resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" @@ -4449,13 +4499,6 @@ async-limiter@~1.0.0: resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== -async-mutex@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.4.0.tgz#ae8048cd4d04ace94347507504b3cf15e631c25f" - integrity sha512-eJFZ1YhRR8UN8eBLoNzcDPcy/jqjsg6I1AP+KvWQX80BqOSW1oJPJXDylPUEeMr2ZQvHgnQ//Lp6f3RQ1zI7HA== - dependencies: - tslib "^2.4.0" - async@^3.2.2, async@^3.2.3: version "3.2.4" resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" @@ -4515,13 +4558,14 @@ axios@^0.24.0: dependencies: follow-redirects "^1.14.4" -axios@^0.27.2: - version "0.27.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972" - integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== +axios@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.3.4.tgz#f5760cefd9cfb51fd2481acf88c05f67c4523024" + integrity sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ== dependencies: - follow-redirects "^1.14.9" + follow-redirects "^1.15.0" form-data "^4.0.0" + proxy-from-env "^1.1.0" axobject-query@^3.1.1: version "3.1.1" @@ -4704,10 +4748,10 @@ babel-preset-current-node-syntax@^1.0.0: "@babel/plugin-syntax-optional-chaining" "^7.8.3" "@babel/plugin-syntax-top-level-await" "^7.8.3" -babel-preset-expo@~9.3.0: - version "9.3.0" - resolved "https://registry.yarnpkg.com/babel-preset-expo/-/babel-preset-expo-9.3.0.tgz#51cb3c6e22126bcc14d17322d2f2dfb418e71222" - integrity sha512-cIz+5TVBkcZgtfpTyFPo1peswr2dvQj2VIwdj5vY37/zESsYBHfaZ+u/A11yb1WnuZHcYD/ZoSLNwmWr20jp4Q== +babel-preset-expo@~9.3.1: + version "9.3.1" + resolved "https://registry.yarnpkg.com/babel-preset-expo/-/babel-preset-expo-9.3.1.tgz#b31ddfce0d7ed1d848705e7178c1eb4ae4be9db0" + integrity sha512-1JL4T7q3uXu9FeJhLXDAKhFbWs75Qj2pixA60eR2ROzE9LnrKxm2g42OfcArS4vJcPj2NzcOdPpMI9/ZgF8i8Q== dependencies: "@babel/plugin-proposal-decorators" "^7.12.9" "@babel/plugin-proposal-object-rest-spread" "^7.12.13" @@ -4715,7 +4759,7 @@ babel-preset-expo@~9.3.0: "@babel/preset-env" "^7.20.0" babel-plugin-module-resolver "^4.1.0" babel-plugin-react-native-web "~0.18.10" - metro-react-native-babel-preset "0.73.7" + metro-react-native-babel-preset "0.73.8" babel-preset-fbjs@^3.4.0: version "3.4.0" @@ -5760,6 +5804,11 @@ create-react-class@^15.7.0: loose-envify "^1.3.1" object-assign "^4.1.1" +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + crelt@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.5.tgz#57c0d52af8c859e354bace1883eb2e1eb182bb94" @@ -6387,6 +6436,11 @@ diff-sequences@^29.4.3: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.3.tgz#9314bc1fabe09267ffeca9cbafc457d8499a13f2" integrity sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA== +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -6540,7 +6594,7 @@ dotenv@^10.0.0: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== -dotenv@^16.0.0, dotenv@^16.0.2, dotenv@^16.0.3: +dotenv@^16.0.0, dotenv@^16.0.3: version "16.0.3" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07" integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ== @@ -6800,16 +6854,16 @@ escape-html@~1.0.3: resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== -escape-string-regexp@2.0.0, escape-string-regexp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" - integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== - escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + escape-string-regexp@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" @@ -7375,10 +7429,10 @@ expo-modules-autolinking@1.1.2: find-up "^5.0.0" fs-extra "^9.1.0" -expo-modules-core@1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/expo-modules-core/-/expo-modules-core-1.2.5.tgz#3f9166f4c32c68ab8ef3e120c70ce9890b711650" - integrity sha512-5pXNlLHNKLayOusAFMbqr27gjgymHuKuWl/Dtbw2MjoyJY1MZCGD2nIJxd1TTcfnyxNxLg6OQmgkyqoBUFqBuw== +expo-modules-core@1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/expo-modules-core/-/expo-modules-core-1.2.6.tgz#921abc8031fe0e5474ee48905071902b9627d051" + integrity sha512-vyleKepkP8F6L+D55B/E4FbZ8x9pdy3yw/mdbGBkDkrmo2gmeMjOM1mKLSszOkLIqet05O7Wy8m0FZHZTo0VBg== dependencies: compare-versions "^3.4.0" invariant "^2.2.4" @@ -7411,17 +7465,17 @@ expo-updates-interface@~0.9.0: resolved "https://registry.yarnpkg.com/expo-updates-interface/-/expo-updates-interface-0.9.1.tgz#e81308d551ed5a4c35c8770ac61434f6ca749610" integrity sha512-wk88LLhseQ7LJvxdN7BTKiryyqALxnrvr+lyHK3/prg76Yy0EGi2Q/oE/rtFyyZ1JmQDRbO/5pdX0EE6QqVQXQ== -expo@~48.0.0-beta.2: - version "48.0.7" - resolved "https://registry.yarnpkg.com/expo/-/expo-48.0.7.tgz#7900bfda316d25127ed9c412daa31db66dc4a869" - integrity sha512-4sPW+HWm03z72FKIG9IddwEhF9+RlAUsTh8pnsoZjZbXALVikmV3QjD4zp/Dkt9YuiCAnJN1VBaT2AlhbYk2Rg== +expo@~48.0.9: + version "48.0.9" + resolved "https://registry.yarnpkg.com/expo/-/expo-48.0.9.tgz#7beaecc09e0c364a2c152a0b8bd71060b2d37186" + integrity sha512-RlYpJSny4g3G2sqAfx1taaT7QFEw2cIfYLlZWmguA6EQSCviaeaQU1m4tvVXU1jIXb/w8jqer18XIq56VuECfg== dependencies: "@babel/runtime" "^7.20.0" "@expo/cli" "0.6.2" "@expo/config" "8.0.2" "@expo/config-plugins" "6.0.1" "@expo/vector-icons" "^13.0.0" - babel-preset-expo "~9.3.0" + babel-preset-expo "~9.3.1" cross-spawn "^6.0.5" expo-application "~5.1.1" expo-asset "~8.9.1" @@ -7430,7 +7484,7 @@ expo@~48.0.0-beta.2: expo-font "~11.1.1" expo-keep-awake "~12.0.1" expo-modules-autolinking "1.1.2" - expo-modules-core "1.2.5" + expo-modules-core "1.2.6" fbemitter "^3.0.0" getenv "^1.0.0" invariant "^2.2.4" @@ -7444,7 +7498,7 @@ express-async-errors@^3.1.1: resolved "https://registry.yarnpkg.com/express-async-errors/-/express-async-errors-3.1.1.tgz#6053236d61d21ddef4892d6bd1d736889fc9da41" integrity sha512-h6aK1da4tpqWSbyCa3FxB/V6Ehd4EEB15zyQq9qe75OZBp0krinNKuH4rAY+S/U/2I36vdLAUFSjQJ+TFmODng== -express@^4.17.2, express@^4.17.3: +express@^4.17.2, express@^4.17.3, express@^4.18.2: version "4.18.2" resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== @@ -7573,6 +7627,11 @@ fast-redact@^3.1.1: resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.1.2.tgz#d58e69e9084ce9fa4c1a6fa98a3e1ecf5d7839aa" integrity sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw== +fast-text-encoding@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz#0aa25f7f638222e3396d72bf936afcf1d42d6867" + integrity sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w== + fast-xml-parser@^4.0.12: version "4.1.3" resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.1.3.tgz#0254ad0d4d27f07e6b48254b068c0c137488dd97" @@ -7817,7 +7876,7 @@ flow-parser@^0.185.0: resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.185.2.tgz#cb7ee57f77377d6c5d69a469e980f6332a15e492" integrity sha512-2hJ5ACYeJCzNtiVULov6pljKOLygy0zddoqSI1fFetM+XRPpRshFdGEijtqlamA1XwyZ+7rhryI6FQFzvtLWUQ== -follow-redirects@^1.0.0, follow-redirects@^1.14.4, follow-redirects@^1.14.9: +follow-redirects@^1.0.0, follow-redirects@^1.14.4, follow-redirects@^1.15.0: version "1.15.2" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== @@ -8235,6 +8294,11 @@ grapheme-splitter@^1.0.4: resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + graphql-tag@^2.10.1: version "2.12.6" resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.12.6.tgz#d441a569c1d2537ef10ca3d1633b48725329b5f1" @@ -8726,7 +8790,7 @@ interpret@^3.1.1: resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4" integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ== -invariant@*, invariant@2.2.4, invariant@^2.2.4: +invariant@*, invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== @@ -9509,7 +9573,7 @@ jest-environment-node@^29.2.1, jest-environment-node@^29.5.0: jest-mock "^29.5.0" jest-util "^29.5.0" -jest-expo@^48.0.0-beta.2: +jest-expo@^48.0.2: version "48.0.2" resolved "https://registry.yarnpkg.com/jest-expo/-/jest-expo-48.0.2.tgz#eedab424e29e9bec2cf17a2fe1a653096ec82b04" integrity sha512-hxppv3I3/WgtswladHpPlcEHCv+5/6OG8nOuR3VqtS0h7ZJYuyQCMpXbsKZiA4R/sT4fHS0BUj9BBsdhrk/zXg== @@ -10504,6 +10568,11 @@ kysely@^0.22.0: resolved "https://registry.yarnpkg.com/kysely/-/kysely-0.22.0.tgz#8aac53942da3cadc604d7d154a746d983fe8f7b9" integrity sha512-ZE3qWtnqLOalodzfK5QUEcm7AEulhxsPNuKaGFsC3XiqO92vMLm+mAHk/NnbSIOtC4RmGm0nsv700i8KDp1gfQ== +kysely@^0.23.4: + version "0.23.5" + resolved "https://registry.yarnpkg.com/kysely/-/kysely-0.23.5.tgz#60c63d94e1c42cc0411be8aaa688a0f27405f514" + integrity sha512-TH+b56pVXQq0tsyooYLeNfV11j6ih7D50dyN8tkM0e7ndiUH28Nziojiog3qRFlmEj9XePYdZUrNJ2079Qjdow== + lande@^1.0.10: version "1.0.10" resolved "https://registry.yarnpkg.com/lande/-/lande-1.0.10.tgz#1f6c6542e628338eb18def22edd1038f5fce9e7a" @@ -10802,7 +10871,7 @@ make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0: dependencies: semver "^6.0.0" -make-error@^1.3.6: +make-error@^1.1.1, make-error@^1.3.6: version "1.3.6" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== @@ -10931,16 +11000,6 @@ methods@~1.1.2: resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== -metro-babel-transformer@0.73.7: - version "0.73.7" - resolved "https://registry.yarnpkg.com/metro-babel-transformer/-/metro-babel-transformer-0.73.7.tgz#561ffa0336eb6d7d112e7128e957114c729fdb71" - integrity sha512-s7UVkwovGTEXYEQrv5hcmSBbFJ9s9lhCRNMScn4Itgj3UMdqRr9lU8DXKEFlJ7osgRxN6n5+eXqcvhE4B1H1VQ== - dependencies: - "@babel/core" "^7.20.0" - hermes-parser "0.8.0" - metro-source-map "0.73.7" - nullthrows "^1.1.1" - metro-babel-transformer@0.73.8: version "0.73.8" resolved "https://registry.yarnpkg.com/metro-babel-transformer/-/metro-babel-transformer-0.73.8.tgz#521374cb9234ba126f3f8d63588db5901308b4ed" @@ -10951,43 +11010,53 @@ metro-babel-transformer@0.73.8: metro-source-map "0.73.8" nullthrows "^1.1.1" -metro-cache-key@0.73.8: - version "0.73.8" - resolved "https://registry.yarnpkg.com/metro-cache-key/-/metro-cache-key-0.73.8.tgz#afc9f63454edbd9d207544445a66e8a4e119462d" - integrity sha512-VzFGu4kJGIkLjyDgVoM2ZxIHlMdCZWMqVIux9N+EeyMVMvGXTiXW8eGROgxzDhVjyR58IjfMsYpRCKz5dR+2ew== +metro-babel-transformer@0.73.9: + version "0.73.9" + resolved "https://registry.yarnpkg.com/metro-babel-transformer/-/metro-babel-transformer-0.73.9.tgz#bec8aaaf1bbdc2e469fde586fde455f8b2a83073" + integrity sha512-DlYwg9wwYIZTHtic7dyD4BP0SDftoltZ3clma76nHu43blMWsCnrImHeHsAVne3XsQ+RJaSRxhN5nkG2VyVHwA== + dependencies: + "@babel/core" "^7.20.0" + hermes-parser "0.8.0" + metro-source-map "0.73.9" + nullthrows "^1.1.1" -metro-cache@0.73.8: - version "0.73.8" - resolved "https://registry.yarnpkg.com/metro-cache/-/metro-cache-0.73.8.tgz#85e2d7f7c7c74d1f942b7ecd168f7aceb987d883" - integrity sha512-/uFbTIw813Rvb8kSAIHvax9gWl41dtgjY2SpJLNIBLdQ6oFZ3CVo3ahZIiEZOrCeHl9xfGn5tmvNb8CEFa/Q5w== +metro-cache-key@0.73.9: + version "0.73.9" + resolved "https://registry.yarnpkg.com/metro-cache-key/-/metro-cache-key-0.73.9.tgz#7d8c441a3b7150f7b201273087ef3cf7d3435d9f" + integrity sha512-uJg+6Al7UoGIuGfoxqPBy6y1Ewq7Y8/YapGYIDh6sohInwt/kYKnPZgLDYHIPvY2deORnQ/2CYo4tOeBTnhCXQ== + +metro-cache@0.73.9: + version "0.73.9" + resolved "https://registry.yarnpkg.com/metro-cache/-/metro-cache-0.73.9.tgz#773c2df6ba53434e58ccbe421b0c54e6da8d2890" + integrity sha512-upiRxY8rrQkUWj7ieACD6tna7xXuXdu2ZqrheksT79ePI0aN/t0memf6WcyUtJUMHZetke3j+ppELNvlmp3tOw== dependencies: - metro-core "0.73.8" + metro-core "0.73.9" rimraf "^3.0.2" -metro-config@0.73.8: - version "0.73.8" - resolved "https://registry.yarnpkg.com/metro-config/-/metro-config-0.73.8.tgz#8f6c22c94528919635c6688ed8d2ad8a10c70b27" - integrity sha512-sAYq+llL6ZAfro64U99ske8HcKKswxX4wIZbll9niBKG7TkWm7tfMY1jO687XEmE4683rHncZeBRav9pLngIzg== +metro-config@0.73.9: + version "0.73.9" + resolved "https://registry.yarnpkg.com/metro-config/-/metro-config-0.73.9.tgz#6b43c70681bdd6b00f44400fc76dddbe53374500" + integrity sha512-NiWl1nkYtjqecDmw77tbRbXnzIAwdO6DXGZTuKSkH+H/c1NKq1eizO8Fe+NQyFtwR9YLqn8Q0WN1nmkwM1j8CA== dependencies: cosmiconfig "^5.0.5" jest-validate "^26.5.2" - metro "0.73.8" - metro-cache "0.73.8" - metro-core "0.73.8" - metro-runtime "0.73.8" + metro "0.73.9" + metro-cache "0.73.9" + metro-core "0.73.9" + metro-runtime "0.73.9" -metro-core@0.73.8: - version "0.73.8" - resolved "https://registry.yarnpkg.com/metro-core/-/metro-core-0.73.8.tgz#a31ba7d7bfe3f4c2ac2c7a2493aa4229ecad701e" - integrity sha512-Aew4dthbZf8bRRjlYGL3cnai3+LKYTf6mc7YS2xLQRWtgGZ1b/H8nQtBvXZpfRYFcS84UeEQ10vwIf5eR3qPdQ== +metro-core@0.73.9: + version "0.73.9" + resolved "https://registry.yarnpkg.com/metro-core/-/metro-core-0.73.9.tgz#410c5c0aeae840536c10039f68098fdab3da568e" + integrity sha512-1NTs0IErlKcFTfYyRT3ljdgrISWpl1nys+gaHkXapzTSpvtX9F1NQNn5cgAuE+XIuTJhbsCdfIJiM2JXbrJQaQ== dependencies: lodash.throttle "^4.1.1" - metro-resolver "0.73.8" + metro-resolver "0.73.9" -metro-file-map@0.73.8: - version "0.73.8" - resolved "https://registry.yarnpkg.com/metro-file-map/-/metro-file-map-0.73.8.tgz#88d666e7764e1b0adf5fd634d91e97e3135d2db7" - integrity sha512-CM552hUO9om02jJdLszOCIDADKNaaeVz8CjYXItndvgr5jmFlQYAR+UMvaDzeT8oYdAV1DXAljma2CS2UBymPg== +metro-file-map@0.73.9: + version "0.73.9" + resolved "https://registry.yarnpkg.com/metro-file-map/-/metro-file-map-0.73.9.tgz#09c04a8e8ef1eaa6ecb2b9cb8cb53bb0fa0167ec" + integrity sha512-R/Wg3HYeQhYY3ehWtfedw8V0ne4lpufG7a21L3GWer8tafnC9pmjoCKEbJz9XZkVj9i1FtxE7UTbrtZNeIILxQ== dependencies: abort-controller "^3.0.0" anymatch "^3.0.3" @@ -11005,39 +11074,39 @@ metro-file-map@0.73.8: optionalDependencies: fsevents "^2.3.2" -metro-hermes-compiler@0.73.8: - version "0.73.8" - resolved "https://registry.yarnpkg.com/metro-hermes-compiler/-/metro-hermes-compiler-0.73.8.tgz#c522e2c97afc8bdc249755d88146a75720bc2498" - integrity sha512-2d7t+TEoQLk+jyXgBykmAtPPJK2B46DB3qUYIMKDFDDaKzCljrojyVuGgQq6SM1f95fe6HDAQ3K9ihTjeB90yw== +metro-hermes-compiler@0.73.9: + version "0.73.9" + resolved "https://registry.yarnpkg.com/metro-hermes-compiler/-/metro-hermes-compiler-0.73.9.tgz#6f473e67e8f76066066f00e2e0ecce865f7d445d" + integrity sha512-5B3vXIwQkZMSh3DQQY23XpTCpX9kPLqZbA3rDuAcbGW0tzC3f8dCenkyBb0GcCzyTDncJeot/A7oVCVK6zapwg== -metro-inspector-proxy@0.73.8: - version "0.73.8" - resolved "https://registry.yarnpkg.com/metro-inspector-proxy/-/metro-inspector-proxy-0.73.8.tgz#67d5aadfc33fe97f61c716eb168db4bd5d0e3c96" - integrity sha512-F0QxwDTox0TDeXVRN7ZmI7BknBjPDVKQ1ZeKznFBiMa0SXiD1kzoksfpDbZ6hTEKrhVM9Ep0YQmC7avwZouOnA== +metro-inspector-proxy@0.73.9: + version "0.73.9" + resolved "https://registry.yarnpkg.com/metro-inspector-proxy/-/metro-inspector-proxy-0.73.9.tgz#8e11cd300adf3f904f1f5afe28b198312cdcd8c2" + integrity sha512-B3WrWZnlYhtTrv0IaX3aUAhi2qVILPAZQzb5paO1e+xrz4YZHk9c7dXv7qe7B/IQ132e3w46y3AL7rFo90qVjA== dependencies: connect "^3.6.5" debug "^2.2.0" ws "^7.5.1" yargs "^17.5.1" -metro-minify-terser@0.73.8: - version "0.73.8" - resolved "https://registry.yarnpkg.com/metro-minify-terser/-/metro-minify-terser-0.73.8.tgz#a0fe857d6aaf99cba3a2aef59ee06ac409682c6b" - integrity sha512-pnagyXAoMPhihWrHRIWqCxrP6EJ8Hfugv5RXBb6HbOANmwajn2uQuzeu18+dXaN1yPoDCMCgpg/UA4ibFN5jtQ== +metro-minify-terser@0.73.9: + version "0.73.9" + resolved "https://registry.yarnpkg.com/metro-minify-terser/-/metro-minify-terser-0.73.9.tgz#301aef2e106b0802f7a14ef0f2b4883b20c80018" + integrity sha512-MTGPu2qV5qtzPJ2SqH6s58awHDtZ4jd7lmmLR+7TXDwtZDjIBA0YVfI0Zak2Haby2SqoNKrhhUns/b4dPAQAVg== dependencies: terser "^5.15.0" -metro-minify-uglify@0.73.8: - version "0.73.8" - resolved "https://registry.yarnpkg.com/metro-minify-uglify/-/metro-minify-uglify-0.73.8.tgz#b2e2430014c340479db4fc393a2ea4c5bad75ecd" - integrity sha512-9wZqKfraVfmtMXdOzRyan+6r1woQXqqa4KeXfVh7+Mxl+5+J0Lmw6EvTrWawsaOEpvpn32q9MfoHC1d8plDJwA== +metro-minify-uglify@0.73.9: + version "0.73.9" + resolved "https://registry.yarnpkg.com/metro-minify-uglify/-/metro-minify-uglify-0.73.9.tgz#cf4f8c19b688deea103905689ec736c2f2acd733" + integrity sha512-gzxD/7WjYcnCNGiFJaA26z34rjOp+c/Ft++194Wg91lYep3TeWQ0CnH8t2HRS7AYDHU81SGWgvD3U7WV0g4LGA== dependencies: uglify-es "^3.1.9" -metro-react-native-babel-preset@0.73.7: - version "0.73.7" - resolved "https://registry.yarnpkg.com/metro-react-native-babel-preset/-/metro-react-native-babel-preset-0.73.7.tgz#78e1ce448aa9a5cf3651c0ebe73cb225465211b4" - integrity sha512-RKcmRZREjJCzHKP+JhC9QTCohkeb3xa/DtqHU14U5KWzJHdC0mMrkTZYNXhV0cryxsaVKVEw5873KhbZyZHMVw== +metro-react-native-babel-preset@0.73.8, metro-react-native-babel-preset@^0.73.7: + version "0.73.8" + resolved "https://registry.yarnpkg.com/metro-react-native-babel-preset/-/metro-react-native-babel-preset-0.73.8.tgz#04908f264f5d99c944ae20b5b11f659431328431" + integrity sha512-spNrcQJTbQntEIqJnCA6yL4S+dzV9fXCk7U+Rm7yJasZ4o4Frn7jP23isu7FlZIp1Azx1+6SbP7SgQM+IP5JgQ== dependencies: "@babel/core" "^7.20.0" "@babel/plugin-proposal-async-generator-functions" "^7.0.0" @@ -11078,10 +11147,10 @@ metro-react-native-babel-preset@0.73.7: "@babel/template" "^7.0.0" react-refresh "^0.4.0" -metro-react-native-babel-preset@0.73.8, metro-react-native-babel-preset@^0.73.7: - version "0.73.8" - resolved "https://registry.yarnpkg.com/metro-react-native-babel-preset/-/metro-react-native-babel-preset-0.73.8.tgz#04908f264f5d99c944ae20b5b11f659431328431" - integrity sha512-spNrcQJTbQntEIqJnCA6yL4S+dzV9fXCk7U+Rm7yJasZ4o4Frn7jP23isu7FlZIp1Azx1+6SbP7SgQM+IP5JgQ== +metro-react-native-babel-preset@0.73.9: + version "0.73.9" + resolved "https://registry.yarnpkg.com/metro-react-native-babel-preset/-/metro-react-native-babel-preset-0.73.9.tgz#ef54637dd20f025197beb49e71309a9c539e73e2" + integrity sha512-AoD7v132iYDV4K78yN2OLgTPwtAKn0XlD2pOhzyBxiI8PeXzozhbKyPV7zUOJUPETj+pcEVfuYj5ZN/8+bhbCw== dependencies: "@babel/core" "^7.20.0" "@babel/plugin-proposal-async-generator-functions" "^7.0.0" @@ -11122,19 +11191,6 @@ metro-react-native-babel-preset@0.73.8, metro-react-native-babel-preset@^0.73.7: "@babel/template" "^7.0.0" react-refresh "^0.4.0" -metro-react-native-babel-transformer@0.73.7: - version "0.73.7" - resolved "https://registry.yarnpkg.com/metro-react-native-babel-transformer/-/metro-react-native-babel-transformer-0.73.7.tgz#a92055fd564cd403255cc34f925c5e99ce457565" - integrity sha512-73HW8betjX+VPm3iqsMBe8F/F2Tt+hONO6YJwcF7FonTqQYW1oTz0dOp0dClZGfHUXxpJBz6Vuo7J6TpdzDD+w== - dependencies: - "@babel/core" "^7.20.0" - babel-preset-fbjs "^3.4.0" - hermes-parser "0.8.0" - metro-babel-transformer "0.73.7" - metro-react-native-babel-preset "0.73.7" - metro-source-map "0.73.7" - nullthrows "^1.1.1" - metro-react-native-babel-transformer@0.73.8: version "0.73.8" resolved "https://registry.yarnpkg.com/metro-react-native-babel-transformer/-/metro-react-native-babel-transformer-0.73.8.tgz#cbcd4b243216878431dc4311ce46f02a928e3991" @@ -11148,20 +11204,25 @@ metro-react-native-babel-transformer@0.73.8: metro-source-map "0.73.8" nullthrows "^1.1.1" -metro-resolver@0.73.8: - version "0.73.8" - resolved "https://registry.yarnpkg.com/metro-resolver/-/metro-resolver-0.73.8.tgz#65cc158575d130363296f66a33257c7971228640" - integrity sha512-GiBWont7/OgAftkkj2TiEp+Gf1PYZUk8xV4MbtnQjIKyy3MlGY3GbpMQ1BHih9GUQqlF0n9jsUlC2K5P0almXQ== +metro-react-native-babel-transformer@0.73.9: + version "0.73.9" + resolved "https://registry.yarnpkg.com/metro-react-native-babel-transformer/-/metro-react-native-babel-transformer-0.73.9.tgz#4f4f0cfa5119bab8b53e722fabaf90687d0cbff0" + integrity sha512-DSdrEHuQ22ixY7DyipyKkIcqhOJrt5s6h6X7BYJCP9AMUfXOwLe2biY3BcgJz5GOXv8/Akry4vTCvQscVS1otQ== dependencies: - absolute-path "^0.0.0" + "@babel/core" "^7.20.0" + babel-preset-fbjs "^3.4.0" + hermes-parser "0.8.0" + metro-babel-transformer "0.73.9" + metro-react-native-babel-preset "0.73.9" + metro-source-map "0.73.9" + nullthrows "^1.1.1" -metro-runtime@0.73.7: - version "0.73.7" - resolved "https://registry.yarnpkg.com/metro-runtime/-/metro-runtime-0.73.7.tgz#9f3a7f3ff668c1a87370650e32b47d8f6329fd1e" - integrity sha512-2fxRGrF8FyrwwHY0TCitdUljzutfW6CWEpdvPilfrs8p0PI5X8xOWg8ficeYtw+DldHtHIAL2phT59PqzHTyVA== +metro-resolver@0.73.9: + version "0.73.9" + resolved "https://registry.yarnpkg.com/metro-resolver/-/metro-resolver-0.73.9.tgz#f3cf77e6c7606a34aa81bad40edb856aad671cf3" + integrity sha512-Ej3wAPOeNRPDnJmkK0zk7vJ33iU07n+oPhpcf5L0NFkWneMmSM2bflMPibI86UjzZGmRfn0AhGhs8yGeBwQ/Xg== dependencies: - "@babel/runtime" "^7.0.0" - react-refresh "^0.4.0" + absolute-path "^0.0.0" metro-runtime@0.73.8: version "0.73.8" @@ -11171,19 +11232,13 @@ metro-runtime@0.73.8: "@babel/runtime" "^7.0.0" react-refresh "^0.4.0" -metro-source-map@0.73.7: - version "0.73.7" - resolved "https://registry.yarnpkg.com/metro-source-map/-/metro-source-map-0.73.7.tgz#8e9f850a72d60ea7ace05b984f981c8ec843e7a0" - integrity sha512-gbC/lfUN52TtQhEsTTA+987MaFUpQlufuCI05blLGLosDcFCsARikHsxa65Gtslm/rG2MqvFLiPA5hviONNv9g== +metro-runtime@0.73.9: + version "0.73.9" + resolved "https://registry.yarnpkg.com/metro-runtime/-/metro-runtime-0.73.9.tgz#0b24c0b066b8629ee855a6e5035b65061fef60d5" + integrity sha512-d5Hs83FpKB9r8q8Vb95+fa6ESpwysmPr4lL1I2rM2qXAFiO7OAPT9Bc23WmXgidkBtD0uUFdB2lG+H1ATz8rZg== dependencies: - "@babel/traverse" "^7.20.0" - "@babel/types" "^7.20.0" - invariant "^2.2.4" - metro-symbolicate "0.73.7" - nullthrows "^1.1.1" - ob1 "0.73.7" - source-map "^0.5.6" - vlq "^1.0.0" + "@babel/runtime" "^7.0.0" + react-refresh "^0.4.0" metro-source-map@0.73.8: version "0.73.8" @@ -11199,16 +11254,18 @@ metro-source-map@0.73.8: source-map "^0.5.6" vlq "^1.0.0" -metro-symbolicate@0.73.7: - version "0.73.7" - resolved "https://registry.yarnpkg.com/metro-symbolicate/-/metro-symbolicate-0.73.7.tgz#40e4cda81f8030b86afe391b5e686a0b06822b0a" - integrity sha512-571ThWmX5o8yGNzoXjlcdhmXqpByHU/bSZtWKhtgV2TyIAzYCYt4hawJAS5+/qDazUvjHdm8BbdqFUheM0EKNQ== +metro-source-map@0.73.9: + version "0.73.9" + resolved "https://registry.yarnpkg.com/metro-source-map/-/metro-source-map-0.73.9.tgz#89ca41f6346aeb12f7f23496fa363e520adafebe" + integrity sha512-l4VZKzdqafipriETYR6lsrwtavCF1+CMhCOY9XbyWeTrpGSNgJQgdeJpttzEZTHQQTLR0csQo0nD1ef3zEP6IQ== dependencies: + "@babel/traverse" "^7.20.0" + "@babel/types" "^7.20.0" invariant "^2.2.4" - metro-source-map "0.73.7" + metro-symbolicate "0.73.9" nullthrows "^1.1.1" + ob1 "0.73.9" source-map "^0.5.6" - through2 "^2.0.1" vlq "^1.0.0" metro-symbolicate@0.73.8: @@ -11223,10 +11280,22 @@ metro-symbolicate@0.73.8: through2 "^2.0.1" vlq "^1.0.0" -metro-transform-plugins@0.73.8: - version "0.73.8" - resolved "https://registry.yarnpkg.com/metro-transform-plugins/-/metro-transform-plugins-0.73.8.tgz#07be7fd94a448ea1b245ab02ce7d277d757f9a32" - integrity sha512-IxjlnB5eA49M0WfvPEzvRikK3Rr6bECUUfcZt/rWpSphq/mttgyLYcHQ+VTZZl0zHolC3cTLwgoDod4IIJBn1A== +metro-symbolicate@0.73.9: + version "0.73.9" + resolved "https://registry.yarnpkg.com/metro-symbolicate/-/metro-symbolicate-0.73.9.tgz#cb452299a36e5b86b2826e7426d51221635c48bf" + integrity sha512-4TUOwxRHHqbEHxRqRJ3wZY5TA8xq7AHMtXrXcjegMH9FscgYztsrIG9aNBUBS+VLB6g1qc6BYbfIgoAnLjCDyw== + dependencies: + invariant "^2.2.4" + metro-source-map "0.73.9" + nullthrows "^1.1.1" + source-map "^0.5.6" + through2 "^2.0.1" + vlq "^1.0.0" + +metro-transform-plugins@0.73.9: + version "0.73.9" + resolved "https://registry.yarnpkg.com/metro-transform-plugins/-/metro-transform-plugins-0.73.9.tgz#9fffbe1b24269e3d114286fa681abc570072d9b8" + integrity sha512-r9NeiqMngmooX2VOKLJVQrMuV7PAydbqst5bFhdVBPcFpZkxxqyzjzo+kzrszGy2UpSQBZr2P1L6OMjLHwQwfQ== dependencies: "@babel/core" "^7.20.0" "@babel/generator" "^7.20.0" @@ -11234,29 +11303,29 @@ metro-transform-plugins@0.73.8: "@babel/traverse" "^7.20.0" nullthrows "^1.1.1" -metro-transform-worker@0.73.8: - version "0.73.8" - resolved "https://registry.yarnpkg.com/metro-transform-worker/-/metro-transform-worker-0.73.8.tgz#701a006c2b4d93f1bb24802f3f2834c963153db9" - integrity sha512-B8kR6lmcvyG4UFSF2QDfr/eEnWJvg0ZadooF8Dg6m/3JSm9OAqfSoC0YrWqAuvtWImNDnbeKWN7/+ns44Hv6tg== +metro-transform-worker@0.73.9: + version "0.73.9" + resolved "https://registry.yarnpkg.com/metro-transform-worker/-/metro-transform-worker-0.73.9.tgz#30384cef2d5e35a4abe91b15bf1a8344f5720441" + integrity sha512-Rq4b489sIaTUENA+WCvtu9yvlT/C6zFMWhU4sq+97W29Zj0mPBjdk+qGT5n1ZBgtBIJzZWt1KxeYuc17f4aYtQ== dependencies: "@babel/core" "^7.20.0" "@babel/generator" "^7.20.0" "@babel/parser" "^7.20.0" "@babel/types" "^7.20.0" babel-preset-fbjs "^3.4.0" - metro "0.73.8" - metro-babel-transformer "0.73.8" - metro-cache "0.73.8" - metro-cache-key "0.73.8" - metro-hermes-compiler "0.73.8" - metro-source-map "0.73.8" - metro-transform-plugins "0.73.8" + metro "0.73.9" + metro-babel-transformer "0.73.9" + metro-cache "0.73.9" + metro-cache-key "0.73.9" + metro-hermes-compiler "0.73.9" + metro-source-map "0.73.9" + metro-transform-plugins "0.73.9" nullthrows "^1.1.1" -metro@0.73.8: - version "0.73.8" - resolved "https://registry.yarnpkg.com/metro/-/metro-0.73.8.tgz#25f014e4064eb34a4833c316e0a9094528061a8c" - integrity sha512-2EMJME9w5x7Uzn+DnQ4hzWr33u/aASaOBGdpf4lxbrlk6/vl4UBfX1sru6KU535qc/0Z1BMt4Vq9qsP3ZGFmWg== +metro@0.73.9: + version "0.73.9" + resolved "https://registry.yarnpkg.com/metro/-/metro-0.73.9.tgz#150e69a6735fab0bcb4f6ee97fd1efc65b3ec36f" + integrity sha512-BlYbPmTF60hpetyNdKhdvi57dSqutb+/oK0u3ni4emIh78PiI0axGo7RfdsZ/mn3saASXc94tDbpC5yn7+NpEg== dependencies: "@babel/code-frame" "^7.0.0" "@babel/core" "^7.20.0" @@ -11280,23 +11349,23 @@ metro@0.73.8: invariant "^2.2.4" jest-worker "^27.2.0" lodash.throttle "^4.1.1" - metro-babel-transformer "0.73.8" - metro-cache "0.73.8" - metro-cache-key "0.73.8" - metro-config "0.73.8" - metro-core "0.73.8" - metro-file-map "0.73.8" - metro-hermes-compiler "0.73.8" - metro-inspector-proxy "0.73.8" - metro-minify-terser "0.73.8" - metro-minify-uglify "0.73.8" - metro-react-native-babel-preset "0.73.8" - metro-resolver "0.73.8" - metro-runtime "0.73.8" - metro-source-map "0.73.8" - metro-symbolicate "0.73.8" - metro-transform-plugins "0.73.8" - metro-transform-worker "0.73.8" + metro-babel-transformer "0.73.9" + metro-cache "0.73.9" + metro-cache-key "0.73.9" + metro-config "0.73.9" + metro-core "0.73.9" + metro-file-map "0.73.9" + metro-hermes-compiler "0.73.9" + metro-inspector-proxy "0.73.9" + metro-minify-terser "0.73.9" + metro-minify-uglify "0.73.9" + metro-react-native-babel-preset "0.73.9" + metro-resolver "0.73.9" + metro-runtime "0.73.9" + metro-source-map "0.73.9" + metro-symbolicate "0.73.9" + metro-transform-plugins "0.73.9" + metro-transform-worker "0.73.9" mime-types "^2.1.27" node-fetch "^2.2.0" nullthrows "^1.1.1" @@ -11796,16 +11865,16 @@ nwsapi@^2.2.0, nwsapi@^2.2.2: resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.2.tgz#e5418863e7905df67d51ec95938d67bf801f0bb0" integrity sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw== -ob1@0.73.7: - version "0.73.7" - resolved "https://registry.yarnpkg.com/ob1/-/ob1-0.73.7.tgz#14c9b6ddc26cf99144f59eb542d7ae956e6b3192" - integrity sha512-DfelfvR843KADhSUATGGhuepVMRcf5VQX+6MQLy5AW0BKDLlO7Usj6YZeAAZP7P86QwsoTxB0RXCFiA7t6S1IQ== - ob1@0.73.8: version "0.73.8" resolved "https://registry.yarnpkg.com/ob1/-/ob1-0.73.8.tgz#c569f1a15ce2d04da6fd70293ad44b5a93b11978" integrity sha512-1F7j+jzD+edS6ohQP7Vg5f3yiIk5i3x1uLrNIHOmLHWzWK1t3zrDpjnoXghccdVlsU+UjbyURnDynm4p0GgXeA== +ob1@0.73.9: + version "0.73.9" + resolved "https://registry.yarnpkg.com/ob1/-/ob1-0.73.9.tgz#d5677a0dd3e2f16ad84231278d79424436c38c59" + integrity sha512-kHOzCOFXmAM26fy7V/YuXNKne2TyRiXbFAvPBIbuedJCZZWQZHLdPzMeXJI4Egt6IcfDttRzN3jQ90wOwq1iNw== + object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -12358,7 +12427,7 @@ pg-types@^2.1.0: postgres-date "~1.0.4" postgres-interval "^1.1.0" -pg@^8.8.0: +pg@^8.8.0, pg@^8.9.0: version "8.10.0" resolved "https://registry.yarnpkg.com/pg/-/pg-8.10.0.tgz#5b8379c9b4a36451d110fc8cd98fc325fe62ad24" integrity sha512-ke7o7qSTMb47iwzOSaZMfeR7xToFdkE71ifIipOAAaLIM0DYzfOAXlgFFmYUIE2BcJtvnVlGCID84ZzCegE8CQ== @@ -12423,7 +12492,7 @@ pino-abstract-transport@v1.0.0: readable-stream "^4.0.0" split2 "^4.0.0" -pino-http@^8.2.1: +pino-http@^8.2.1, pino-http@^8.3.3: version "8.3.3" resolved "https://registry.yarnpkg.com/pino-http/-/pino-http-8.3.3.tgz#2b140e734bfc6babe0df272a43bb8f36f2b525c0" integrity sha512-p4umsNIXXVu95HD2C8wie/vXH7db5iGRpc+yj1/ZQ3sRtTQLXNjoS6Be5+eI+rQbqCRxen/7k/KSN+qiZubGDw== @@ -12438,7 +12507,7 @@ pino-std-serializers@^6.0.0: resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-6.1.0.tgz#307490fd426eefc95e06067e85d8558603e8e844" integrity sha512-KO0m2f1HkrPe9S0ldjx7za9BJjeHqBku5Ch8JyxETxT8dEFGz1PwgrHaOQupVYitpzbFSYm7nnljxD8dik2c+g== -pino@^8.0.0, pino@^8.6.1: +pino@^8.0.0, pino@^8.11.0, pino@^8.6.1: version "8.11.0" resolved "https://registry.yarnpkg.com/pino/-/pino-8.11.0.tgz#2a91f454106b13e708a66c74ebc1c2ab7ab38498" integrity sha512-Z2eKSvlrl2rH8p5eveNUnTdd4AjJk8tAsLkHYZQKGHP4WTh2Gi1cOSOs3eWPqaj+niS3gj4UkoreoaWgF3ZWYg== @@ -13406,6 +13475,11 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + pseudomap@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" @@ -13451,13 +13525,6 @@ qs@6.11.0: dependencies: side-channel "^1.0.4" -qs@^6.5.1: - version "6.11.1" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.1.tgz#6c29dff97f0c0060765911ba65cbc9764186109f" - integrity sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ== - dependencies: - side-channel "^1.0.4" - query-string@^7.1.3: version "7.1.3" resolved "https://registry.yarnpkg.com/query-string/-/query-string-7.1.3.tgz#a1cf90e994abb113a325804a972d98276fe02328" @@ -13699,7 +13766,7 @@ react-native-get-random-values@^1.8.0: dependencies: fast-base64-decode "^1.0.0" -react-native-gradle-plugin@^0.71.15: +react-native-gradle-plugin@^0.71.16: version "0.71.16" resolved "https://registry.yarnpkg.com/react-native-gradle-plugin/-/react-native-gradle-plugin-0.71.16.tgz#822bb0c680e03b5df5aa65f2e5ffc2bc2930854a" integrity sha512-H2BjG2zk7B7Wii9sXvd9qhCVRQYDAHSWdMw9tscmZBqSP62DkIWEQSk4/B2GhQ4aK9ydVXgtqR6tBeg3yy8TSA== @@ -13804,13 +13871,6 @@ react-native-web-linear-gradient@^1.1.2: resolved "https://registry.yarnpkg.com/react-native-web-linear-gradient/-/react-native-web-linear-gradient-1.1.2.tgz#33f85f7085a0bb5ffa5106faf02ed105b92a9ed7" integrity sha512-SmUnpwT49CEe78pXvIvYf72Es8Pv+ZYKCnEOgb2zAKpEUDMo0+xElfRJhwt5nfI8krJ5WbFPKnoDgD0uUjAN1A== -react-native-web-webview@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/react-native-web-webview/-/react-native-web-webview-1.0.2.tgz#c215efa70c17589f2c8d640b1f1dc669b18c6e02" - integrity sha512-oNAYNuqUqeqTuAAdIejzDqvUtYA+k5lrvhUYmASdUznZNmyIaoQFA6OKoA4K9F3wdMvark42vUXkUWIp875ewg== - dependencies: - qs "^6.5.1" - react-native-web@^0.18.11: version "0.18.12" resolved "https://registry.yarnpkg.com/react-native-web/-/react-native-web-0.18.12.tgz#d4bb3a783ece2514ba0508d7805b09c0a98f5a8e" @@ -13824,30 +13884,15 @@ react-native-web@^0.18.11: postcss-value-parser "^4.2.0" styleq "^0.1.2" -react-native-webview@11.26.0: - version "11.26.0" - resolved "https://registry.yarnpkg.com/react-native-webview/-/react-native-webview-11.26.0.tgz#e524992876fe4a79e69905f0fab8949b470e9f16" - integrity sha512-4T4CKRm8xlaQDz9h/bCMPGAvtkesrhkRWqCX9FDJEzBToaVUIsV0ZOqtC4w/JSnCtFKKYiaC1ReJtCGv+4mFeQ== - dependencies: - escape-string-regexp "2.0.0" - invariant "2.2.4" - -react-native-youtube-iframe@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/react-native-youtube-iframe/-/react-native-youtube-iframe-2.2.2.tgz#ade1a2e4ead3d539fbb80463f45b59ff1b510b55" - integrity sha512-og2KW21kCwAHKcnWoyWWBYC6J2Xtqjjwpghhoy9G6zfwZkr8Ej27BbQIAKM/TheJJUZ5/YUrqsgqAdnFYDx5TQ== - dependencies: - events "^3.2.0" - -react-native@0.71.3: - version "0.71.3" - resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.71.3.tgz#0faab799c49e61ba12df9e6525c3ac7d595d673c" - integrity sha512-RYJXCcQGa4NTfKiPgl92eRDUuQ6JGDnHqFEzRwJSqEx9lWvlvRRIebstJfurzPDKLQWQrvITR7aI7e09E25mLw== +react-native@0.71.4: + version "0.71.4" + resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.71.4.tgz#f03f600efe68f745d19454ab17f9c1a9ef304790" + integrity sha512-3hSYqvWrOdKhpV3HpEKp1/CkWx8Sr/N/miCrmUIAsVTSJUR7JW0VvIsrV9urDhUj/s6v2WF4n7qIEEJsmTCrPw== dependencies: "@jest/create-cache-key-function" "^29.2.1" - "@react-native-community/cli" "10.1.3" - "@react-native-community/cli-platform-android" "10.1.3" - "@react-native-community/cli-platform-ios" "10.1.1" + "@react-native-community/cli" "10.2.0" + "@react-native-community/cli-platform-android" "10.2.0" + "@react-native-community/cli-platform-ios" "10.2.0" "@react-native/assets" "1.0.0" "@react-native/normalize-color" "2.1.0" "@react-native/polyfills" "2.0.0" @@ -13860,16 +13905,16 @@ react-native@0.71.3: jest-environment-node "^29.2.1" jsc-android "^250231.0.0" memoize-one "^5.0.0" - metro-react-native-babel-transformer "0.73.7" - metro-runtime "0.73.7" - metro-source-map "0.73.7" + metro-react-native-babel-transformer "0.73.8" + metro-runtime "0.73.8" + metro-source-map "0.73.8" mkdirp "^0.5.1" nullthrows "^1.1.1" pretty-format "^26.5.2" promise "^8.3.0" react-devtools-core "^4.26.1" react-native-codegen "^0.71.5" - react-native-gradle-plugin "^0.71.15" + react-native-gradle-plugin "^0.71.16" react-refresh "^0.4.0" react-shallow-renderer "^16.15.0" regenerator-runtime "^0.13.2" @@ -15771,6 +15816,25 @@ ts-interface-checker@^0.1.9: resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== +ts-node@^10.9.1: + version "10.9.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" + integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + tsconfig-paths@^3.14.1: version "3.14.2" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz#6e32f1f79412decd261f92d633a9dc1cfa99f088" @@ -16164,6 +16228,11 @@ uuid@^9.0.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + v8-to-istanbul@^8.1.0: version "8.1.1" resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz#77b752fd3975e31bbcef938f85e9bd1c7a8d60ed" @@ -16990,6 +17059,11 @@ yargs@^17.3.1, yargs@^17.5.1: y18n "^5.0.5" yargs-parser "^21.1.1" +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" |