Lex refactor (#362)
* Remove the hackcheck for upgrades * Rename the PostEmbeds folder to match the codebase style * Updates to latest lex refactor * Update to use new bsky agent * Update to use api package's richtext library * Switch to upsertProfile * Add TextEncoder/TextDecoder polyfill * Add Intl.Segmenter polyfill * Update composer to calculate lengths by grapheme * Fix detox * Fix login in e2e * Create account e2e passing * Implement an e2e mocking framework * Don't use private methods on mobx models as mobx can't track them * Add tooling for e2e-specific builds and add e2e media-picker mock * Add some tests and fix some bugs around profile editing * Add shell tests * Add home screen tests * Add thread screen tests * Add tests for other user profile screens * Add search screen tests * Implement profile imagery change tools and tests * Update to new embed behaviors * Add post tests * Fix to profile-screen test * Fix session resumption * Update web composer to new api * 1.11.0 * Fix pagination cursor parameters * Add quote posts to notifications * Fix embed layouts * Remove youtube inline player and improve tap handling on link cards * Reset minimal shell mode on all screen loads and feed swipes (close #299) * Update podfile.lock * Improve post notfound UI (close #366) * Bump atproto packageszio/stable
parent
19f3a2fa92
commit
a3334a01a2
11
.detoxrc.js
11
.detoxrc.js
|
@ -3,7 +3,7 @@ module.exports = {
|
||||||
testRunner: {
|
testRunner: {
|
||||||
args: {
|
args: {
|
||||||
$0: 'jest',
|
$0: 'jest',
|
||||||
config: 'e2e/jest.config.js',
|
config: '__e2e__/jest.config.js',
|
||||||
},
|
},
|
||||||
jest: {
|
jest: {
|
||||||
setupTimeout: 120000,
|
setupTimeout: 120000,
|
||||||
|
@ -12,15 +12,16 @@ module.exports = {
|
||||||
apps: {
|
apps: {
|
||||||
'ios.debug': {
|
'ios.debug': {
|
||||||
type: 'ios.app',
|
type: 'ios.app',
|
||||||
binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/app.app',
|
binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/bluesky.app',
|
||||||
build:
|
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': {
|
'ios.release': {
|
||||||
type: 'ios.app',
|
type: 'ios.app',
|
||||||
binaryPath: 'ios/build/Build/Products/Release-iphonesimulator/app.app',
|
binaryPath:
|
||||||
|
'ios/build/Build/Products/Release-iphonesimulator/bluesky.app',
|
||||||
build:
|
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': {
|
'android.debug': {
|
||||||
type: 'android.apk',
|
type: 'android.apk',
|
||||||
|
|
|
@ -18,6 +18,12 @@
|
||||||
- iOS: `yarn ios`
|
- iOS: `yarn ios`
|
||||||
- Android: `yarn android`
|
- Android: `yarn android`
|
||||||
- Web: `yarn web`
|
- 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
|
- Tips
|
||||||
- `npx react-native info` Checks what has been installed.
|
- `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)
|
- On M1 macs, [you need to exclude "arm64" from the target architectures](https://stackoverflow.com/a/65399525)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/** @type {import('@jest/types').Config.InitialOptions} */
|
/** @type {import('@jest/types').Config.InitialOptions} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
rootDir: '..',
|
rootDir: '..',
|
||||||
testMatch: ['<rootDir>/e2e/**/*.test.js'],
|
testMatch: ['<rootDir>/__e2e__/**/*.test.ts'],
|
||||||
testTimeout: 120000,
|
testTimeout: 120000,
|
||||||
maxWorkers: 1,
|
maxWorkers: 1,
|
||||||
globalSetup: 'detox/runners/jest/globalSetup',
|
globalSetup: 'detox/runners/jest/globalSetup',
|
|
@ -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()
|
|
@ -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()
|
||||||
|
})
|
||||||
|
})
|
|
@ -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()
|
||||||
|
})
|
||||||
|
})
|
|
@ -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()
|
||||||
|
})
|
||||||
|
})
|
|
@ -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')
|
||||||
|
})
|
||||||
|
})
|
|
@ -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()
|
||||||
|
})
|
||||||
|
})
|
|
@ -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()
|
||||||
|
})
|
||||||
|
})
|
|
@ -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()
|
||||||
|
})
|
||||||
|
})
|
|
@ -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()
|
||||||
|
})
|
||||||
|
})
|
|
@ -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))
|
|
@ -4,14 +4,14 @@ import {
|
||||||
getLikelyType,
|
getLikelyType,
|
||||||
} from '../../src/lib/link-meta/link-meta'
|
} from '../../src/lib/link-meta/link-meta'
|
||||||
import {exampleComHtml} from './__mocks__/exampleComHtml'
|
import {exampleComHtml} from './__mocks__/exampleComHtml'
|
||||||
import AtpAgent from '@atproto/api'
|
import {BskyAgent} from '@atproto/api'
|
||||||
import {DEFAULT_SERVICE, RootStoreModel} from '../../src/state'
|
import {DEFAULT_SERVICE, RootStoreModel} from '../../src/state'
|
||||||
|
|
||||||
describe('getLinkMeta', () => {
|
describe('getLinkMeta', () => {
|
||||||
let rootStore: RootStoreModel
|
let rootStore: RootStoreModel
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
rootStore = new RootStoreModel(new AtpAgent({service: DEFAULT_SERVICE}))
|
rootStore = new RootStoreModel(new BskyAgent({service: DEFAULT_SERVICE}))
|
||||||
})
|
})
|
||||||
|
|
||||||
const inputs = [
|
const inputs = [
|
||||||
|
|
|
@ -7,172 +7,10 @@ import {
|
||||||
} from '../../src/lib/strings/url-helpers'
|
} from '../../src/lib/strings/url-helpers'
|
||||||
import {pluralize, enforceLen} from '../../src/lib/strings/helpers'
|
import {pluralize, enforceLen} from '../../src/lib/strings/helpers'
|
||||||
import {ago} from '../../src/lib/strings/time'
|
import {ago} from '../../src/lib/strings/time'
|
||||||
import {
|
import {detectLinkables} from '../../src/lib/strings/rich-text-detection'
|
||||||
extractEntities,
|
|
||||||
detectLinkables,
|
|
||||||
} from '../../src/lib/strings/rich-text-detection'
|
|
||||||
import {makeValidHandle, createFullHandle} from '../../src/lib/strings/handles'
|
import {makeValidHandle, createFullHandle} from '../../src/lib/strings/handles'
|
||||||
import {cleanError} from '../../src/lib/strings/errors'
|
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', () => {
|
describe('detectLinkables', () => {
|
||||||
const inputs = [
|
const inputs = [
|
||||||
'no linkable',
|
'no linkable',
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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')
|
|
||||||
})
|
|
||||||
})
|
|
2
app.json
2
app.json
|
@ -2,7 +2,7 @@
|
||||||
"expo": {
|
"expo": {
|
||||||
"name": "bluesky",
|
"name": "bluesky",
|
||||||
"slug": "bluesky",
|
"slug": "bluesky",
|
||||||
"version": "1.10.0",
|
"version": "1.11.0",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"icon": "./assets/icon.png",
|
"icon": "./assets/icon.png",
|
||||||
"userInterfaceStyle": "light",
|
"userInterfaceStyle": "light",
|
||||||
|
|
|
@ -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()
|
|
||||||
})
|
|
||||||
})
|
|
466
ios/Podfile.lock
466
ios/Podfile.lock
|
@ -22,7 +22,7 @@ PODS:
|
||||||
- EXMediaLibrary (15.2.3):
|
- EXMediaLibrary (15.2.3):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- React-Core
|
- React-Core
|
||||||
- Expo (48.0.7):
|
- Expo (48.0.9):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- expo-dev-client (2.1.5):
|
- expo-dev-client (2.1.5):
|
||||||
- EXManifests
|
- EXManifests
|
||||||
|
@ -102,7 +102,7 @@ PODS:
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- ExpoLocalization (14.1.1):
|
- ExpoLocalization (14.1.1):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- ExpoModulesCore (1.2.5):
|
- ExpoModulesCore (1.2.6):
|
||||||
- React-Core
|
- React-Core
|
||||||
- React-RCTAppDelegate
|
- React-RCTAppDelegate
|
||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
|
@ -110,19 +110,19 @@ PODS:
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- React-Core
|
- React-Core
|
||||||
- EXUpdatesInterface (0.9.1)
|
- EXUpdatesInterface (0.9.1)
|
||||||
- FBLazyVector (0.71.3)
|
- FBLazyVector (0.71.4)
|
||||||
- FBReactNativeSpec (0.71.3):
|
- FBReactNativeSpec (0.71.4):
|
||||||
- RCT-Folly (= 2021.07.22.00)
|
- RCT-Folly (= 2021.07.22.00)
|
||||||
- RCTRequired (= 0.71.3)
|
- RCTRequired (= 0.71.4)
|
||||||
- RCTTypeSafety (= 0.71.3)
|
- RCTTypeSafety (= 0.71.4)
|
||||||
- React-Core (= 0.71.3)
|
- React-Core (= 0.71.4)
|
||||||
- React-jsi (= 0.71.3)
|
- React-jsi (= 0.71.4)
|
||||||
- ReactCommon/turbomodule/core (= 0.71.3)
|
- ReactCommon/turbomodule/core (= 0.71.4)
|
||||||
- fmt (6.2.1)
|
- fmt (6.2.1)
|
||||||
- glog (0.3.5)
|
- glog (0.3.5)
|
||||||
- hermes-engine (0.71.3):
|
- hermes-engine (0.71.4):
|
||||||
- hermes-engine/Pre-built (= 0.71.3)
|
- hermes-engine/Pre-built (= 0.71.4)
|
||||||
- hermes-engine/Pre-built (0.71.3)
|
- hermes-engine/Pre-built (0.71.4)
|
||||||
- libevent (2.1.12)
|
- libevent (2.1.12)
|
||||||
- libwebp (1.2.4):
|
- libwebp (1.2.4):
|
||||||
- libwebp/demux (= 1.2.4)
|
- libwebp/demux (= 1.2.4)
|
||||||
|
@ -150,26 +150,26 @@ PODS:
|
||||||
- fmt (~> 6.2.1)
|
- fmt (~> 6.2.1)
|
||||||
- glog
|
- glog
|
||||||
- libevent
|
- libevent
|
||||||
- RCTRequired (0.71.3)
|
- RCTRequired (0.71.4)
|
||||||
- RCTTypeSafety (0.71.3):
|
- RCTTypeSafety (0.71.4):
|
||||||
- FBLazyVector (= 0.71.3)
|
- FBLazyVector (= 0.71.4)
|
||||||
- RCTRequired (= 0.71.3)
|
- RCTRequired (= 0.71.4)
|
||||||
- React-Core (= 0.71.3)
|
- React-Core (= 0.71.4)
|
||||||
- React (0.71.3):
|
- React (0.71.4):
|
||||||
- React-Core (= 0.71.3)
|
- React-Core (= 0.71.4)
|
||||||
- React-Core/DevSupport (= 0.71.3)
|
- React-Core/DevSupport (= 0.71.4)
|
||||||
- React-Core/RCTWebSocket (= 0.71.3)
|
- React-Core/RCTWebSocket (= 0.71.4)
|
||||||
- React-RCTActionSheet (= 0.71.3)
|
- React-RCTActionSheet (= 0.71.4)
|
||||||
- React-RCTAnimation (= 0.71.3)
|
- React-RCTAnimation (= 0.71.4)
|
||||||
- React-RCTBlob (= 0.71.3)
|
- React-RCTBlob (= 0.71.4)
|
||||||
- React-RCTImage (= 0.71.3)
|
- React-RCTImage (= 0.71.4)
|
||||||
- React-RCTLinking (= 0.71.3)
|
- React-RCTLinking (= 0.71.4)
|
||||||
- React-RCTNetwork (= 0.71.3)
|
- React-RCTNetwork (= 0.71.4)
|
||||||
- React-RCTSettings (= 0.71.3)
|
- React-RCTSettings (= 0.71.4)
|
||||||
- React-RCTText (= 0.71.3)
|
- React-RCTText (= 0.71.4)
|
||||||
- React-RCTVibration (= 0.71.3)
|
- React-RCTVibration (= 0.71.4)
|
||||||
- React-callinvoker (0.71.3)
|
- React-callinvoker (0.71.4)
|
||||||
- React-Codegen (0.71.3):
|
- React-Codegen (0.71.4):
|
||||||
- FBReactNativeSpec
|
- FBReactNativeSpec
|
||||||
- hermes-engine
|
- hermes-engine
|
||||||
- RCT-Folly
|
- RCT-Folly
|
||||||
|
@ -180,209 +180,209 @@ PODS:
|
||||||
- React-jsiexecutor
|
- React-jsiexecutor
|
||||||
- ReactCommon/turbomodule/bridging
|
- ReactCommon/turbomodule/bridging
|
||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- React-Core (0.71.3):
|
- React-Core (0.71.4):
|
||||||
- glog
|
- glog
|
||||||
- hermes-engine
|
- hermes-engine
|
||||||
- RCT-Folly (= 2021.07.22.00)
|
- RCT-Folly (= 2021.07.22.00)
|
||||||
- React-Core/Default (= 0.71.3)
|
- React-Core/Default (= 0.71.4)
|
||||||
- React-cxxreact (= 0.71.3)
|
- React-cxxreact (= 0.71.4)
|
||||||
- React-hermes
|
- React-hermes
|
||||||
- React-jsi (= 0.71.3)
|
- React-jsi (= 0.71.4)
|
||||||
- React-jsiexecutor (= 0.71.3)
|
- React-jsiexecutor (= 0.71.4)
|
||||||
- React-perflogger (= 0.71.3)
|
- React-perflogger (= 0.71.4)
|
||||||
- Yoga
|
- Yoga
|
||||||
- React-Core/CoreModulesHeaders (0.71.3):
|
- React-Core/CoreModulesHeaders (0.71.4):
|
||||||
- glog
|
- glog
|
||||||
- hermes-engine
|
- hermes-engine
|
||||||
- RCT-Folly (= 2021.07.22.00)
|
- RCT-Folly (= 2021.07.22.00)
|
||||||
- React-Core/Default
|
- React-Core/Default
|
||||||
- React-cxxreact (= 0.71.3)
|
- React-cxxreact (= 0.71.4)
|
||||||
- React-hermes
|
- React-hermes
|
||||||
- React-jsi (= 0.71.3)
|
- React-jsi (= 0.71.4)
|
||||||
- React-jsiexecutor (= 0.71.3)
|
- React-jsiexecutor (= 0.71.4)
|
||||||
- React-perflogger (= 0.71.3)
|
- React-perflogger (= 0.71.4)
|
||||||
- Yoga
|
- Yoga
|
||||||
- React-Core/Default (0.71.3):
|
- React-Core/Default (0.71.4):
|
||||||
- glog
|
- glog
|
||||||
- hermes-engine
|
- hermes-engine
|
||||||
- RCT-Folly (= 2021.07.22.00)
|
- RCT-Folly (= 2021.07.22.00)
|
||||||
- React-cxxreact (= 0.71.3)
|
- React-cxxreact (= 0.71.4)
|
||||||
- React-hermes
|
- React-hermes
|
||||||
- React-jsi (= 0.71.3)
|
- React-jsi (= 0.71.4)
|
||||||
- React-jsiexecutor (= 0.71.3)
|
- React-jsiexecutor (= 0.71.4)
|
||||||
- React-perflogger (= 0.71.3)
|
- React-perflogger (= 0.71.4)
|
||||||
- Yoga
|
- Yoga
|
||||||
- React-Core/DevSupport (0.71.3):
|
- React-Core/DevSupport (0.71.4):
|
||||||
- glog
|
- glog
|
||||||
- hermes-engine
|
- hermes-engine
|
||||||
- RCT-Folly (= 2021.07.22.00)
|
- RCT-Folly (= 2021.07.22.00)
|
||||||
- React-Core/Default (= 0.71.3)
|
- React-Core/Default (= 0.71.4)
|
||||||
- React-Core/RCTWebSocket (= 0.71.3)
|
- React-Core/RCTWebSocket (= 0.71.4)
|
||||||
- React-cxxreact (= 0.71.3)
|
- React-cxxreact (= 0.71.4)
|
||||||
- React-hermes
|
- React-hermes
|
||||||
- React-jsi (= 0.71.3)
|
- React-jsi (= 0.71.4)
|
||||||
- React-jsiexecutor (= 0.71.3)
|
- React-jsiexecutor (= 0.71.4)
|
||||||
- React-jsinspector (= 0.71.3)
|
- React-jsinspector (= 0.71.4)
|
||||||
- React-perflogger (= 0.71.3)
|
- React-perflogger (= 0.71.4)
|
||||||
- Yoga
|
- Yoga
|
||||||
- React-Core/RCTActionSheetHeaders (0.71.3):
|
- React-Core/RCTActionSheetHeaders (0.71.4):
|
||||||
- glog
|
- glog
|
||||||
- hermes-engine
|
- hermes-engine
|
||||||
- RCT-Folly (= 2021.07.22.00)
|
- RCT-Folly (= 2021.07.22.00)
|
||||||
- React-Core/Default
|
- React-Core/Default
|
||||||
- React-cxxreact (= 0.71.3)
|
- React-cxxreact (= 0.71.4)
|
||||||
- React-hermes
|
- React-hermes
|
||||||
- React-jsi (= 0.71.3)
|
- React-jsi (= 0.71.4)
|
||||||
- React-jsiexecutor (= 0.71.3)
|
- React-jsiexecutor (= 0.71.4)
|
||||||
- React-perflogger (= 0.71.3)
|
- React-perflogger (= 0.71.4)
|
||||||
- Yoga
|
- Yoga
|
||||||
- React-Core/RCTAnimationHeaders (0.71.3):
|
- React-Core/RCTAnimationHeaders (0.71.4):
|
||||||
- glog
|
- glog
|
||||||
- hermes-engine
|
- hermes-engine
|
||||||
- RCT-Folly (= 2021.07.22.00)
|
- RCT-Folly (= 2021.07.22.00)
|
||||||
- React-Core/Default
|
- React-Core/Default
|
||||||
- React-cxxreact (= 0.71.3)
|
- React-cxxreact (= 0.71.4)
|
||||||
- React-hermes
|
- React-hermes
|
||||||
- React-jsi (= 0.71.3)
|
- React-jsi (= 0.71.4)
|
||||||
- React-jsiexecutor (= 0.71.3)
|
- React-jsiexecutor (= 0.71.4)
|
||||||
- React-perflogger (= 0.71.3)
|
- React-perflogger (= 0.71.4)
|
||||||
- Yoga
|
- Yoga
|
||||||
- React-Core/RCTBlobHeaders (0.71.3):
|
- React-Core/RCTBlobHeaders (0.71.4):
|
||||||
- glog
|
- glog
|
||||||
- hermes-engine
|
- hermes-engine
|
||||||
- RCT-Folly (= 2021.07.22.00)
|
- RCT-Folly (= 2021.07.22.00)
|
||||||
- React-Core/Default
|
- React-Core/Default
|
||||||
- React-cxxreact (= 0.71.3)
|
- React-cxxreact (= 0.71.4)
|
||||||
- React-hermes
|
- React-hermes
|
||||||
- React-jsi (= 0.71.3)
|
- React-jsi (= 0.71.4)
|
||||||
- React-jsiexecutor (= 0.71.3)
|
- React-jsiexecutor (= 0.71.4)
|
||||||
- React-perflogger (= 0.71.3)
|
- React-perflogger (= 0.71.4)
|
||||||
- Yoga
|
- Yoga
|
||||||
- React-Core/RCTImageHeaders (0.71.3):
|
- React-Core/RCTImageHeaders (0.71.4):
|
||||||
- glog
|
- glog
|
||||||
- hermes-engine
|
- hermes-engine
|
||||||
- RCT-Folly (= 2021.07.22.00)
|
- RCT-Folly (= 2021.07.22.00)
|
||||||
- React-Core/Default
|
- React-Core/Default
|
||||||
- React-cxxreact (= 0.71.3)
|
- React-cxxreact (= 0.71.4)
|
||||||
- React-hermes
|
- React-hermes
|
||||||
- React-jsi (= 0.71.3)
|
- React-jsi (= 0.71.4)
|
||||||
- React-jsiexecutor (= 0.71.3)
|
- React-jsiexecutor (= 0.71.4)
|
||||||
- React-perflogger (= 0.71.3)
|
- React-perflogger (= 0.71.4)
|
||||||
- Yoga
|
- Yoga
|
||||||
- React-Core/RCTLinkingHeaders (0.71.3):
|
- React-Core/RCTLinkingHeaders (0.71.4):
|
||||||
- glog
|
- glog
|
||||||
- hermes-engine
|
- hermes-engine
|
||||||
- RCT-Folly (= 2021.07.22.00)
|
- RCT-Folly (= 2021.07.22.00)
|
||||||
- React-Core/Default
|
- React-Core/Default
|
||||||
- React-cxxreact (= 0.71.3)
|
- React-cxxreact (= 0.71.4)
|
||||||
- React-hermes
|
- React-hermes
|
||||||
- React-jsi (= 0.71.3)
|
- React-jsi (= 0.71.4)
|
||||||
- React-jsiexecutor (= 0.71.3)
|
- React-jsiexecutor (= 0.71.4)
|
||||||
- React-perflogger (= 0.71.3)
|
- React-perflogger (= 0.71.4)
|
||||||
- Yoga
|
- Yoga
|
||||||
- React-Core/RCTNetworkHeaders (0.71.3):
|
- React-Core/RCTNetworkHeaders (0.71.4):
|
||||||
- glog
|
- glog
|
||||||
- hermes-engine
|
- hermes-engine
|
||||||
- RCT-Folly (= 2021.07.22.00)
|
- RCT-Folly (= 2021.07.22.00)
|
||||||
- React-Core/Default
|
- React-Core/Default
|
||||||
- React-cxxreact (= 0.71.3)
|
- React-cxxreact (= 0.71.4)
|
||||||
- React-hermes
|
- React-hermes
|
||||||
- React-jsi (= 0.71.3)
|
- React-jsi (= 0.71.4)
|
||||||
- React-jsiexecutor (= 0.71.3)
|
- React-jsiexecutor (= 0.71.4)
|
||||||
- React-perflogger (= 0.71.3)
|
- React-perflogger (= 0.71.4)
|
||||||
- Yoga
|
- Yoga
|
||||||
- React-Core/RCTSettingsHeaders (0.71.3):
|
- React-Core/RCTSettingsHeaders (0.71.4):
|
||||||
- glog
|
- glog
|
||||||
- hermes-engine
|
- hermes-engine
|
||||||
- RCT-Folly (= 2021.07.22.00)
|
- RCT-Folly (= 2021.07.22.00)
|
||||||
- React-Core/Default
|
- React-Core/Default
|
||||||
- React-cxxreact (= 0.71.3)
|
- React-cxxreact (= 0.71.4)
|
||||||
- React-hermes
|
- React-hermes
|
||||||
- React-jsi (= 0.71.3)
|
- React-jsi (= 0.71.4)
|
||||||
- React-jsiexecutor (= 0.71.3)
|
- React-jsiexecutor (= 0.71.4)
|
||||||
- React-perflogger (= 0.71.3)
|
- React-perflogger (= 0.71.4)
|
||||||
- Yoga
|
- Yoga
|
||||||
- React-Core/RCTTextHeaders (0.71.3):
|
- React-Core/RCTTextHeaders (0.71.4):
|
||||||
- glog
|
- glog
|
||||||
- hermes-engine
|
- hermes-engine
|
||||||
- RCT-Folly (= 2021.07.22.00)
|
- RCT-Folly (= 2021.07.22.00)
|
||||||
- React-Core/Default
|
- React-Core/Default
|
||||||
- React-cxxreact (= 0.71.3)
|
- React-cxxreact (= 0.71.4)
|
||||||
- React-hermes
|
- React-hermes
|
||||||
- React-jsi (= 0.71.3)
|
- React-jsi (= 0.71.4)
|
||||||
- React-jsiexecutor (= 0.71.3)
|
- React-jsiexecutor (= 0.71.4)
|
||||||
- React-perflogger (= 0.71.3)
|
- React-perflogger (= 0.71.4)
|
||||||
- Yoga
|
- Yoga
|
||||||
- React-Core/RCTVibrationHeaders (0.71.3):
|
- React-Core/RCTVibrationHeaders (0.71.4):
|
||||||
- glog
|
- glog
|
||||||
- hermes-engine
|
- hermes-engine
|
||||||
- RCT-Folly (= 2021.07.22.00)
|
- RCT-Folly (= 2021.07.22.00)
|
||||||
- React-Core/Default
|
- React-Core/Default
|
||||||
- React-cxxreact (= 0.71.3)
|
- React-cxxreact (= 0.71.4)
|
||||||
- React-hermes
|
- React-hermes
|
||||||
- React-jsi (= 0.71.3)
|
- React-jsi (= 0.71.4)
|
||||||
- React-jsiexecutor (= 0.71.3)
|
- React-jsiexecutor (= 0.71.4)
|
||||||
- React-perflogger (= 0.71.3)
|
- React-perflogger (= 0.71.4)
|
||||||
- Yoga
|
- Yoga
|
||||||
- React-Core/RCTWebSocket (0.71.3):
|
- React-Core/RCTWebSocket (0.71.4):
|
||||||
- glog
|
- glog
|
||||||
- hermes-engine
|
- hermes-engine
|
||||||
- RCT-Folly (= 2021.07.22.00)
|
- RCT-Folly (= 2021.07.22.00)
|
||||||
- React-Core/Default (= 0.71.3)
|
- React-Core/Default (= 0.71.4)
|
||||||
- React-cxxreact (= 0.71.3)
|
- React-cxxreact (= 0.71.4)
|
||||||
- React-hermes
|
- React-hermes
|
||||||
- React-jsi (= 0.71.3)
|
- React-jsi (= 0.71.4)
|
||||||
- React-jsiexecutor (= 0.71.3)
|
- React-jsiexecutor (= 0.71.4)
|
||||||
- React-perflogger (= 0.71.3)
|
- React-perflogger (= 0.71.4)
|
||||||
- Yoga
|
- Yoga
|
||||||
- React-CoreModules (0.71.3):
|
- React-CoreModules (0.71.4):
|
||||||
- RCT-Folly (= 2021.07.22.00)
|
- RCT-Folly (= 2021.07.22.00)
|
||||||
- RCTTypeSafety (= 0.71.3)
|
- RCTTypeSafety (= 0.71.4)
|
||||||
- React-Codegen (= 0.71.3)
|
- React-Codegen (= 0.71.4)
|
||||||
- React-Core/CoreModulesHeaders (= 0.71.3)
|
- React-Core/CoreModulesHeaders (= 0.71.4)
|
||||||
- React-jsi (= 0.71.3)
|
- React-jsi (= 0.71.4)
|
||||||
- React-RCTBlob
|
- React-RCTBlob
|
||||||
- React-RCTImage (= 0.71.3)
|
- React-RCTImage (= 0.71.4)
|
||||||
- ReactCommon/turbomodule/core (= 0.71.3)
|
- ReactCommon/turbomodule/core (= 0.71.4)
|
||||||
- React-cxxreact (0.71.3):
|
- React-cxxreact (0.71.4):
|
||||||
- boost (= 1.76.0)
|
- boost (= 1.76.0)
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
- glog
|
- glog
|
||||||
- hermes-engine
|
- hermes-engine
|
||||||
- RCT-Folly (= 2021.07.22.00)
|
- RCT-Folly (= 2021.07.22.00)
|
||||||
- React-callinvoker (= 0.71.3)
|
- React-callinvoker (= 0.71.4)
|
||||||
- React-jsi (= 0.71.3)
|
- React-jsi (= 0.71.4)
|
||||||
- React-jsinspector (= 0.71.3)
|
- React-jsinspector (= 0.71.4)
|
||||||
- React-logger (= 0.71.3)
|
- React-logger (= 0.71.4)
|
||||||
- React-perflogger (= 0.71.3)
|
- React-perflogger (= 0.71.4)
|
||||||
- React-runtimeexecutor (= 0.71.3)
|
- React-runtimeexecutor (= 0.71.4)
|
||||||
- React-hermes (0.71.3):
|
- React-hermes (0.71.4):
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
- glog
|
- glog
|
||||||
- hermes-engine
|
- hermes-engine
|
||||||
- RCT-Folly (= 2021.07.22.00)
|
- RCT-Folly (= 2021.07.22.00)
|
||||||
- RCT-Folly/Futures (= 2021.07.22.00)
|
- RCT-Folly/Futures (= 2021.07.22.00)
|
||||||
- React-cxxreact (= 0.71.3)
|
- React-cxxreact (= 0.71.4)
|
||||||
- React-jsi
|
- React-jsi
|
||||||
- React-jsiexecutor (= 0.71.3)
|
- React-jsiexecutor (= 0.71.4)
|
||||||
- React-jsinspector (= 0.71.3)
|
- React-jsinspector (= 0.71.4)
|
||||||
- React-perflogger (= 0.71.3)
|
- React-perflogger (= 0.71.4)
|
||||||
- React-jsi (0.71.3):
|
- React-jsi (0.71.4):
|
||||||
- boost (= 1.76.0)
|
- boost (= 1.76.0)
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
- glog
|
- glog
|
||||||
- hermes-engine
|
- hermes-engine
|
||||||
- RCT-Folly (= 2021.07.22.00)
|
- RCT-Folly (= 2021.07.22.00)
|
||||||
- React-jsiexecutor (0.71.3):
|
- React-jsiexecutor (0.71.4):
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
- glog
|
- glog
|
||||||
- hermes-engine
|
- hermes-engine
|
||||||
- RCT-Folly (= 2021.07.22.00)
|
- RCT-Folly (= 2021.07.22.00)
|
||||||
- React-cxxreact (= 0.71.3)
|
- React-cxxreact (= 0.71.4)
|
||||||
- React-jsi (= 0.71.3)
|
- React-jsi (= 0.71.4)
|
||||||
- React-perflogger (= 0.71.3)
|
- React-perflogger (= 0.71.4)
|
||||||
- React-jsinspector (0.71.3)
|
- React-jsinspector (0.71.4)
|
||||||
- React-logger (0.71.3):
|
- React-logger (0.71.4):
|
||||||
- glog
|
- glog
|
||||||
- react-native-blur (4.3.0):
|
- react-native-blur (4.3.0):
|
||||||
- React-Core
|
- React-Core
|
||||||
|
@ -407,92 +407,90 @@ PODS:
|
||||||
- React-Core
|
- React-Core
|
||||||
- react-native-version-number (0.3.6):
|
- react-native-version-number (0.3.6):
|
||||||
- React
|
- React
|
||||||
- react-native-webview (11.26.0):
|
- React-perflogger (0.71.4)
|
||||||
- React-Core
|
- React-RCTActionSheet (0.71.4):
|
||||||
- React-perflogger (0.71.3)
|
- React-Core/RCTActionSheetHeaders (= 0.71.4)
|
||||||
- React-RCTActionSheet (0.71.3):
|
- React-RCTAnimation (0.71.4):
|
||||||
- React-Core/RCTActionSheetHeaders (= 0.71.3)
|
|
||||||
- React-RCTAnimation (0.71.3):
|
|
||||||
- RCT-Folly (= 2021.07.22.00)
|
- RCT-Folly (= 2021.07.22.00)
|
||||||
- RCTTypeSafety (= 0.71.3)
|
- RCTTypeSafety (= 0.71.4)
|
||||||
- React-Codegen (= 0.71.3)
|
- React-Codegen (= 0.71.4)
|
||||||
- React-Core/RCTAnimationHeaders (= 0.71.3)
|
- React-Core/RCTAnimationHeaders (= 0.71.4)
|
||||||
- React-jsi (= 0.71.3)
|
- React-jsi (= 0.71.4)
|
||||||
- ReactCommon/turbomodule/core (= 0.71.3)
|
- ReactCommon/turbomodule/core (= 0.71.4)
|
||||||
- React-RCTAppDelegate (0.71.3):
|
- React-RCTAppDelegate (0.71.4):
|
||||||
- RCT-Folly
|
- RCT-Folly
|
||||||
- RCTRequired
|
- RCTRequired
|
||||||
- RCTTypeSafety
|
- RCTTypeSafety
|
||||||
- React-Core
|
- React-Core
|
||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- React-RCTBlob (0.71.3):
|
- React-RCTBlob (0.71.4):
|
||||||
- hermes-engine
|
- hermes-engine
|
||||||
- RCT-Folly (= 2021.07.22.00)
|
- RCT-Folly (= 2021.07.22.00)
|
||||||
- React-Codegen (= 0.71.3)
|
- React-Codegen (= 0.71.4)
|
||||||
- React-Core/RCTBlobHeaders (= 0.71.3)
|
- React-Core/RCTBlobHeaders (= 0.71.4)
|
||||||
- React-Core/RCTWebSocket (= 0.71.3)
|
- React-Core/RCTWebSocket (= 0.71.4)
|
||||||
- React-jsi (= 0.71.3)
|
- React-jsi (= 0.71.4)
|
||||||
- React-RCTNetwork (= 0.71.3)
|
- React-RCTNetwork (= 0.71.4)
|
||||||
- ReactCommon/turbomodule/core (= 0.71.3)
|
- ReactCommon/turbomodule/core (= 0.71.4)
|
||||||
- React-RCTImage (0.71.3):
|
- React-RCTImage (0.71.4):
|
||||||
- RCT-Folly (= 2021.07.22.00)
|
- RCT-Folly (= 2021.07.22.00)
|
||||||
- RCTTypeSafety (= 0.71.3)
|
- RCTTypeSafety (= 0.71.4)
|
||||||
- React-Codegen (= 0.71.3)
|
- React-Codegen (= 0.71.4)
|
||||||
- React-Core/RCTImageHeaders (= 0.71.3)
|
- React-Core/RCTImageHeaders (= 0.71.4)
|
||||||
- React-jsi (= 0.71.3)
|
- React-jsi (= 0.71.4)
|
||||||
- React-RCTNetwork (= 0.71.3)
|
- React-RCTNetwork (= 0.71.4)
|
||||||
- ReactCommon/turbomodule/core (= 0.71.3)
|
- ReactCommon/turbomodule/core (= 0.71.4)
|
||||||
- React-RCTLinking (0.71.3):
|
- React-RCTLinking (0.71.4):
|
||||||
- React-Codegen (= 0.71.3)
|
- React-Codegen (= 0.71.4)
|
||||||
- React-Core/RCTLinkingHeaders (= 0.71.3)
|
- React-Core/RCTLinkingHeaders (= 0.71.4)
|
||||||
- React-jsi (= 0.71.3)
|
- React-jsi (= 0.71.4)
|
||||||
- ReactCommon/turbomodule/core (= 0.71.3)
|
- ReactCommon/turbomodule/core (= 0.71.4)
|
||||||
- React-RCTNetwork (0.71.3):
|
- React-RCTNetwork (0.71.4):
|
||||||
- RCT-Folly (= 2021.07.22.00)
|
- RCT-Folly (= 2021.07.22.00)
|
||||||
- RCTTypeSafety (= 0.71.3)
|
- RCTTypeSafety (= 0.71.4)
|
||||||
- React-Codegen (= 0.71.3)
|
- React-Codegen (= 0.71.4)
|
||||||
- React-Core/RCTNetworkHeaders (= 0.71.3)
|
- React-Core/RCTNetworkHeaders (= 0.71.4)
|
||||||
- React-jsi (= 0.71.3)
|
- React-jsi (= 0.71.4)
|
||||||
- ReactCommon/turbomodule/core (= 0.71.3)
|
- ReactCommon/turbomodule/core (= 0.71.4)
|
||||||
- React-RCTSettings (0.71.3):
|
- React-RCTSettings (0.71.4):
|
||||||
- RCT-Folly (= 2021.07.22.00)
|
- RCT-Folly (= 2021.07.22.00)
|
||||||
- RCTTypeSafety (= 0.71.3)
|
- RCTTypeSafety (= 0.71.4)
|
||||||
- React-Codegen (= 0.71.3)
|
- React-Codegen (= 0.71.4)
|
||||||
- React-Core/RCTSettingsHeaders (= 0.71.3)
|
- React-Core/RCTSettingsHeaders (= 0.71.4)
|
||||||
- React-jsi (= 0.71.3)
|
- React-jsi (= 0.71.4)
|
||||||
- ReactCommon/turbomodule/core (= 0.71.3)
|
- ReactCommon/turbomodule/core (= 0.71.4)
|
||||||
- React-RCTText (0.71.3):
|
- React-RCTText (0.71.4):
|
||||||
- React-Core/RCTTextHeaders (= 0.71.3)
|
- React-Core/RCTTextHeaders (= 0.71.4)
|
||||||
- React-RCTVibration (0.71.3):
|
- React-RCTVibration (0.71.4):
|
||||||
- RCT-Folly (= 2021.07.22.00)
|
- RCT-Folly (= 2021.07.22.00)
|
||||||
- React-Codegen (= 0.71.3)
|
- React-Codegen (= 0.71.4)
|
||||||
- React-Core/RCTVibrationHeaders (= 0.71.3)
|
- React-Core/RCTVibrationHeaders (= 0.71.4)
|
||||||
- React-jsi (= 0.71.3)
|
- React-jsi (= 0.71.4)
|
||||||
- ReactCommon/turbomodule/core (= 0.71.3)
|
- ReactCommon/turbomodule/core (= 0.71.4)
|
||||||
- React-runtimeexecutor (0.71.3):
|
- React-runtimeexecutor (0.71.4):
|
||||||
- React-jsi (= 0.71.3)
|
- React-jsi (= 0.71.4)
|
||||||
- ReactCommon/turbomodule/bridging (0.71.3):
|
- ReactCommon/turbomodule/bridging (0.71.4):
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
- glog
|
- glog
|
||||||
- hermes-engine
|
- hermes-engine
|
||||||
- RCT-Folly (= 2021.07.22.00)
|
- RCT-Folly (= 2021.07.22.00)
|
||||||
- React-callinvoker (= 0.71.3)
|
- React-callinvoker (= 0.71.4)
|
||||||
- React-Core (= 0.71.3)
|
- React-Core (= 0.71.4)
|
||||||
- React-cxxreact (= 0.71.3)
|
- React-cxxreact (= 0.71.4)
|
||||||
- React-jsi (= 0.71.3)
|
- React-jsi (= 0.71.4)
|
||||||
- React-logger (= 0.71.3)
|
- React-logger (= 0.71.4)
|
||||||
- React-perflogger (= 0.71.3)
|
- React-perflogger (= 0.71.4)
|
||||||
- ReactCommon/turbomodule/core (0.71.3):
|
- ReactCommon/turbomodule/core (0.71.4):
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
- glog
|
- glog
|
||||||
- hermes-engine
|
- hermes-engine
|
||||||
- RCT-Folly (= 2021.07.22.00)
|
- RCT-Folly (= 2021.07.22.00)
|
||||||
- React-callinvoker (= 0.71.3)
|
- React-callinvoker (= 0.71.4)
|
||||||
- React-Core (= 0.71.3)
|
- React-Core (= 0.71.4)
|
||||||
- React-cxxreact (= 0.71.3)
|
- React-cxxreact (= 0.71.4)
|
||||||
- React-jsi (= 0.71.3)
|
- React-jsi (= 0.71.4)
|
||||||
- React-logger (= 0.71.3)
|
- React-logger (= 0.71.4)
|
||||||
- React-perflogger (= 0.71.3)
|
- React-perflogger (= 0.71.4)
|
||||||
- rn-fetch-blob (0.12.0):
|
- rn-fetch-blob (0.12.0):
|
||||||
- React-Core
|
- React-Core
|
||||||
- RNBackgroundFetch (4.1.9):
|
- RNBackgroundFetch (4.1.9):
|
||||||
|
@ -627,7 +625,6 @@ DEPENDENCIES:
|
||||||
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
|
- 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-splash-screen (from `../node_modules/react-native-splash-screen`)
|
||||||
- react-native-version-number (from `../node_modules/react-native-version-number`)
|
- 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-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
|
||||||
- React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`)
|
- React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`)
|
||||||
- React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`)
|
- React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`)
|
||||||
|
@ -770,8 +767,6 @@ EXTERNAL SOURCES:
|
||||||
:path: "../node_modules/react-native-splash-screen"
|
:path: "../node_modules/react-native-splash-screen"
|
||||||
react-native-version-number:
|
react-native-version-number:
|
||||||
:path: "../node_modules/react-native-version-number"
|
:path: "../node_modules/react-native-version-number"
|
||||||
react-native-webview:
|
|
||||||
:path: "../node_modules/react-native-webview"
|
|
||||||
React-perflogger:
|
React-perflogger:
|
||||||
:path: "../node_modules/react-native/ReactCommon/reactperflogger"
|
:path: "../node_modules/react-native/ReactCommon/reactperflogger"
|
||||||
React-RCTActionSheet:
|
React-RCTActionSheet:
|
||||||
|
@ -846,38 +841,38 @@ SPEC CHECKSUMS:
|
||||||
EXJSONUtils: 48b1e764ac35160e6f54d21ab60d7d9501f3e473
|
EXJSONUtils: 48b1e764ac35160e6f54d21ab60d7d9501f3e473
|
||||||
EXManifests: 500666d48e8dd7ca5a482c9e729e4a7a6c34081b
|
EXManifests: 500666d48e8dd7ca5a482c9e729e4a7a6c34081b
|
||||||
EXMediaLibrary: 587cd8aad27a6fc8d7c38b950bc75bc1845a7480
|
EXMediaLibrary: 587cd8aad27a6fc8d7c38b950bc75bc1845a7480
|
||||||
Expo: 707f9b0039eacc6a1dce90c08c9e37b9c417bba2
|
Expo: 863488a600a4565698a79577117c70b170054d08
|
||||||
expo-dev-client: 7c1ef51516853465f4d448c14ddf365167d20361
|
expo-dev-client: 7c1ef51516853465f4d448c14ddf365167d20361
|
||||||
expo-dev-launcher: 90de99d9e5d1a883d81355ca10e87c2f3c81d46e
|
expo-dev-launcher: 90de99d9e5d1a883d81355ca10e87c2f3c81d46e
|
||||||
expo-dev-menu: d4369e74d8d21a0ccdee35f7c732e7118b0fee16
|
expo-dev-menu: 4f54ef98df59d9d625677cb18ad4582de92b4a7d
|
||||||
expo-dev-menu-interface: 6c82ae323c4b8724dead4763ce3ff24a2108bdb1
|
expo-dev-menu-interface: 6c82ae323c4b8724dead4763ce3ff24a2108bdb1
|
||||||
ExpoImagePicker: 270dea232b3a072d981dd564e2cafc63a864edb1
|
ExpoImagePicker: 270dea232b3a072d981dd564e2cafc63a864edb1
|
||||||
ExpoKeepAwake: 69f5f627670d62318410392d03e0b5db0f85759a
|
ExpoKeepAwake: 69f5f627670d62318410392d03e0b5db0f85759a
|
||||||
ExpoLocalization: f26cd431ad9ea3533c5b08c4fabd879176a794bb
|
ExpoLocalization: f26cd431ad9ea3533c5b08c4fabd879176a794bb
|
||||||
ExpoModulesCore: 397fc99e9d6c9dcc010f36d5802097c17b90424c
|
ExpoModulesCore: 6e0259511f4c4341b6b8357db393624df2280828
|
||||||
EXSplashScreen: cd7fb052dff5ba8311d5c2455ecbebffe1b7a8ca
|
EXSplashScreen: cd7fb052dff5ba8311d5c2455ecbebffe1b7a8ca
|
||||||
EXUpdatesInterface: dd699d1930e28639dcbd70a402caea98e86364ca
|
EXUpdatesInterface: dd699d1930e28639dcbd70a402caea98e86364ca
|
||||||
FBLazyVector: 60195509584153283780abdac5569feffb8f08cc
|
FBLazyVector: 446e84642979fff0ba57f3c804c2228a473aeac2
|
||||||
FBReactNativeSpec: 9c191fb58d06dc05ab5559a5505fc32139e9e4a2
|
FBReactNativeSpec: 241709e132e3bf1526c1c4f00bc5384dd39dfba9
|
||||||
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
|
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
|
||||||
glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b
|
glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b
|
||||||
hermes-engine: 38bfe887e456b33b697187570a08de33969f5db7
|
hermes-engine: a1f157c49ea579c28b0296bda8530e980c45bdb3
|
||||||
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
|
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
|
||||||
libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef
|
libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef
|
||||||
RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1
|
RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1
|
||||||
RCTRequired: bec48f07daf7bcdc2655a0cde84e07d24d2a9e2a
|
RCTRequired: 5a024fdf458fa8c0d82fc262e76f982d4dcdecdd
|
||||||
RCTTypeSafety: 171394eebacf71e1cfad79dbfae7ee8fc16ca80a
|
RCTTypeSafety: b6c253064466411c6810b45f66bc1e43ce0c54ba
|
||||||
React: d7433ccb6a8c36e4cbed59a73c0700fc83c3e98a
|
React: 715292db5bd46989419445a5547954b25d2090f0
|
||||||
React-callinvoker: 15f165009bd22ae829b2b600e50bcc98076ce4b8
|
React-callinvoker: 105392d1179058585b564d35b4592fe1c46d6fba
|
||||||
React-Codegen: b5910000eaf1e0c2f47d29be6f82f5f1264420d7
|
React-Codegen: b75333b93d835afce84b73472927cccaef2c9f8c
|
||||||
React-Core: b6f2f78d580a90b83fd7b0d1c6911c799f6eac82
|
React-Core: 88838ed1724c64905fc6c0811d752828a92e395b
|
||||||
React-CoreModules: e0cbc1a4f4f3f60e23c476fef7ab37be363ea8c1
|
React-CoreModules: cd238b4bb8dc8529ccc8b34ceae7267b04ce1882
|
||||||
React-cxxreact: c87f3f124b2117d00d410b35f16c2257e25e50fa
|
React-cxxreact: 291bfab79d8098dc5ebab98f62e6bdfe81b3955a
|
||||||
React-hermes: c64ca6bdf16a7069773103c9bedaf30ec90ab38f
|
React-hermes: b1e67e9a81c71745704950516f40ee804349641c
|
||||||
React-jsi: 39729361645568e238081b3b3180fbad803f25a4
|
React-jsi: c9d5b563a6af6bb57034a82c2b0d39d0a7483bdc
|
||||||
React-jsiexecutor: 515b703d23ffadeac7687bc2d12fb08b90f0aaa1
|
React-jsiexecutor: d6b7fa9260aa3cb40afee0507e3bc1d17ecaa6f2
|
||||||
React-jsinspector: 9f7c9137605e72ca0343db4cea88006cb94856dd
|
React-jsinspector: 1f51e775819199d3fe9410e69ee8d4c4161c7b06
|
||||||
React-logger: 957e5dc96d9dbffc6e0f15e0ee4d2b42829ff207
|
React-logger: 0d58569ec51d30d1792c5e86a8e3b78d24b582c6
|
||||||
react-native-blur: 50c9feabacbc5f49b61337ebc32192c6be7ec3c3
|
react-native-blur: 50c9feabacbc5f49b61337ebc32192c6be7ec3c3
|
||||||
react-native-cameraroll: f3050460fe1708378698c16686bfaa5f34099be2
|
react-native-cameraroll: f3050460fe1708378698c16686bfaa5f34099be2
|
||||||
react-native-get-random-values: a6ea6a8a65dc93e96e24a11105b1a9c8cfe1d72a
|
react-native-get-random-values: a6ea6a8a65dc93e96e24a11105b1a9c8cfe1d72a
|
||||||
|
@ -887,20 +882,19 @@ SPEC CHECKSUMS:
|
||||||
react-native-safe-area-context: 39c2d8be3328df5d437ac1700f4f3a4f75716acc
|
react-native-safe-area-context: 39c2d8be3328df5d437ac1700f4f3a4f75716acc
|
||||||
react-native-splash-screen: 4312f786b13a81b5169ef346d76d33bc0c6dc457
|
react-native-splash-screen: 4312f786b13a81b5169ef346d76d33bc0c6dc457
|
||||||
react-native-version-number: b415bbec6a13f2df62bf978e85bc0d699462f37f
|
react-native-version-number: b415bbec6a13f2df62bf978e85bc0d699462f37f
|
||||||
react-native-webview: 994b9f8fbb504d6314dc40d83f94f27c6831b3bf
|
React-perflogger: 0bb0522a12e058f6eb69d888bc16f40c16c4b907
|
||||||
React-perflogger: af8a3d31546077f42d729b949925cc4549f14def
|
React-RCTActionSheet: bfd675a10f06a18728ea15d82082d48f228a213a
|
||||||
React-RCTActionSheet: 57cc5adfefbaaf0aae2cf7e10bccd746f2903673
|
React-RCTAnimation: 2fa220b2052ec75b733112aca39143d34546a941
|
||||||
React-RCTAnimation: 11c61e94da700c4dc915cf134513764d87fc5e2b
|
React-RCTAppDelegate: 8564f93c1d9274e95e3b0c746d08a87ff5a621b2
|
||||||
React-RCTAppDelegate: c3980adeaadcfd6cb495532e928b36ac6db3c14a
|
React-RCTBlob: d0336111f46301ae8aba2e161817e451aad72dd6
|
||||||
React-RCTBlob: ccc5049d742b41971141415ca86b83b201495695
|
React-RCTImage: fec592c46edb7c12a9cde08780bdb4a688416c62
|
||||||
React-RCTImage: 7a9226b0944f1e76e8e01e35a9245c2477cdbabb
|
React-RCTLinking: 14eccac5d2a3b34b89dbfa29e8ef6219a153fe2d
|
||||||
React-RCTLinking: bbe8cc582046a9c04f79c235b73c93700263e8b4
|
React-RCTNetwork: 1fbce92e772e39ca3687a2ebb854501ff6226dd7
|
||||||
React-RCTNetwork: fc2ca322159dc54e06508d4f5c3e934da63dc013
|
React-RCTSettings: 1abea36c9bb16d9979df6c4b42e2ea281b4bbcc5
|
||||||
React-RCTSettings: f1e9db2cdf946426d3f2b210e4ff4ce0f0d842ef
|
React-RCTText: 15355c41561a9f43dfd23616d0a0dd40ba05ed61
|
||||||
React-RCTText: 1c41dd57e5d742b1396b4eeb251851ce7ff0fca1
|
React-RCTVibration: ad17efcfb2fa8f6bfd8ac0cf48d96668b8b28e0b
|
||||||
React-RCTVibration: 5199a180d04873366a83855de55ac33ce60fe4d5
|
React-runtimeexecutor: 8fa50b38df6b992c76537993a2b0553d3b088004
|
||||||
React-runtimeexecutor: 7bf0dafc7b727d93c8cb94eb00a9d3753c446c3e
|
ReactCommon: b49a4b00ca6d181ff74b17c12b2d59ac4add0bde
|
||||||
ReactCommon: 6f65ea5b7d84deb9e386f670dd11ce499ded7b40
|
|
||||||
rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba
|
rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba
|
||||||
RNBackgroundFetch: 642777e4e76435773c149d565a043d66f1781237
|
RNBackgroundFetch: 642777e4e76435773c149d565a043d66f1781237
|
||||||
RNCAsyncStorage: 09fc8595e6d6f6d5abf16b23a56b257d9c6b7c5b
|
RNCAsyncStorage: 09fc8595e6d6f6d5abf16b23a56b257d9c6b7c5b
|
||||||
|
@ -921,7 +915,7 @@ SPEC CHECKSUMS:
|
||||||
sovran-react-native: fd3dc8f1a4b14acdc4ad25fc6b4ac4f52a2a2a15
|
sovran-react-native: fd3dc8f1a4b14acdc4ad25fc6b4ac4f52a2a2a15
|
||||||
Swime: d7b2c277503b6cea317774aedc2dce05613f8b0b
|
Swime: d7b2c277503b6cea317774aedc2dce05613f8b0b
|
||||||
TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863
|
TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863
|
||||||
Yoga: 5ed1699acbba8863755998a4245daa200ff3817b
|
Yoga: 79dd7410de6f8ad73a77c868d3d368843f0c93e0
|
||||||
|
|
||||||
PODFILE CHECKSUM: 5570c7b7d6ce7895f95d9db8a3a99b136a3f42c4
|
PODFILE CHECKSUM: 5570c7b7d6ce7895f95d9db8a3a99b136a3f42c4
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.10</string>
|
<string>1.11</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
|
|
257
jest/test-pds.ts
257
jest/test-pds.ts
|
@ -1,86 +1,73 @@
|
||||||
import {AddressInfo} from 'net'
|
import {AddressInfo} from 'net'
|
||||||
import os from 'os'
|
import os from 'os'
|
||||||
|
import net from 'net'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import * as crypto from '@atproto/crypto'
|
import * as crypto from '@atproto/crypto'
|
||||||
import PDSServer, {
|
import {PDS, ServerConfig, Database, MemoryBlobStore} from '@atproto/pds'
|
||||||
Database as PDSDatabase,
|
import * as plc from '@did-plc/lib'
|
||||||
MemoryBlobStore,
|
import {PlcServer, Database as PlcDatabase} from '@did-plc/server'
|
||||||
ServerConfig as PDSServerConfig,
|
import {BskyAgent} from '@atproto/api'
|
||||||
} from '@atproto/pds'
|
|
||||||
import * as plc from '@atproto/plc'
|
const ADMIN_PASSWORD = 'admin-pass'
|
||||||
import AtpAgent from '@atproto/api'
|
const SECOND = 1000
|
||||||
|
const MINUTE = SECOND * 60
|
||||||
|
const HOUR = MINUTE * 60
|
||||||
|
|
||||||
export interface TestUser {
|
export interface TestUser {
|
||||||
email: string
|
email: string
|
||||||
did: string
|
did: string
|
||||||
declarationCid: string
|
|
||||||
handle: string
|
handle: string
|
||||||
password: string
|
password: string
|
||||||
agent: AtpAgent
|
agent: BskyAgent
|
||||||
}
|
|
||||||
|
|
||||||
export interface TestUsers {
|
|
||||||
alice: TestUser
|
|
||||||
bob: TestUser
|
|
||||||
carla: TestUser
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TestPDS {
|
export interface TestPDS {
|
||||||
pdsUrl: string
|
pdsUrl: string
|
||||||
users: TestUsers
|
mocker: Mocker
|
||||||
close: () => Promise<void>
|
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> {
|
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()
|
||||||
|
|
||||||
// run plc server
|
const plcDb = PlcDatabase.mock()
|
||||||
const plcDb = plc.Database.memory()
|
|
||||||
await plcDb.migrateToLatestOrThrow()
|
const plcServer = PlcServer.create({db: plcDb})
|
||||||
const plcServer = plc.PlcServer.create({db: plcDb})
|
|
||||||
const plcListener = await plcServer.start()
|
const plcListener = await plcServer.start()
|
||||||
const plcPort = (plcListener.address() as AddressInfo).port
|
const plcPort = (plcListener.address() as AddressInfo).port
|
||||||
const plcUrl = `http://localhost:${plcPort}`
|
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 plcClient = new plc.Client(plcUrl)
|
||||||
const serverDid = await plcClient.createDid(
|
const serverDid = await plcClient.createDid({
|
||||||
keypair,
|
signingKey: repoSigningKey.did(),
|
||||||
recoveryKey,
|
rotationKeys: [recoveryKey, plcRotationKey.did()],
|
||||||
'localhost',
|
handle: 'localhost',
|
||||||
'https://pds.public.url',
|
pds: `http://localhost:${port}`,
|
||||||
)
|
signer: plcRotationKey,
|
||||||
|
})
|
||||||
|
|
||||||
const blobstoreLoc = path.join(os.tmpdir(), crypto.randomStr(5, 'base32'))
|
const blobstoreLoc = path.join(os.tmpdir(), crypto.randomStr(5, 'base32'))
|
||||||
|
|
||||||
const cfg = new PDSServerConfig({
|
const cfg = new ServerConfig({
|
||||||
debugMode: true,
|
debugMode: true,
|
||||||
version: '0.0.0',
|
version: '0.0.0',
|
||||||
scheme: 'http',
|
scheme: 'http',
|
||||||
hostname: 'localhost',
|
hostname: 'localhost',
|
||||||
|
port,
|
||||||
serverDid,
|
serverDid,
|
||||||
recoveryKey,
|
recoveryKey,
|
||||||
adminPassword: 'admin-pass',
|
adminPassword: ADMIN_PASSWORD,
|
||||||
inviteRequired: false,
|
inviteRequired: false,
|
||||||
didPlcUrl: plcUrl,
|
didPlcUrl: plcUrl,
|
||||||
jwtSecret: 'jwt-secret',
|
jwtSecret: 'jwt-secret',
|
||||||
availableUserDomains: ['.test'],
|
availableUserDomains: ['.test'],
|
||||||
appUrlPasswordReset: 'app://forgot-password',
|
appUrlPasswordReset: 'app://forgot-password',
|
||||||
emailNoReplyAddress: 'noreply@blueskyweb.xyz',
|
emailNoReplyAddress: 'noreply@blueskyweb.xyz',
|
||||||
publicUrl: 'https://pds.public.url',
|
publicUrl: `http://localhost:${port}`,
|
||||||
imgUriSalt: '9dd04221f5755bce5f55f47464c27e1e',
|
imgUriSalt: '9dd04221f5755bce5f55f47464c27e1e',
|
||||||
imgUriKey:
|
imgUriKey:
|
||||||
'f23ecd142835025f42c3db2cf25dd813956c178392760256211f9d315f8ab4d8',
|
'f23ecd142835025f42c3db2cf25dd813956c178392760256211f9d315f8ab4d8',
|
||||||
|
@ -88,22 +75,33 @@ export async function createServer(): Promise<TestPDS> {
|
||||||
blobstoreLocation: `${blobstoreLoc}/blobs`,
|
blobstoreLocation: `${blobstoreLoc}/blobs`,
|
||||||
blobstoreTmp: `${blobstoreLoc}/tmp`,
|
blobstoreTmp: `${blobstoreLoc}/tmp`,
|
||||||
maxSubscriptionBuffer: 200,
|
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()
|
await db.migrateToLatestOrThrow()
|
||||||
|
|
||||||
const blobstore = new MemoryBlobStore()
|
const blobstore = new MemoryBlobStore()
|
||||||
|
|
||||||
const pds = PDSServer.create({db, blobstore, keypair, config: cfg})
|
const pds = PDS.create({
|
||||||
const pdsServer = await pds.start()
|
db,
|
||||||
const pdsPort = (pdsServer.address() as AddressInfo).port
|
blobstore,
|
||||||
const pdsUrl = `http://localhost:${pdsPort}`
|
repoSigningKey,
|
||||||
const testUsers = await genMockData(pdsUrl)
|
plcRotationKey,
|
||||||
|
config: cfg,
|
||||||
|
})
|
||||||
|
await pds.start()
|
||||||
|
const pdsUrl = `http://localhost:${port}`
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pdsUrl,
|
pdsUrl,
|
||||||
users: testUsers,
|
mocker: new Mocker(pdsUrl),
|
||||||
async close() {
|
async close() {
|
||||||
await pds.destroy()
|
await pds.destroy()
|
||||||
await plcServer.destroy()
|
await plcServer.destroy()
|
||||||
|
@ -111,90 +109,93 @@ export async function createServer(): Promise<TestPDS> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function genMockData(pdsUrl: string): Promise<TestUsers> {
|
class Mocker {
|
||||||
const date = dateGen()
|
agent: BskyAgent
|
||||||
|
users: Record<string, TestUser> = {}
|
||||||
|
|
||||||
const agents = {
|
constructor(public service: string) {
|
||||||
loggedout: new AtpAgent({service: pdsUrl}),
|
this.agent = new BskyAgent({service})
|
||||||
alice: new AtpAgent({service: pdsUrl}),
|
|
||||||
bob: new AtpAgent({service: pdsUrl}),
|
|
||||||
carla: new AtpAgent({service: pdsUrl}),
|
|
||||||
}
|
}
|
||||||
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',
|
|
||||||
password: 'hunter2',
|
|
||||||
agent: agents.carla,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
const alice = users[0]
|
|
||||||
const bob = users[1]
|
|
||||||
const carla = users[2]
|
|
||||||
|
|
||||||
let _i = 1
|
// NOTE
|
||||||
for (const user of users) {
|
// deterministic date generator
|
||||||
const res = await agents.loggedout.api.com.atproto.account.create({
|
// we use this to ensure the mock dataset is always the same
|
||||||
email: user.email,
|
// which is very useful when testing
|
||||||
handle: user.handle,
|
*dateGen() {
|
||||||
password: user.password,
|
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',
|
||||||
})
|
})
|
||||||
user.agent.api.setHeader('Authorization', `Bearer ${res.data.accessJwt}`)
|
this.users[name] = {
|
||||||
const {data: profile} = await user.agent.api.app.bsky.actor.getProfile({
|
did: res.data.did,
|
||||||
actor: user.handle,
|
email,
|
||||||
})
|
handle: name + '.test',
|
||||||
user.did = res.data.did
|
password: 'hunter2',
|
||||||
user.declarationCid = profile.declaration.cid
|
agent: agent,
|
||||||
await user.agent.api.app.bsky.actor.profile.create(
|
}
|
||||||
{did: user.did},
|
|
||||||
{
|
|
||||||
displayName: ucfirst(user.handle).slice(0, -5),
|
|
||||||
description: `Test user ${_i++}`,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// everybody follows everybody
|
async follow(a: string, b: string) {
|
||||||
const follow = async (author: TestUser, subject: TestUser) => {
|
await this.users[a].agent.follow(this.users[b].did)
|
||||||
await author.agent.api.app.bsky.graph.follow.create(
|
|
||||||
{did: author.did},
|
|
||||||
{
|
|
||||||
subject: {
|
|
||||||
did: subject.did,
|
|
||||||
declarationCid: subject.declarationCid,
|
|
||||||
},
|
|
||||||
createdAt: date.next().value || '',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
await follow(alice, bob)
|
|
||||||
await follow(alice, carla)
|
|
||||||
await follow(bob, alice)
|
|
||||||
await follow(bob, carla)
|
|
||||||
await follow(carla, alice)
|
|
||||||
await follow(carla, bob)
|
|
||||||
|
|
||||||
return {alice, bob, carla}
|
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')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ucfirst(str: string): string {
|
const checkAvailablePort = (port: number) =>
|
||||||
return str.at(0)?.toUpperCase() + str.slice(1)
|
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')
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
// Learn more https://docs.expo.io/guides/customizing-metro
|
// Learn more https://docs.expo.io/guides/customizing-metro
|
||||||
const {getDefaultConfig} = require('expo/metro-config')
|
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
|
||||||
|
|
26
package.json
26
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "bsky.app",
|
"name": "bsky.app",
|
||||||
"version": "1.10.0",
|
"version": "1.11.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "patch-package",
|
"postinstall": "patch-package",
|
||||||
|
@ -15,12 +15,13 @@
|
||||||
"test-ci": "jest --ci --forceExit --reporters=default --reporters=jest-junit",
|
"test-ci": "jest --ci --forceExit --reporters=default --reporters=jest-junit",
|
||||||
"test-coverage": "jest --coverage",
|
"test-coverage": "jest --coverage",
|
||||||
"lint": "eslint ./src --ext .js,.jsx,.ts,.tsx",
|
"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": {
|
"dependencies": {
|
||||||
"@atproto/api": "0.1.3",
|
"@atproto/api": "0.2.0",
|
||||||
"@atproto/lexicon": "^0.0.4",
|
|
||||||
"@atproto/xrpc": "^0.0.4",
|
|
||||||
"@bam.tech/react-native-image-resizer": "^3.0.4",
|
"@bam.tech/react-native-image-resizer": "^3.0.4",
|
||||||
"@expo/webpack-config": "^18.0.1",
|
"@expo/webpack-config": "^18.0.1",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
||||||
|
@ -55,7 +56,7 @@
|
||||||
"await-lock": "^2.2.2",
|
"await-lock": "^2.2.2",
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
"email-validator": "^2.0.4",
|
"email-validator": "^2.0.4",
|
||||||
"expo": "~48.0.0-beta.2",
|
"expo": "~48.0.9",
|
||||||
"expo-camera": "~13.2.1",
|
"expo-camera": "~13.2.1",
|
||||||
"expo-dev-client": "~2.1.1",
|
"expo-dev-client": "~2.1.1",
|
||||||
"expo-image-picker": "~14.1.1",
|
"expo-image-picker": "~14.1.1",
|
||||||
|
@ -63,6 +64,8 @@
|
||||||
"expo-media-library": "~15.2.3",
|
"expo-media-library": "~15.2.3",
|
||||||
"expo-splash-screen": "~0.18.1",
|
"expo-splash-screen": "~0.18.1",
|
||||||
"expo-status-bar": "~1.4.4",
|
"expo-status-bar": "~1.4.4",
|
||||||
|
"fast-text-encoding": "^1.0.6",
|
||||||
|
"graphemer": "^1.4.0",
|
||||||
"he": "^1.2.0",
|
"he": "^1.2.0",
|
||||||
"history": "^5.3.0",
|
"history": "^5.3.0",
|
||||||
"js-sha256": "^0.9.0",
|
"js-sha256": "^0.9.0",
|
||||||
|
@ -84,7 +87,7 @@
|
||||||
"react-avatar-editor": "^13.0.0",
|
"react-avatar-editor": "^13.0.0",
|
||||||
"react-circular-progressbar": "^2.1.0",
|
"react-circular-progressbar": "^2.1.0",
|
||||||
"react-dom": "^18.2.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-appstate-hook": "^1.0.6",
|
||||||
"react-native-background-fetch": "^4.1.8",
|
"react-native-background-fetch": "^4.1.8",
|
||||||
"react-native-drawer-layout": "^3.2.0",
|
"react-native-drawer-layout": "^3.2.0",
|
||||||
|
@ -109,19 +112,17 @@
|
||||||
"react-native-version-number": "^0.3.6",
|
"react-native-version-number": "^0.3.6",
|
||||||
"react-native-web": "^0.18.11",
|
"react-native-web": "^0.18.11",
|
||||||
"react-native-web-linear-gradient": "^1.1.2",
|
"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",
|
"rn-fetch-blob": "^0.12.0",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"tlds": "^1.234.0",
|
"tlds": "^1.234.0",
|
||||||
"zod": "^3.20.2"
|
"zod": "^3.20.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@atproto/pds": "^0.0.3",
|
"@atproto/pds": "^0.1.0",
|
||||||
"@babel/core": "^7.20.0",
|
"@babel/core": "^7.20.0",
|
||||||
"@babel/preset-env": "^7.20.0",
|
"@babel/preset-env": "^7.20.0",
|
||||||
"@babel/runtime": "^7.20.0",
|
"@babel/runtime": "^7.20.0",
|
||||||
|
"@did-plc/server": "^0.0.1",
|
||||||
"@react-native-community/eslint-config": "^3.0.0",
|
"@react-native-community/eslint-config": "^3.0.0",
|
||||||
"@testing-library/jest-native": "^5.4.1",
|
"@testing-library/jest-native": "^5.4.1",
|
||||||
"@testing-library/react-native": "^11.5.2",
|
"@testing-library/react-native": "^11.5.2",
|
||||||
|
@ -150,13 +151,14 @@
|
||||||
"eslint-plugin-ft-flow": "^2.0.3",
|
"eslint-plugin-ft-flow": "^2.0.3",
|
||||||
"html-webpack-plugin": "^5.5.0",
|
"html-webpack-plugin": "^5.5.0",
|
||||||
"jest": "^29.4.3",
|
"jest": "^29.4.3",
|
||||||
"jest-expo": "^48.0.0-beta.2",
|
"jest-expo": "^48.0.2",
|
||||||
"jest-junit": "^15.0.0",
|
"jest-junit": "^15.0.0",
|
||||||
"metro-react-native-babel-preset": "^0.73.7",
|
"metro-react-native-babel-preset": "^0.73.7",
|
||||||
"prettier": "^2.8.3",
|
"prettier": "^2.8.3",
|
||||||
"react-native-dotenv": "^3.3.1",
|
"react-native-dotenv": "^3.3.1",
|
||||||
"react-scripts": "^5.0.1",
|
"react-scripts": "^5.0.1",
|
||||||
"react-test-renderer": "18.2.0",
|
"react-test-renderer": "18.2.0",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^4.4.4",
|
"typescript": "^4.4.4",
|
||||||
"url-loader": "^4.1.1",
|
"url-loader": "^4.1.1",
|
||||||
"webpack": "^5.75.0",
|
"webpack": "^5.75.0",
|
||||||
|
|
|
@ -29,7 +29,6 @@ const App = observer(() => {
|
||||||
analytics.init(store)
|
analytics.init(store)
|
||||||
notifee.init(store)
|
notifee.init(store)
|
||||||
SplashScreen.hide()
|
SplashScreen.hide()
|
||||||
store.hackCheckIfUpgradeNeeded()
|
|
||||||
Linking.getInitialURL().then((url: string | null) => {
|
Linking.getInitialURL().then((url: string | null) => {
|
||||||
if (url) {
|
if (url) {
|
||||||
handleLink(url)
|
handleLink(url)
|
||||||
|
|
|
@ -31,7 +31,7 @@ import {ProfileScreen} from './view/screens/Profile'
|
||||||
import {ProfileFollowersScreen} from './view/screens/ProfileFollowers'
|
import {ProfileFollowersScreen} from './view/screens/ProfileFollowers'
|
||||||
import {ProfileFollowsScreen} from './view/screens/ProfileFollows'
|
import {ProfileFollowsScreen} from './view/screens/ProfileFollows'
|
||||||
import {PostThreadScreen} from './view/screens/PostThread'
|
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 {PostRepostedByScreen} from './view/screens/PostRepostedBy'
|
||||||
import {DebugScreen} from './view/screens/Debug'
|
import {DebugScreen} from './view/screens/Debug'
|
||||||
import {LogScreen} from './view/screens/Log'
|
import {LogScreen} from './view/screens/Log'
|
||||||
|
@ -62,7 +62,7 @@ function commonScreens(Stack: typeof HomeTab) {
|
||||||
/>
|
/>
|
||||||
<Stack.Screen name="ProfileFollows" component={ProfileFollowsScreen} />
|
<Stack.Screen name="ProfileFollows" component={ProfileFollowsScreen} />
|
||||||
<Stack.Screen name="PostThread" component={PostThreadScreen} />
|
<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="PostRepostedBy" component={PostRepostedByScreen} />
|
||||||
<Stack.Screen name="Debug" component={DebugScreen} />
|
<Stack.Screen name="Debug" component={DebugScreen} />
|
||||||
<Stack.Screen name="Log" component={LogScreen} />
|
<Stack.Screen name="Log" component={LogScreen} />
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import AtpAgent from '@atproto/api'
|
import {BskyAgent, stringifyLex, jsonToLex} from '@atproto/api'
|
||||||
import RNFS from 'react-native-fs'
|
import RNFS from 'react-native-fs'
|
||||||
|
|
||||||
const GET_TIMEOUT = 15e3 // 15s
|
const GET_TIMEOUT = 15e3 // 15s
|
||||||
const POST_TIMEOUT = 60e3 // 60s
|
const POST_TIMEOUT = 60e3 // 60s
|
||||||
|
|
||||||
export function doPolyfill() {
|
export function doPolyfill() {
|
||||||
AtpAgent.configure({fetch: fetchHandler})
|
BskyAgent.configure({fetch: fetchHandler})
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FetchHandlerResponse {
|
interface FetchHandlerResponse {
|
||||||
|
@ -22,7 +22,7 @@ async function fetchHandler(
|
||||||
): Promise<FetchHandlerResponse> {
|
): Promise<FetchHandlerResponse> {
|
||||||
const reqMimeType = reqHeaders['Content-Type'] || reqHeaders['content-type']
|
const reqMimeType = reqHeaders['Content-Type'] || reqHeaders['content-type']
|
||||||
if (reqMimeType && reqMimeType.startsWith('application/json')) {
|
if (reqMimeType && reqMimeType.startsWith('application/json')) {
|
||||||
reqBody = JSON.stringify(reqBody)
|
reqBody = stringifyLex(reqBody)
|
||||||
} else if (
|
} else if (
|
||||||
typeof reqBody === 'string' &&
|
typeof reqBody === 'string' &&
|
||||||
(reqBody.startsWith('/') || reqBody.startsWith('file:'))
|
(reqBody.startsWith('/') || reqBody.startsWith('file:'))
|
||||||
|
@ -65,7 +65,7 @@ async function fetchHandler(
|
||||||
let resBody
|
let resBody
|
||||||
if (resMimeType) {
|
if (resMimeType) {
|
||||||
if (resMimeType.startsWith('application/json')) {
|
if (resMimeType.startsWith('application/json')) {
|
||||||
resBody = await res.json()
|
resBody = jsonToLex(await res.json())
|
||||||
} else if (resMimeType.startsWith('text/')) {
|
} else if (resMimeType.startsWith('text/')) {
|
||||||
resBody = await res.text()
|
resBody = await res.text()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
export function doPolyfill() {
|
export function doPolyfill() {
|
||||||
// TODO needed? native fetch may work fine -prf
|
// no polyfill is needed on web
|
||||||
// AtpApi.xrpc.fetch = fetchHandler
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import {RootStoreModel} from 'state/index'
|
import {RootStoreModel} from 'state/index'
|
||||||
import {
|
import {
|
||||||
AppBskyFeedFeedViewPost,
|
AppBskyFeedDefs,
|
||||||
AppBskyFeedGetAuthorFeed as GetAuthorFeed,
|
AppBskyFeedGetAuthorFeed as GetAuthorFeed,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
type ReasonRepost = AppBskyFeedFeedViewPost.ReasonRepost
|
type ReasonRepost = AppBskyFeedDefs.ReasonRepost
|
||||||
|
|
||||||
async function getMultipleAuthorsPosts(
|
async function getMultipleAuthorsPosts(
|
||||||
rootStore: RootStoreModel,
|
rootStore: RootStoreModel,
|
||||||
|
@ -12,12 +12,12 @@ async function getMultipleAuthorsPosts(
|
||||||
limit: number = 10,
|
limit: number = 10,
|
||||||
) {
|
) {
|
||||||
const responses = await Promise.all(
|
const responses = await Promise.all(
|
||||||
authors.map((author, index) =>
|
authors.map((actor, index) =>
|
||||||
rootStore.api.app.bsky.feed
|
rootStore.agent
|
||||||
.getAuthorFeed({
|
.getAuthorFeed({
|
||||||
author,
|
actor,
|
||||||
limit,
|
limit,
|
||||||
before: cursor ? cursor.split(',')[index] : undefined,
|
cursor: cursor ? cursor.split(',')[index] : undefined,
|
||||||
})
|
})
|
||||||
.catch(_err => ({success: false, headers: {}, data: {feed: []}})),
|
.catch(_err => ({success: false, headers: {}, data: {feed: []}})),
|
||||||
),
|
),
|
||||||
|
@ -29,14 +29,14 @@ function mergePosts(
|
||||||
responses: GetAuthorFeed.Response[],
|
responses: GetAuthorFeed.Response[],
|
||||||
{repostsOnly, bestOfOnly}: {repostsOnly?: boolean; bestOfOnly?: boolean},
|
{repostsOnly, bestOfOnly}: {repostsOnly?: boolean; bestOfOnly?: boolean},
|
||||||
) {
|
) {
|
||||||
let posts: AppBskyFeedFeedViewPost.Main[] = []
|
let posts: AppBskyFeedDefs.FeedViewPost[] = []
|
||||||
|
|
||||||
if (bestOfOnly) {
|
if (bestOfOnly) {
|
||||||
for (const res of responses) {
|
for (const res of responses) {
|
||||||
if (res.success) {
|
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(
|
res.data.feed = res.data.feed.reduce(
|
||||||
(acc: AppBskyFeedFeedViewPost.Main[], v) => {
|
(acc: AppBskyFeedDefs.FeedViewPost[], v) => {
|
||||||
if (
|
if (
|
||||||
!acc?.[0] &&
|
!acc?.[0] &&
|
||||||
!v.reason &&
|
!v.reason &&
|
||||||
|
@ -49,7 +49,7 @@ function mergePosts(
|
||||||
acc &&
|
acc &&
|
||||||
!v.reason &&
|
!v.reason &&
|
||||||
!v.reply &&
|
!v.reply &&
|
||||||
v.post.upvoteCount > acc[0]?.post.upvoteCount &&
|
(v.post.likeCount || 0) > (acc[0]?.post.likeCount || 0) &&
|
||||||
isRecentEnough(v.post.indexedAt)
|
isRecentEnough(v.post.indexedAt)
|
||||||
) {
|
) {
|
||||||
return [v]
|
return [v]
|
||||||
|
@ -92,7 +92,7 @@ function mergePosts(
|
||||||
return posts
|
return posts
|
||||||
}
|
}
|
||||||
|
|
||||||
function isARepostOfSomeoneElse(post: AppBskyFeedFeedViewPost.Main): boolean {
|
function isARepostOfSomeoneElse(post: AppBskyFeedDefs.FeedViewPost): boolean {
|
||||||
return (
|
return (
|
||||||
post.reason?.$type === 'app.bsky.feed.feedViewPost#reasonRepost' &&
|
post.reason?.$type === 'app.bsky.feed.feedViewPost#reasonRepost' &&
|
||||||
post.post.author.did !== (post.reason as ReasonRepost).by.did
|
post.post.author.did !== (post.reason as ReasonRepost).by.did
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import {AppBskyFeedFeedViewPost} from '@atproto/api'
|
import {AppBskyFeedDefs} from '@atproto/api'
|
||||||
import lande from 'lande'
|
import lande from 'lande'
|
||||||
type FeedViewPost = AppBskyFeedFeedViewPost.Main
|
import {hasProp} from 'lib/type-guards'
|
||||||
import {hasProp} from '@atproto/lexicon'
|
|
||||||
import {LANGUAGES_MAP_CODE2} from '../../locale/languages'
|
import {LANGUAGES_MAP_CODE2} from '../../locale/languages'
|
||||||
|
type FeedViewPost = AppBskyFeedDefs.FeedViewPost
|
||||||
|
|
||||||
export type FeedTunerFn = (
|
export type FeedTunerFn = (
|
||||||
tuner: FeedTuner,
|
tuner: FeedTuner,
|
||||||
|
@ -174,7 +174,7 @@ export class FeedTuner {
|
||||||
}
|
}
|
||||||
const item = slices[i].rootItem
|
const item = slices[i].rootItem
|
||||||
const isRepost = Boolean(item.reason)
|
const isRepost = Boolean(item.reason)
|
||||||
if (!isRepost && item.post.upvoteCount < 2) {
|
if (!isRepost && (item.post.likeCount || 0) < 2) {
|
||||||
slices.splice(i, 1)
|
slices.splice(i, 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import {
|
import {
|
||||||
AppBskyEmbedImages,
|
AppBskyEmbedImages,
|
||||||
AppBskyEmbedExternal,
|
AppBskyEmbedExternal,
|
||||||
ComAtprotoBlobUpload,
|
|
||||||
AppBskyEmbedRecord,
|
AppBskyEmbedRecord,
|
||||||
|
AppBskyEmbedRecordWithMedia,
|
||||||
|
ComAtprotoRepoUploadBlob,
|
||||||
|
RichText,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {AtUri} from '../../third-party/uri'
|
import {AtUri} from '../../third-party/uri'
|
||||||
import {RootStoreModel} from 'state/models/root-store'
|
import {RootStoreModel} from 'state/models/root-store'
|
||||||
import {extractEntities} from 'lib/strings/rich-text-detection'
|
|
||||||
import {isNetworkError} from 'lib/strings/errors'
|
import {isNetworkError} from 'lib/strings/errors'
|
||||||
import {LinkMeta} from '../link-meta/link-meta'
|
import {LinkMeta} from '../link-meta/link-meta'
|
||||||
import {Image} from '../media/manip'
|
import {Image} from '../media/manip'
|
||||||
import {RichText} from '../strings/rich-text'
|
|
||||||
import {isWeb} from 'platform/detection'
|
import {isWeb} from 'platform/detection'
|
||||||
|
|
||||||
export interface ExternalEmbedDraft {
|
export interface ExternalEmbedDraft {
|
||||||
|
@ -27,7 +27,7 @@ export async function resolveName(store: RootStoreModel, didOrHandle: string) {
|
||||||
if (didOrHandle.startsWith('did:')) {
|
if (didOrHandle.startsWith('did:')) {
|
||||||
return didOrHandle
|
return didOrHandle
|
||||||
}
|
}
|
||||||
const res = await store.api.com.atproto.handle.resolve({
|
const res = await store.agent.resolveHandle({
|
||||||
handle: didOrHandle,
|
handle: didOrHandle,
|
||||||
})
|
})
|
||||||
return res.data.did
|
return res.data.did
|
||||||
|
@ -37,15 +37,15 @@ export async function uploadBlob(
|
||||||
store: RootStoreModel,
|
store: RootStoreModel,
|
||||||
blob: string,
|
blob: string,
|
||||||
encoding: string,
|
encoding: string,
|
||||||
): Promise<ComAtprotoBlobUpload.Response> {
|
): Promise<ComAtprotoRepoUploadBlob.Response> {
|
||||||
if (isWeb) {
|
if (isWeb) {
|
||||||
// `blob` should be a data uri
|
// `blob` should be a data uri
|
||||||
return store.api.com.atproto.blob.upload(convertDataURIToUint8Array(blob), {
|
return store.agent.uploadBlob(convertDataURIToUint8Array(blob), {
|
||||||
encoding,
|
encoding,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// `blob` should be a path to a file in the local FS
|
// `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
|
blob, // this will be special-cased by the fetch monkeypatch in /src/state/lib/api.ts
|
||||||
{encoding},
|
{encoding},
|
||||||
)
|
)
|
||||||
|
@ -70,22 +70,18 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
|
||||||
| AppBskyEmbedImages.Main
|
| AppBskyEmbedImages.Main
|
||||||
| AppBskyEmbedExternal.Main
|
| AppBskyEmbedExternal.Main
|
||||||
| AppBskyEmbedRecord.Main
|
| AppBskyEmbedRecord.Main
|
||||||
|
| AppBskyEmbedRecordWithMedia.Main
|
||||||
| undefined
|
| undefined
|
||||||
let reply
|
let reply
|
||||||
const text = new RichText(opts.rawText, undefined, {
|
const rt = new RichText(
|
||||||
|
{text: opts.rawText.trim()},
|
||||||
|
{
|
||||||
cleanNewlines: true,
|
cleanNewlines: true,
|
||||||
}).text.trim()
|
},
|
||||||
|
)
|
||||||
|
|
||||||
opts.onStateChange?.('Processing...')
|
opts.onStateChange?.('Processing...')
|
||||||
const entities = extractEntities(text, opts.knownHandles)
|
await rt.detectFacets(store.agent)
|
||||||
if (entities) {
|
|
||||||
for (const ent of entities) {
|
|
||||||
if (ent.type === 'mention') {
|
|
||||||
const prof = await store.profiles.getProfile(ent.value)
|
|
||||||
ent.value = prof.data.did
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.quote) {
|
if (opts.quote) {
|
||||||
embed = {
|
embed = {
|
||||||
|
@ -95,24 +91,37 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
|
||||||
cid: opts.quote.cid,
|
cid: opts.quote.cid,
|
||||||
},
|
},
|
||||||
} as AppBskyEmbedRecord.Main
|
} as AppBskyEmbedRecord.Main
|
||||||
} else if (opts.images?.length) {
|
}
|
||||||
embed = {
|
|
||||||
$type: 'app.bsky.embed.images',
|
if (opts.images?.length) {
|
||||||
images: [],
|
const images: AppBskyEmbedImages.Image[] = []
|
||||||
} as AppBskyEmbedImages.Main
|
|
||||||
let i = 1
|
|
||||||
for (const image of opts.images) {
|
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')
|
const res = await uploadBlob(store, image, 'image/jpeg')
|
||||||
embed.images.push({
|
images.push({
|
||||||
image: {
|
image: res.data.blob,
|
||||||
cid: res.data.cid,
|
|
||||||
mimeType: 'image/jpeg',
|
|
||||||
},
|
|
||||||
alt: '', // TODO supply alt text
|
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
|
let thumb
|
||||||
if (opts.extLink.localThumb) {
|
if (opts.extLink.localThumb) {
|
||||||
opts.onStateChange?.('Uploading link thumbnail...')
|
opts.onStateChange?.('Uploading link thumbnail...')
|
||||||
|
@ -138,12 +147,25 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
|
||||||
opts.extLink.localThumb.path,
|
opts.extLink.localThumb.path,
|
||||||
encoding,
|
encoding,
|
||||||
)
|
)
|
||||||
thumb = {
|
thumb = thumbUploadRes.data.blob
|
||||||
cid: thumbUploadRes.data.cid,
|
|
||||||
mimeType: encoding,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 = {
|
embed = {
|
||||||
$type: 'app.bsky.embed.external',
|
$type: 'app.bsky.embed.external',
|
||||||
external: {
|
external: {
|
||||||
|
@ -154,11 +176,12 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
|
||||||
},
|
},
|
||||||
} as AppBskyEmbedExternal.Main
|
} as AppBskyEmbedExternal.Main
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (opts.replyTo) {
|
if (opts.replyTo) {
|
||||||
const replyToUrip = new AtUri(opts.replyTo)
|
const replyToUrip = new AtUri(opts.replyTo)
|
||||||
const parentPost = await store.api.app.bsky.feed.post.get({
|
const parentPost = await store.agent.getPost({
|
||||||
user: replyToUrip.host,
|
repo: replyToUrip.host,
|
||||||
rkey: replyToUrip.rkey,
|
rkey: replyToUrip.rkey,
|
||||||
})
|
})
|
||||||
if (parentPost) {
|
if (parentPost) {
|
||||||
|
@ -175,16 +198,12 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
opts.onStateChange?.('Posting...')
|
opts.onStateChange?.('Posting...')
|
||||||
return await store.api.app.bsky.feed.post.create(
|
return await store.agent.post({
|
||||||
{did: store.me.did || ''},
|
text: rt.text,
|
||||||
{
|
facets: rt.facets,
|
||||||
text,
|
|
||||||
reply,
|
reply,
|
||||||
embed,
|
embed,
|
||||||
entities,
|
})
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(`Failed to create post: ${e.toString()}`)
|
console.error(`Failed to create post: ${e.toString()}`)
|
||||||
if (isNetworkError(e)) {
|
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
|
// helpers
|
||||||
// =
|
// =
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -45,7 +45,7 @@ export function displayNotificationFromModel(
|
||||||
let author = notif.author.displayName || notif.author.handle
|
let author = notif.author.displayName || notif.author.handle
|
||||||
let title: string
|
let title: string
|
||||||
let body: string = ''
|
let body: string = ''
|
||||||
if (notif.isUpvote) {
|
if (notif.isLike) {
|
||||||
title = `${author} liked your post`
|
title = `${author} liked your post`
|
||||||
body = notif.additionalPost?.thread?.postRecord?.text || ''
|
body = notif.additionalPost?.thread?.postRecord?.text || ''
|
||||||
} else if (notif.isRepost) {
|
} else if (notif.isRepost) {
|
||||||
|
@ -65,7 +65,7 @@ export function displayNotificationFromModel(
|
||||||
}
|
}
|
||||||
let image
|
let image
|
||||||
if (
|
if (
|
||||||
AppBskyEmbedImages.isPresented(notif.additionalPost?.thread?.post.embed) &&
|
AppBskyEmbedImages.isView(notif.additionalPost?.thread?.post.embed) &&
|
||||||
notif.additionalPost?.thread?.post.embed.images[0]?.thumb
|
notif.additionalPost?.thread?.post.embed.images[0]?.thumb
|
||||||
) {
|
) {
|
||||||
image = notif.additionalPost.thread.post.embed.images[0].thumb
|
image = notif.additionalPost.thread.post.embed.images[0].thumb
|
||||||
|
|
|
@ -10,7 +10,7 @@ export type CommonNavigatorParams = {
|
||||||
ProfileFollowers: {name: string}
|
ProfileFollowers: {name: string}
|
||||||
ProfileFollows: {name: string}
|
ProfileFollows: {name: string}
|
||||||
PostThread: {name: string; rkey: string}
|
PostThread: {name: string; rkey: string}
|
||||||
PostUpvotedBy: {name: string; rkey: string}
|
PostLikedBy: {name: string; rkey: string}
|
||||||
PostRepostedBy: {name: string; rkey: string}
|
PostRepostedBy: {name: string; rkey: string}
|
||||||
Debug: undefined
|
Debug: undefined
|
||||||
Log: undefined
|
Log: undefined
|
||||||
|
|
|
@ -1,64 +1,5 @@
|
||||||
import {AppBskyFeedPost} from '@atproto/api'
|
|
||||||
type Entity = AppBskyFeedPost.Entity
|
|
||||||
import {isValidDomain} from './url-helpers'
|
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 {
|
interface DetectedLink {
|
||||||
link: string
|
link: string
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -71,6 +71,7 @@ export const s = StyleSheet.create({
|
||||||
borderBottom1: {borderBottomWidth: 1},
|
borderBottom1: {borderBottomWidth: 1},
|
||||||
borderLeft1: {borderLeftWidth: 1},
|
borderLeft1: {borderLeftWidth: 1},
|
||||||
hidden: {display: 'none'},
|
hidden: {display: 'none'},
|
||||||
|
dimmed: {opacity: 0.5},
|
||||||
|
|
||||||
// font weights
|
// font weights
|
||||||
fw600: {fontWeight: '600'},
|
fw600: {fontWeight: '600'},
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'fast-text-encoding'
|
||||||
|
import Graphemer from 'graphemer'
|
||||||
export {}
|
export {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -48,3 +50,18 @@ globalThis.atob = (str: string): string => {
|
||||||
}
|
}
|
||||||
return result
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -2,3 +2,11 @@
|
||||||
|
|
||||||
// @ts-ignore whatever typescript wants to complain about here, I dont care about -prf
|
// @ts-ignore whatever typescript wants to complain about here, I dont care about -prf
|
||||||
window.setImmediate = (cb: () => void) => setTimeout(cb, 0)
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ export const router = new Router({
|
||||||
ProfileFollowers: '/profile/:name/followers',
|
ProfileFollowers: '/profile/:name/followers',
|
||||||
ProfileFollows: '/profile/:name/follows',
|
ProfileFollows: '/profile/:name/follows',
|
||||||
PostThread: '/profile/:name/post/:rkey',
|
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',
|
PostRepostedBy: '/profile/:name/post/:rkey/reposted-by',
|
||||||
Debug: '/sys/debug',
|
Debug: '/sys/debug',
|
||||||
Log: '/sys/log',
|
Log: '/sys/log',
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import {autorun} from 'mobx'
|
import {autorun} from 'mobx'
|
||||||
import {AppState, Platform} from 'react-native'
|
import {AppState, Platform} from 'react-native'
|
||||||
import {AtpAgent} from '@atproto/api'
|
import {BskyAgent} from '@atproto/api'
|
||||||
import {RootStoreModel} from './models/root-store'
|
import {RootStoreModel} from './models/root-store'
|
||||||
import * as apiPolyfill from 'lib/api/api-polyfill'
|
import * as apiPolyfill from 'lib/api/api-polyfill'
|
||||||
import * as storage from 'lib/storage'
|
import * as storage from 'lib/storage'
|
||||||
|
@ -19,7 +19,7 @@ export async function setupState(serviceUri = DEFAULT_SERVICE) {
|
||||||
|
|
||||||
apiPolyfill.doPolyfill()
|
apiPolyfill.doPolyfill()
|
||||||
|
|
||||||
rootStore = new RootStoreModel(new AtpAgent({service: serviceUri}))
|
rootStore = new RootStoreModel(new BskyAgent({service: serviceUri}))
|
||||||
try {
|
try {
|
||||||
data = (await storage.load(ROOT_STATE_STORAGE_KEY)) || {}
|
data = (await storage.load(ROOT_STATE_STORAGE_KEY)) || {}
|
||||||
rootStore.log.debug('Initial hydrate', {hasSession: !!data.session})
|
rootStore.log.debug('Initial hydrate', {hasSession: !!data.session})
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {Dim} from 'lib/media/manip'
|
||||||
|
|
||||||
export class ImageSizesCache {
|
export class ImageSizesCache {
|
||||||
sizes: Map<string, Dim> = new Map()
|
sizes: Map<string, Dim> = new Map()
|
||||||
private activeRequests: Map<string, Promise<Dim>> = new Map()
|
activeRequests: Map<string, Promise<Dim>> = new Map()
|
||||||
|
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,12 @@
|
||||||
import {makeAutoObservable, runInAction} from 'mobx'
|
import {makeAutoObservable, runInAction} from 'mobx'
|
||||||
import {FollowRecord, AppBskyActorProfile, AppBskyActorRef} from '@atproto/api'
|
import {FollowRecord, AppBskyActorDefs} from '@atproto/api'
|
||||||
import {RootStoreModel} from '../root-store'
|
import {RootStoreModel} from '../root-store'
|
||||||
import {bundleAsync} from 'lib/async/bundle'
|
import {bundleAsync} from 'lib/async/bundle'
|
||||||
|
|
||||||
const CACHE_TTL = 1000 * 60 * 60 // hourly
|
const CACHE_TTL = 1000 * 60 * 60 // hourly
|
||||||
type FollowsListResponse = Awaited<ReturnType<FollowRecord['list']>>
|
type FollowsListResponse = Awaited<ReturnType<FollowRecord['list']>>
|
||||||
type FollowsListResponseRecord = FollowsListResponse['records'][0]
|
type FollowsListResponseRecord = FollowsListResponse['records'][0]
|
||||||
type Profile =
|
type Profile = AppBskyActorDefs.ProfileViewBasic | AppBskyActorDefs.ProfileView
|
||||||
| AppBskyActorProfile.ViewBasic
|
|
||||||
| AppBskyActorProfile.View
|
|
||||||
| AppBskyActorRef.WithInfo
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This model is used to maintain a synced local cache of the user's
|
* This model is used to maintain a synced local cache of the user's
|
||||||
|
@ -53,21 +50,21 @@ export class MyFollowsCache {
|
||||||
|
|
||||||
fetch = bundleAsync(async () => {
|
fetch = bundleAsync(async () => {
|
||||||
this.rootStore.log.debug('MyFollowsModel:fetch running full fetch')
|
this.rootStore.log.debug('MyFollowsModel:fetch running full fetch')
|
||||||
let before
|
let rkeyStart
|
||||||
let records: FollowsListResponseRecord[] = []
|
let records: FollowsListResponseRecord[] = []
|
||||||
do {
|
do {
|
||||||
const res: FollowsListResponse =
|
const res: FollowsListResponse =
|
||||||
await this.rootStore.api.app.bsky.graph.follow.list({
|
await this.rootStore.agent.app.bsky.graph.follow.list({
|
||||||
user: this.rootStore.me.did,
|
repo: this.rootStore.me.did,
|
||||||
before,
|
rkeyStart,
|
||||||
})
|
})
|
||||||
records = records.concat(res.records)
|
records = records.concat(res.records)
|
||||||
before = res.cursor
|
rkeyStart = res.cursor
|
||||||
} while (typeof before !== 'undefined')
|
} while (typeof rkeyStart !== 'undefined')
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.followDidToRecordMap = {}
|
this.followDidToRecordMap = {}
|
||||||
for (const record of records) {
|
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.lastSync = Date.now()
|
||||||
this.myDid = this.rootStore.me.did
|
this.myDid = this.rootStore.me.did
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import {AppBskyActorProfile, AppBskyActorRef} from '@atproto/api'
|
import {AppBskyActorDefs} from '@atproto/api'
|
||||||
import {makeAutoObservable, runInAction} from 'mobx'
|
import {makeAutoObservable, runInAction} from 'mobx'
|
||||||
import sampleSize from 'lodash.samplesize'
|
import sampleSize from 'lodash.samplesize'
|
||||||
import {bundleAsync} from 'lib/async/bundle'
|
import {bundleAsync} from 'lib/async/bundle'
|
||||||
import {RootStoreModel} from '../root-store'
|
import {RootStoreModel} from '../root-store'
|
||||||
|
|
||||||
export type RefWithInfoAndFollowers = AppBskyActorRef.WithInfo & {
|
export type RefWithInfoAndFollowers = AppBskyActorDefs.ProfileViewBasic & {
|
||||||
followers: AppBskyActorProfile.View[]
|
followers: AppBskyActorDefs.ProfileView[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProfileViewFollows = AppBskyActorProfile.View & {
|
export type ProfileViewFollows = AppBskyActorDefs.ProfileView & {
|
||||||
follows: AppBskyActorRef.WithInfo[]
|
follows: AppBskyActorDefs.ProfileViewBasic[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FoafsModel {
|
export class FoafsModel {
|
||||||
|
@ -51,14 +51,14 @@ export class FoafsModel {
|
||||||
this.popular.length = 0
|
this.popular.length = 0
|
||||||
|
|
||||||
// fetch their profiles
|
// fetch their profiles
|
||||||
const profiles = await this.rootStore.api.app.bsky.actor.getProfiles({
|
const profiles = await this.rootStore.agent.getProfiles({
|
||||||
actors: this.sources,
|
actors: this.sources,
|
||||||
})
|
})
|
||||||
|
|
||||||
// fetch their follows
|
// fetch their follows
|
||||||
const results = await Promise.allSettled(
|
const results = await Promise.allSettled(
|
||||||
this.sources.map(source =>
|
this.sources.map(source =>
|
||||||
this.rootStore.api.app.bsky.graph.getFollows({user: source}),
|
this.rootStore.agent.getFollows({actor: source}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import {makeAutoObservable, runInAction} from 'mobx'
|
import {makeAutoObservable, runInAction} from 'mobx'
|
||||||
import {AppBskyActorProfile as Profile} from '@atproto/api'
|
import {AppBskyActorDefs} from '@atproto/api'
|
||||||
import shuffle from 'lodash.shuffle'
|
import shuffle from 'lodash.shuffle'
|
||||||
import {RootStoreModel} from '../root-store'
|
import {RootStoreModel} from '../root-store'
|
||||||
import {cleanError} from 'lib/strings/errors'
|
import {cleanError} from 'lib/strings/errors'
|
||||||
|
@ -8,7 +8,9 @@ import {SUGGESTED_FOLLOWS} from 'lib/constants'
|
||||||
|
|
||||||
const PAGE_SIZE = 30
|
const PAGE_SIZE = 30
|
||||||
|
|
||||||
export type SuggestedActor = Profile.ViewBasic | Profile.View
|
export type SuggestedActor =
|
||||||
|
| AppBskyActorDefs.ProfileViewBasic
|
||||||
|
| AppBskyActorDefs.ProfileView
|
||||||
|
|
||||||
export class SuggestedActorsModel {
|
export class SuggestedActorsModel {
|
||||||
// state
|
// state
|
||||||
|
@ -20,7 +22,7 @@ export class SuggestedActorsModel {
|
||||||
hasMore = true
|
hasMore = true
|
||||||
loadMoreCursor?: string
|
loadMoreCursor?: string
|
||||||
|
|
||||||
private hardCodedSuggestions: SuggestedActor[] | undefined
|
hardCodedSuggestions: SuggestedActor[] | undefined
|
||||||
|
|
||||||
// data
|
// data
|
||||||
suggestions: SuggestedActor[] = []
|
suggestions: SuggestedActor[] = []
|
||||||
|
@ -82,7 +84,7 @@ export class SuggestedActorsModel {
|
||||||
this.loadMoreCursor = undefined
|
this.loadMoreCursor = undefined
|
||||||
} else {
|
} else {
|
||||||
// pull from the PDS' algo
|
// 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,
|
limit: this.pageSize,
|
||||||
cursor: this.loadMoreCursor,
|
cursor: this.loadMoreCursor,
|
||||||
})
|
})
|
||||||
|
@ -104,7 +106,7 @@ export class SuggestedActorsModel {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
private async fetchHardcodedSuggestions() {
|
async fetchHardcodedSuggestions() {
|
||||||
if (this.hardCodedSuggestions) {
|
if (this.hardCodedSuggestions) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -118,9 +120,9 @@ export class SuggestedActorsModel {
|
||||||
]
|
]
|
||||||
|
|
||||||
// fetch the profiles in chunks of 25 (the limit allowed by `getProfiles`)
|
// fetch the profiles in chunks of 25 (the limit allowed by `getProfiles`)
|
||||||
let profiles: Profile.View[] = []
|
let profiles: AppBskyActorDefs.ProfileView[] = []
|
||||||
do {
|
do {
|
||||||
const res = await this.rootStore.api.app.bsky.actor.getProfiles({
|
const res = await this.rootStore.agent.getProfiles({
|
||||||
actors: actors.splice(0, 25),
|
actors: actors.splice(0, 25),
|
||||||
})
|
})
|
||||||
profiles = profiles.concat(res.data.profiles)
|
profiles = profiles.concat(res.data.profiles)
|
||||||
|
@ -152,13 +154,13 @@ export class SuggestedActorsModel {
|
||||||
// state transitions
|
// state transitions
|
||||||
// =
|
// =
|
||||||
|
|
||||||
private _xLoading(isRefreshing = false) {
|
_xLoading(isRefreshing = false) {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
this.isRefreshing = isRefreshing
|
this.isRefreshing = isRefreshing
|
||||||
this.error = ''
|
this.error = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
private _xIdle(err?: any) {
|
_xIdle(err?: any) {
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
this.isRefreshing = false
|
this.isRefreshing = false
|
||||||
this.hasLoaded = true
|
this.hasLoaded = true
|
||||||
|
|
|
@ -1,32 +1,29 @@
|
||||||
import {makeAutoObservable, runInAction} from 'mobx'
|
import {makeAutoObservable, runInAction} from 'mobx'
|
||||||
import {
|
import {
|
||||||
AppBskyFeedGetTimeline as GetTimeline,
|
AppBskyFeedGetTimeline as GetTimeline,
|
||||||
AppBskyFeedFeedViewPost,
|
AppBskyFeedDefs,
|
||||||
AppBskyFeedPost,
|
AppBskyFeedPost,
|
||||||
AppBskyFeedGetAuthorFeed as GetAuthorFeed,
|
AppBskyFeedGetAuthorFeed as GetAuthorFeed,
|
||||||
|
RichText,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import AwaitLock from 'await-lock'
|
import AwaitLock from 'await-lock'
|
||||||
import {bundleAsync} from 'lib/async/bundle'
|
import {bundleAsync} from 'lib/async/bundle'
|
||||||
import sampleSize from 'lodash.samplesize'
|
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 {RootStoreModel} from './root-store'
|
||||||
import * as apilib from 'lib/api/index'
|
|
||||||
import {cleanError} from 'lib/strings/errors'
|
import {cleanError} from 'lib/strings/errors'
|
||||||
import {RichText} from 'lib/strings/rich-text'
|
|
||||||
import {SUGGESTED_FOLLOWS} from 'lib/constants'
|
import {SUGGESTED_FOLLOWS} from 'lib/constants'
|
||||||
import {
|
import {
|
||||||
getCombinedCursors,
|
getCombinedCursors,
|
||||||
getMultipleAuthorsPosts,
|
getMultipleAuthorsPosts,
|
||||||
mergePosts,
|
mergePosts,
|
||||||
} from 'lib/api/build-suggested-posts'
|
} from 'lib/api/build-suggested-posts'
|
||||||
|
|
||||||
import {FeedTuner, FeedViewPostsSlice} from 'lib/api/feed-manip'
|
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
|
let _idCounter = 0
|
||||||
|
|
||||||
export class FeedItemModel {
|
export class FeedItemModel {
|
||||||
|
@ -51,11 +48,7 @@ export class FeedItemModel {
|
||||||
const valid = AppBskyFeedPost.validateRecord(this.post.record)
|
const valid = AppBskyFeedPost.validateRecord(this.post.record)
|
||||||
if (valid.success) {
|
if (valid.success) {
|
||||||
this.postRecord = this.post.record
|
this.postRecord = this.post.record
|
||||||
this.richText = new RichText(
|
this.richText = new RichText(this.postRecord, {cleanNewlines: true})
|
||||||
this.postRecord.text,
|
|
||||||
this.postRecord.entities,
|
|
||||||
{cleanNewlines: true},
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
rootStore.log.warn(
|
rootStore.log.warn(
|
||||||
'Received an invalid app.bsky.feed.post record',
|
'Received an invalid app.bsky.feed.post record',
|
||||||
|
@ -82,7 +75,7 @@ export class FeedItemModel {
|
||||||
copyMetrics(v: FeedViewPost) {
|
copyMetrics(v: FeedViewPost) {
|
||||||
this.post.replyCount = v.post.replyCount
|
this.post.replyCount = v.post.replyCount
|
||||||
this.post.repostCount = v.post.repostCount
|
this.post.repostCount = v.post.repostCount
|
||||||
this.post.upvoteCount = v.post.upvoteCount
|
this.post.likeCount = v.post.likeCount
|
||||||
this.post.viewer = v.post.viewer
|
this.post.viewer = v.post.viewer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,68 +85,43 @@ export class FeedItemModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async toggleUpvote() {
|
async toggleLike() {
|
||||||
const wasUpvoted = !!this.post.viewer.upvote
|
if (this.post.viewer?.like) {
|
||||||
const wasDownvoted = !!this.post.viewer.downvote
|
await this.rootStore.agent.deleteLike(this.post.viewer.like)
|
||||||
const res = await this.rootStore.api.app.bsky.feed.setVote({
|
|
||||||
subject: {
|
|
||||||
uri: this.post.uri,
|
|
||||||
cid: this.post.cid,
|
|
||||||
},
|
|
||||||
direction: wasUpvoted ? 'none' : 'up',
|
|
||||||
})
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
if (wasDownvoted) {
|
this.post.likeCount = this.post.likeCount || 0
|
||||||
this.post.downvoteCount--
|
this.post.viewer = this.post.viewer || {}
|
||||||
}
|
this.post.likeCount--
|
||||||
if (wasUpvoted) {
|
this.post.viewer.like = undefined
|
||||||
this.post.upvoteCount--
|
})
|
||||||
} else {
|
} else {
|
||||||
this.post.upvoteCount++
|
const res = await this.rootStore.agent.like(this.post.uri, this.post.cid)
|
||||||
}
|
|
||||||
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(() => {
|
runInAction(() => {
|
||||||
if (wasUpvoted) {
|
this.post.likeCount = this.post.likeCount || 0
|
||||||
this.post.upvoteCount--
|
this.post.viewer = this.post.viewer || {}
|
||||||
}
|
this.post.likeCount++
|
||||||
if (wasDownvoted) {
|
this.post.viewer.like = res.uri
|
||||||
this.post.downvoteCount--
|
|
||||||
} else {
|
|
||||||
this.post.downvoteCount++
|
|
||||||
}
|
|
||||||
this.post.viewer.upvote = res.data.upvote
|
|
||||||
this.post.viewer.downvote = res.data.downvote
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async toggleRepost() {
|
async toggleRepost() {
|
||||||
if (this.post.viewer.repost) {
|
if (this.post.viewer?.repost) {
|
||||||
await apilib.unrepost(this.rootStore, this.post.viewer.repost)
|
await this.rootStore.agent.deleteRepost(this.post.viewer.repost)
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
|
this.post.repostCount = this.post.repostCount || 0
|
||||||
|
this.post.viewer = this.post.viewer || {}
|
||||||
this.post.repostCount--
|
this.post.repostCount--
|
||||||
this.post.viewer.repost = undefined
|
this.post.viewer.repost = undefined
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const res = await apilib.repost(
|
const res = await this.rootStore.agent.repost(
|
||||||
this.rootStore,
|
|
||||||
this.post.uri,
|
this.post.uri,
|
||||||
this.post.cid,
|
this.post.cid,
|
||||||
)
|
)
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
|
this.post.repostCount = this.post.repostCount || 0
|
||||||
|
this.post.viewer = this.post.viewer || {}
|
||||||
this.post.repostCount++
|
this.post.repostCount++
|
||||||
this.post.viewer.repost = res.uri
|
this.post.viewer.repost = res.uri
|
||||||
})
|
})
|
||||||
|
@ -161,10 +129,7 @@ export class FeedItemModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete() {
|
async delete() {
|
||||||
await this.rootStore.api.app.bsky.feed.post.delete({
|
await this.rootStore.agent.deletePost(this.post.uri)
|
||||||
did: this.post.author.did,
|
|
||||||
rkey: new AtUri(this.post.uri).rkey,
|
|
||||||
})
|
|
||||||
this.rootStore.emitPostDeleted(this.post.uri)
|
this.rootStore.emitPostDeleted(this.post.uri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -250,7 +215,7 @@ export class FeedModel {
|
||||||
tuner = new FeedTuner()
|
tuner = new FeedTuner()
|
||||||
|
|
||||||
// used to linearize async modifications to state
|
// used to linearize async modifications to state
|
||||||
private lock = new AwaitLock()
|
lock = new AwaitLock()
|
||||||
|
|
||||||
// data
|
// data
|
||||||
slices: FeedSliceModel[] = []
|
slices: FeedSliceModel[] = []
|
||||||
|
@ -291,8 +256,8 @@ export class FeedModel {
|
||||||
const params = this.params as GetAuthorFeed.QueryParams
|
const params = this.params as GetAuthorFeed.QueryParams
|
||||||
const item = slice.rootItem
|
const item = slice.rootItem
|
||||||
const isRepost =
|
const isRepost =
|
||||||
item?.reasonRepost?.by?.handle === params.author ||
|
item?.reasonRepost?.by?.handle === params.actor ||
|
||||||
item?.reasonRepost?.by?.did === params.author
|
item?.reasonRepost?.by?.did === params.actor
|
||||||
return (
|
return (
|
||||||
!item.reply || // not a reply
|
!item.reply || // not a reply
|
||||||
isRepost || // but allow if it's a repost
|
isRepost || // but allow if it's a repost
|
||||||
|
@ -338,7 +303,7 @@ export class FeedModel {
|
||||||
return this.setup()
|
return this.setup()
|
||||||
}
|
}
|
||||||
|
|
||||||
private get feedTuners() {
|
get feedTuners() {
|
||||||
if (this.feedType === 'goodstuff') {
|
if (this.feedType === 'goodstuff') {
|
||||||
return [
|
return [
|
||||||
FeedTuner.dedupReposts,
|
FeedTuner.dedupReposts,
|
||||||
|
@ -406,7 +371,7 @@ export class FeedModel {
|
||||||
this._xLoading()
|
this._xLoading()
|
||||||
try {
|
try {
|
||||||
const res = await this._getFeed({
|
const res = await this._getFeed({
|
||||||
before: this.loadMoreCursor,
|
cursor: this.loadMoreCursor,
|
||||||
limit: PAGE_SIZE,
|
limit: PAGE_SIZE,
|
||||||
})
|
})
|
||||||
await this._appendAll(res)
|
await this._appendAll(res)
|
||||||
|
@ -439,7 +404,7 @@ export class FeedModel {
|
||||||
try {
|
try {
|
||||||
do {
|
do {
|
||||||
const res: GetTimeline.Response = await this._getFeed({
|
const res: GetTimeline.Response = await this._getFeed({
|
||||||
before: cursor,
|
cursor,
|
||||||
limit: Math.min(numToFetch, 100),
|
limit: Math.min(numToFetch, 100),
|
||||||
})
|
})
|
||||||
if (res.data.feed.length === 0) {
|
if (res.data.feed.length === 0) {
|
||||||
|
@ -478,14 +443,18 @@ export class FeedModel {
|
||||||
new FeedSliceModel(this.rootStore, `item-${_idCounter++}`, slice),
|
new FeedSliceModel(this.rootStore, `item-${_idCounter++}`, slice),
|
||||||
)
|
)
|
||||||
if (autoPrepend) {
|
if (autoPrepend) {
|
||||||
|
runInAction(() => {
|
||||||
this.slices = nextSlicesModels.concat(
|
this.slices = nextSlicesModels.concat(
|
||||||
this.slices.filter(slice1 =>
|
this.slices.filter(slice1 =>
|
||||||
nextSlicesModels.find(slice2 => slice1.uri === slice2.uri),
|
nextSlicesModels.find(slice2 => slice1.uri === slice2.uri),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
this.setHasNewLatest(false)
|
this.setHasNewLatest(false)
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
|
runInAction(() => {
|
||||||
this.nextSlices = nextSlicesModels
|
this.nextSlices = nextSlicesModels
|
||||||
|
})
|
||||||
this.setHasNewLatest(true)
|
this.setHasNewLatest(true)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -519,13 +488,13 @@ export class FeedModel {
|
||||||
// state transitions
|
// state transitions
|
||||||
// =
|
// =
|
||||||
|
|
||||||
private _xLoading(isRefreshing = false) {
|
_xLoading(isRefreshing = false) {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
this.isRefreshing = isRefreshing
|
this.isRefreshing = isRefreshing
|
||||||
this.error = ''
|
this.error = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
private _xIdle(err?: any) {
|
_xIdle(err?: any) {
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
this.isRefreshing = false
|
this.isRefreshing = false
|
||||||
this.hasLoaded = true
|
this.hasLoaded = true
|
||||||
|
@ -538,14 +507,12 @@ export class FeedModel {
|
||||||
// helper functions
|
// helper functions
|
||||||
// =
|
// =
|
||||||
|
|
||||||
private async _replaceAll(
|
async _replaceAll(res: GetTimeline.Response | GetAuthorFeed.Response) {
|
||||||
res: GetTimeline.Response | GetAuthorFeed.Response,
|
|
||||||
) {
|
|
||||||
this.pollCursor = res.data.feed[0]?.post.uri
|
this.pollCursor = res.data.feed[0]?.post.uri
|
||||||
return this._appendAll(res, true)
|
return this._appendAll(res, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _appendAll(
|
async _appendAll(
|
||||||
res: GetTimeline.Response | GetAuthorFeed.Response,
|
res: GetTimeline.Response | GetAuthorFeed.Response,
|
||||||
replace = false,
|
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) {
|
for (const item of res.data.feed) {
|
||||||
const existingSlice = this.slices.find(slice =>
|
const existingSlice = this.slices.find(slice =>
|
||||||
slice.containsUri(item.post.uri),
|
slice.containsUri(item.post.uri),
|
||||||
|
@ -596,7 +563,7 @@ export class FeedModel {
|
||||||
const responses = await getMultipleAuthorsPosts(
|
const responses = await getMultipleAuthorsPosts(
|
||||||
this.rootStore,
|
this.rootStore,
|
||||||
sampleSize(SUGGESTED_FOLLOWS(String(this.rootStore.agent.service)), 20),
|
sampleSize(SUGGESTED_FOLLOWS(String(this.rootStore.agent.service)), 20),
|
||||||
params.before,
|
params.cursor,
|
||||||
20,
|
20,
|
||||||
)
|
)
|
||||||
const combinedCursor = getCombinedCursors(responses)
|
const combinedCursor = getCombinedCursors(responses)
|
||||||
|
@ -611,9 +578,7 @@ export class FeedModel {
|
||||||
headers: lastHeaders,
|
headers: lastHeaders,
|
||||||
}
|
}
|
||||||
} else if (this.feedType === 'home') {
|
} else if (this.feedType === 'home') {
|
||||||
return this.rootStore.api.app.bsky.feed.getTimeline(
|
return this.rootStore.agent.getTimeline(params as GetTimeline.QueryParams)
|
||||||
params as GetTimeline.QueryParams,
|
|
||||||
)
|
|
||||||
} else if (this.feedType === 'goodstuff') {
|
} else if (this.feedType === 'goodstuff') {
|
||||||
const res = await getGoodStuff(
|
const res = await getGoodStuff(
|
||||||
this.rootStore.session.currentSession?.accessJwt || '',
|
this.rootStore.session.currentSession?.accessJwt || '',
|
||||||
|
@ -624,7 +589,7 @@ export class FeedModel {
|
||||||
)
|
)
|
||||||
return res
|
return res
|
||||||
} else {
|
} else {
|
||||||
return this.rootStore.api.app.bsky.feed.getAuthorFeed(
|
return this.rootStore.agent.getAuthorFeed(
|
||||||
params as GetAuthorFeed.QueryParams,
|
params as GetAuthorFeed.QueryParams,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import {makeAutoObservable, runInAction} from 'mobx'
|
import {makeAutoObservable, runInAction} from 'mobx'
|
||||||
import {AtUri} from '../../third-party/uri'
|
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 {RootStoreModel} from './root-store'
|
||||||
import {cleanError} from 'lib/strings/errors'
|
import {cleanError} from 'lib/strings/errors'
|
||||||
import {bundleAsync} from 'lib/async/bundle'
|
import {bundleAsync} from 'lib/async/bundle'
|
||||||
|
@ -8,24 +8,24 @@ import * as apilib from 'lib/api/index'
|
||||||
|
|
||||||
const PAGE_SIZE = 30
|
const PAGE_SIZE = 30
|
||||||
|
|
||||||
export type VoteItem = GetVotes.Vote
|
export type LikeItem = GetLikes.Like
|
||||||
|
|
||||||
export class VotesViewModel {
|
export class LikesViewModel {
|
||||||
// state
|
// state
|
||||||
isLoading = false
|
isLoading = false
|
||||||
isRefreshing = false
|
isRefreshing = false
|
||||||
hasLoaded = false
|
hasLoaded = false
|
||||||
error = ''
|
error = ''
|
||||||
resolvedUri = ''
|
resolvedUri = ''
|
||||||
params: GetVotes.QueryParams
|
params: GetLikes.QueryParams
|
||||||
hasMore = true
|
hasMore = true
|
||||||
loadMoreCursor?: string
|
loadMoreCursor?: string
|
||||||
|
|
||||||
// data
|
// data
|
||||||
uri: string = ''
|
uri: string = ''
|
||||||
votes: VoteItem[] = []
|
likes: LikeItem[] = []
|
||||||
|
|
||||||
constructor(public rootStore: RootStoreModel, params: GetVotes.QueryParams) {
|
constructor(public rootStore: RootStoreModel, params: GetLikes.QueryParams) {
|
||||||
makeAutoObservable(
|
makeAutoObservable(
|
||||||
this,
|
this,
|
||||||
{
|
{
|
||||||
|
@ -68,9 +68,9 @@ export class VotesViewModel {
|
||||||
const params = Object.assign({}, this.params, {
|
const params = Object.assign({}, this.params, {
|
||||||
uri: this.resolvedUri,
|
uri: this.resolvedUri,
|
||||||
limit: PAGE_SIZE,
|
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) {
|
if (replace) {
|
||||||
this._replaceAll(res)
|
this._replaceAll(res)
|
||||||
} else {
|
} else {
|
||||||
|
@ -85,13 +85,13 @@ export class VotesViewModel {
|
||||||
// state transitions
|
// state transitions
|
||||||
// =
|
// =
|
||||||
|
|
||||||
private _xLoading(isRefreshing = false) {
|
_xLoading(isRefreshing = false) {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
this.isRefreshing = isRefreshing
|
this.isRefreshing = isRefreshing
|
||||||
this.error = ''
|
this.error = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
private _xIdle(err?: any) {
|
_xIdle(err?: any) {
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
this.isRefreshing = false
|
this.isRefreshing = false
|
||||||
this.hasLoaded = true
|
this.hasLoaded = true
|
||||||
|
@ -104,7 +104,7 @@ export class VotesViewModel {
|
||||||
// helper functions
|
// helper functions
|
||||||
// =
|
// =
|
||||||
|
|
||||||
private async _resolveUri() {
|
async _resolveUri() {
|
||||||
const urip = new AtUri(this.params.uri)
|
const urip = new AtUri(this.params.uri)
|
||||||
if (!urip.host.startsWith('did:')) {
|
if (!urip.host.startsWith('did:')) {
|
||||||
try {
|
try {
|
||||||
|
@ -118,14 +118,14 @@ export class VotesViewModel {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private _replaceAll(res: GetVotes.Response) {
|
_replaceAll(res: GetLikes.Response) {
|
||||||
this.votes = []
|
this.likes = []
|
||||||
this._appendAll(res)
|
this._appendAll(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
private _appendAll(res: GetVotes.Response) {
|
_appendAll(res: GetLikes.Response) {
|
||||||
this.loadMoreCursor = res.data.cursor
|
this.loadMoreCursor = res.data.cursor
|
||||||
this.hasMore = !!this.loadMoreCursor
|
this.hasMore = !!this.loadMoreCursor
|
||||||
this.votes = this.votes.concat(res.data.votes)
|
this.likes = this.likes.concat(res.data.likes)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import {makeAutoObservable} from 'mobx'
|
import {makeAutoObservable} from 'mobx'
|
||||||
import {XRPCError, XRPCInvalidResponseError} from '@atproto/xrpc'
|
// import {XRPCError, XRPCInvalidResponseError} from '@atproto/xrpc' TODO
|
||||||
|
|
||||||
const MAX_ENTRIES = 300
|
const MAX_ENTRIES = 300
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ export class LogModel {
|
||||||
makeAutoObservable(this)
|
makeAutoObservable(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
private add(entry: LogEntry) {
|
add(entry: LogEntry) {
|
||||||
this.entries.push(entry)
|
this.entries.push(entry)
|
||||||
while (this.entries.length > MAX_ENTRIES) {
|
while (this.entries.length > MAX_ENTRIES) {
|
||||||
this.entries = this.entries.slice(50)
|
this.entries = this.entries.slice(50)
|
||||||
|
@ -79,14 +79,14 @@ export class LogModel {
|
||||||
function detailsToStr(details?: any) {
|
function detailsToStr(details?: any) {
|
||||||
if (details && typeof details !== 'string') {
|
if (details && typeof details !== 'string') {
|
||||||
if (
|
if (
|
||||||
details instanceof XRPCInvalidResponseError ||
|
// details instanceof XRPCInvalidResponseError || TODO
|
||||||
details.constructor.name === 'XRPCInvalidResponseError'
|
details.constructor.name === 'XRPCInvalidResponseError'
|
||||||
) {
|
) {
|
||||||
return `The server gave an ill-formatted response.\nMethod: ${
|
return `The server gave an ill-formatted response.\nMethod: ${
|
||||||
details.lexiconNsid
|
details.lexiconNsid
|
||||||
}.\nError: ${details.validationError.toString()}`
|
}.\nError: ${details.validationError.toString()}`
|
||||||
} else if (
|
} else if (
|
||||||
details instanceof XRPCError ||
|
// details instanceof XRPCError || TODO
|
||||||
details.constructor.name === 'XRPCError'
|
details.constructor.name === 'XRPCError'
|
||||||
) {
|
) {
|
||||||
return `An XRPC error occurred.\nStatus: ${details.status}\nError: ${details.error}\nMessage: ${details.message}`
|
return `An XRPC error occurred.\nStatus: ${details.status}\nError: ${details.error}\nMessage: ${details.message}`
|
||||||
|
|
|
@ -85,7 +85,7 @@ export class MeModel {
|
||||||
if (sess.hasSession) {
|
if (sess.hasSession) {
|
||||||
this.did = sess.currentSession?.did || ''
|
this.did = sess.currentSession?.did || ''
|
||||||
this.handle = sess.currentSession?.handle || ''
|
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,
|
actor: this.did,
|
||||||
})
|
})
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import {makeAutoObservable, runInAction} from 'mobx'
|
import {makeAutoObservable, runInAction} from 'mobx'
|
||||||
import {
|
import {
|
||||||
AppBskyNotificationList as ListNotifications,
|
AppBskyNotificationListNotifications as ListNotifications,
|
||||||
AppBskyActorRef as ActorRef,
|
AppBskyActorDefs,
|
||||||
AppBskyFeedPost,
|
AppBskyFeedPost,
|
||||||
AppBskyFeedRepost,
|
AppBskyFeedRepost,
|
||||||
AppBskyFeedVote,
|
AppBskyFeedLike,
|
||||||
AppBskyGraphAssertion,
|
|
||||||
AppBskyGraphFollow,
|
AppBskyGraphFollow,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import AwaitLock from 'await-lock'
|
import AwaitLock from 'await-lock'
|
||||||
|
@ -28,8 +27,7 @@ export interface GroupedNotification extends ListNotifications.Notification {
|
||||||
type SupportedRecord =
|
type SupportedRecord =
|
||||||
| AppBskyFeedPost.Record
|
| AppBskyFeedPost.Record
|
||||||
| AppBskyFeedRepost.Record
|
| AppBskyFeedRepost.Record
|
||||||
| AppBskyFeedVote.Record
|
| AppBskyFeedLike.Record
|
||||||
| AppBskyGraphAssertion.Record
|
|
||||||
| AppBskyGraphFollow.Record
|
| AppBskyGraphFollow.Record
|
||||||
|
|
||||||
export class NotificationsViewItemModel {
|
export class NotificationsViewItemModel {
|
||||||
|
@ -39,11 +37,10 @@ export class NotificationsViewItemModel {
|
||||||
// data
|
// data
|
||||||
uri: string = ''
|
uri: string = ''
|
||||||
cid: string = ''
|
cid: string = ''
|
||||||
author: ActorRef.WithInfo = {
|
author: AppBskyActorDefs.ProfileViewBasic = {
|
||||||
did: '',
|
did: '',
|
||||||
handle: '',
|
handle: '',
|
||||||
avatar: '',
|
avatar: '',
|
||||||
declaration: {cid: '', actorType: ''},
|
|
||||||
}
|
}
|
||||||
reason: string = ''
|
reason: string = ''
|
||||||
reasonSubject?: string
|
reasonSubject?: string
|
||||||
|
@ -86,8 +83,8 @@ export class NotificationsViewItemModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get isUpvote() {
|
get isLike() {
|
||||||
return this.reason === 'vote'
|
return this.reason === 'like'
|
||||||
}
|
}
|
||||||
|
|
||||||
get isRepost() {
|
get isRepost() {
|
||||||
|
@ -102,16 +99,22 @@ export class NotificationsViewItemModel {
|
||||||
return this.reason === 'reply'
|
return this.reason === 'reply'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isQuote() {
|
||||||
|
return this.reason === 'quote'
|
||||||
|
}
|
||||||
|
|
||||||
get isFollow() {
|
get isFollow() {
|
||||||
return this.reason === 'follow'
|
return this.reason === 'follow'
|
||||||
}
|
}
|
||||||
|
|
||||||
get isAssertion() {
|
|
||||||
return this.reason === 'assertion'
|
|
||||||
}
|
|
||||||
|
|
||||||
get needsAdditionalData() {
|
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 !this.additionalPost
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
@ -124,7 +127,7 @@ export class NotificationsViewItemModel {
|
||||||
const record = this.record
|
const record = this.record
|
||||||
if (
|
if (
|
||||||
AppBskyFeedRepost.isRecord(record) ||
|
AppBskyFeedRepost.isRecord(record) ||
|
||||||
AppBskyFeedVote.isRecord(record)
|
AppBskyFeedLike.isRecord(record)
|
||||||
) {
|
) {
|
||||||
return record.subject.uri
|
return record.subject.uri
|
||||||
}
|
}
|
||||||
|
@ -135,8 +138,7 @@ export class NotificationsViewItemModel {
|
||||||
for (const ns of [
|
for (const ns of [
|
||||||
AppBskyFeedPost,
|
AppBskyFeedPost,
|
||||||
AppBskyFeedRepost,
|
AppBskyFeedRepost,
|
||||||
AppBskyFeedVote,
|
AppBskyFeedLike,
|
||||||
AppBskyGraphAssertion,
|
|
||||||
AppBskyGraphFollow,
|
AppBskyGraphFollow,
|
||||||
]) {
|
]) {
|
||||||
if (ns.isRecord(v)) {
|
if (ns.isRecord(v)) {
|
||||||
|
@ -163,9 +165,9 @@ export class NotificationsViewItemModel {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let postUri
|
let postUri
|
||||||
if (this.isReply || this.isMention) {
|
if (this.isReply || this.isQuote || this.isMention) {
|
||||||
postUri = this.uri
|
postUri = this.uri
|
||||||
} else if (this.isUpvote || this.isRepost) {
|
} else if (this.isLike || this.isRepost) {
|
||||||
postUri = this.subjectUri
|
postUri = this.subjectUri
|
||||||
}
|
}
|
||||||
if (postUri) {
|
if (postUri) {
|
||||||
|
@ -194,7 +196,7 @@ export class NotificationsViewModel {
|
||||||
loadMoreCursor?: string
|
loadMoreCursor?: string
|
||||||
|
|
||||||
// used to linearize async modifications to state
|
// used to linearize async modifications to state
|
||||||
private lock = new AwaitLock()
|
lock = new AwaitLock()
|
||||||
|
|
||||||
// data
|
// data
|
||||||
notifications: NotificationsViewItemModel[] = []
|
notifications: NotificationsViewItemModel[] = []
|
||||||
|
@ -266,7 +268,7 @@ export class NotificationsViewModel {
|
||||||
const params = Object.assign({}, this.params, {
|
const params = Object.assign({}, this.params, {
|
||||||
limit: PAGE_SIZE,
|
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)
|
await this._replaceAll(res)
|
||||||
this._xIdle()
|
this._xIdle()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
@ -297,9 +299,9 @@ export class NotificationsViewModel {
|
||||||
try {
|
try {
|
||||||
const params = Object.assign({}, this.params, {
|
const params = Object.assign({}, this.params, {
|
||||||
limit: PAGE_SIZE,
|
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)
|
await this._appendAll(res)
|
||||||
this._xIdle()
|
this._xIdle()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
@ -325,7 +327,7 @@ export class NotificationsViewModel {
|
||||||
try {
|
try {
|
||||||
this._xLoading()
|
this._xLoading()
|
||||||
try {
|
try {
|
||||||
const res = await this.rootStore.api.app.bsky.notification.list({
|
const res = await this.rootStore.agent.listNotifications({
|
||||||
limit: PAGE_SIZE,
|
limit: PAGE_SIZE,
|
||||||
})
|
})
|
||||||
await this._prependAll(res)
|
await this._prependAll(res)
|
||||||
|
@ -357,8 +359,8 @@ export class NotificationsViewModel {
|
||||||
try {
|
try {
|
||||||
do {
|
do {
|
||||||
const res: ListNotifications.Response =
|
const res: ListNotifications.Response =
|
||||||
await this.rootStore.api.app.bsky.notification.list({
|
await this.rootStore.agent.listNotifications({
|
||||||
before: cursor,
|
cursor,
|
||||||
limit: Math.min(numToFetch, 100),
|
limit: Math.min(numToFetch, 100),
|
||||||
})
|
})
|
||||||
if (res.data.notifications.length === 0) {
|
if (res.data.notifications.length === 0) {
|
||||||
|
@ -390,7 +392,7 @@ export class NotificationsViewModel {
|
||||||
*/
|
*/
|
||||||
loadUnreadCount = bundleAsync(async () => {
|
loadUnreadCount = bundleAsync(async () => {
|
||||||
const old = this.unreadCount
|
const old = this.unreadCount
|
||||||
const res = await this.rootStore.api.app.bsky.notification.getCount()
|
const res = await this.rootStore.agent.countUnreadNotifications()
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.unreadCount = res.data.count
|
this.unreadCount = res.data.count
|
||||||
})
|
})
|
||||||
|
@ -408,9 +410,7 @@ export class NotificationsViewModel {
|
||||||
for (const notif of this.notifications) {
|
for (const notif of this.notifications) {
|
||||||
notif.isRead = true
|
notif.isRead = true
|
||||||
}
|
}
|
||||||
await this.rootStore.api.app.bsky.notification.updateSeen({
|
await this.rootStore.agent.updateSeenNotifications()
|
||||||
seenAt: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.rootStore.log.warn('Failed to update notifications read state', e)
|
this.rootStore.log.warn('Failed to update notifications read state', e)
|
||||||
}
|
}
|
||||||
|
@ -418,7 +418,7 @@ export class NotificationsViewModel {
|
||||||
|
|
||||||
async getNewMostRecent(): Promise<NotificationsViewItemModel | undefined> {
|
async getNewMostRecent(): Promise<NotificationsViewItemModel | undefined> {
|
||||||
let old = this.mostRecentNotificationUri
|
let old = this.mostRecentNotificationUri
|
||||||
const res = await this.rootStore.api.app.bsky.notification.list({
|
const res = await this.rootStore.agent.listNotifications({
|
||||||
limit: 1,
|
limit: 1,
|
||||||
})
|
})
|
||||||
if (!res.data.notifications[0] || old === res.data.notifications[0].uri) {
|
if (!res.data.notifications[0] || old === res.data.notifications[0].uri) {
|
||||||
|
@ -437,13 +437,13 @@ export class NotificationsViewModel {
|
||||||
// state transitions
|
// state transitions
|
||||||
// =
|
// =
|
||||||
|
|
||||||
private _xLoading(isRefreshing = false) {
|
_xLoading(isRefreshing = false) {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
this.isRefreshing = isRefreshing
|
this.isRefreshing = isRefreshing
|
||||||
this.error = ''
|
this.error = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
private _xIdle(err?: any) {
|
_xIdle(err?: any) {
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
this.isRefreshing = false
|
this.isRefreshing = false
|
||||||
this.hasLoaded = true
|
this.hasLoaded = true
|
||||||
|
@ -456,14 +456,14 @@ export class NotificationsViewModel {
|
||||||
// helper functions
|
// helper functions
|
||||||
// =
|
// =
|
||||||
|
|
||||||
private async _replaceAll(res: ListNotifications.Response) {
|
async _replaceAll(res: ListNotifications.Response) {
|
||||||
if (res.data.notifications[0]) {
|
if (res.data.notifications[0]) {
|
||||||
this.mostRecentNotificationUri = res.data.notifications[0].uri
|
this.mostRecentNotificationUri = res.data.notifications[0].uri
|
||||||
}
|
}
|
||||||
return this._appendAll(res, true)
|
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.loadMoreCursor = res.data.cursor
|
||||||
this.hasMore = !!this.loadMoreCursor
|
this.hasMore = !!this.loadMoreCursor
|
||||||
const promises = []
|
const promises = []
|
||||||
|
@ -494,7 +494,7 @@ export class NotificationsViewModel {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _prependAll(res: ListNotifications.Response) {
|
async _prependAll(res: ListNotifications.Response) {
|
||||||
const promises = []
|
const promises = []
|
||||||
const itemModels: NotificationsViewItemModel[] = []
|
const itemModels: NotificationsViewItemModel[] = []
|
||||||
const dedupedNotifs = res.data.notifications.filter(
|
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) {
|
for (const item of res.data.notifications) {
|
||||||
const existingItem = this.notifications.find(item2 => isEq(item, item2))
|
const existingItem = this.notifications.find(item2 => isEq(item, item2))
|
||||||
if (existingItem) {
|
if (existingItem) {
|
||||||
|
|
|
@ -2,12 +2,13 @@ import {makeAutoObservable, runInAction} from 'mobx'
|
||||||
import {
|
import {
|
||||||
AppBskyFeedGetPostThread as GetPostThread,
|
AppBskyFeedGetPostThread as GetPostThread,
|
||||||
AppBskyFeedPost as FeedPost,
|
AppBskyFeedPost as FeedPost,
|
||||||
|
AppBskyFeedDefs,
|
||||||
|
RichText,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {AtUri} from '../../third-party/uri'
|
import {AtUri} from '../../third-party/uri'
|
||||||
import {RootStoreModel} from './root-store'
|
import {RootStoreModel} from './root-store'
|
||||||
import * as apilib from 'lib/api/index'
|
import * as apilib from 'lib/api/index'
|
||||||
import {cleanError} from 'lib/strings/errors'
|
import {cleanError} from 'lib/strings/errors'
|
||||||
import {RichText} from 'lib/strings/rich-text'
|
|
||||||
|
|
||||||
function* reactKeyGenerator(): Generator<string> {
|
function* reactKeyGenerator(): Generator<string> {
|
||||||
let counter = 0
|
let counter = 0
|
||||||
|
@ -26,10 +27,10 @@ export class PostThreadViewPostModel {
|
||||||
_hasMore = false
|
_hasMore = false
|
||||||
|
|
||||||
// data
|
// data
|
||||||
post: FeedPost.View
|
post: AppBskyFeedDefs.PostView
|
||||||
postRecord?: FeedPost.Record
|
postRecord?: FeedPost.Record
|
||||||
parent?: PostThreadViewPostModel | GetPostThread.NotFoundPost
|
parent?: PostThreadViewPostModel | AppBskyFeedDefs.NotFoundPost
|
||||||
replies?: (PostThreadViewPostModel | GetPostThread.NotFoundPost)[]
|
replies?: (PostThreadViewPostModel | AppBskyFeedDefs.NotFoundPost)[]
|
||||||
richText?: RichText
|
richText?: RichText
|
||||||
|
|
||||||
get uri() {
|
get uri() {
|
||||||
|
@ -43,7 +44,7 @@ export class PostThreadViewPostModel {
|
||||||
constructor(
|
constructor(
|
||||||
public rootStore: RootStoreModel,
|
public rootStore: RootStoreModel,
|
||||||
reactKey: string,
|
reactKey: string,
|
||||||
v: GetPostThread.ThreadViewPost,
|
v: AppBskyFeedDefs.ThreadViewPost,
|
||||||
) {
|
) {
|
||||||
this._reactKey = reactKey
|
this._reactKey = reactKey
|
||||||
this.post = v.post
|
this.post = v.post
|
||||||
|
@ -51,11 +52,7 @@ export class PostThreadViewPostModel {
|
||||||
const valid = FeedPost.validateRecord(this.post.record)
|
const valid = FeedPost.validateRecord(this.post.record)
|
||||||
if (valid.success) {
|
if (valid.success) {
|
||||||
this.postRecord = this.post.record
|
this.postRecord = this.post.record
|
||||||
this.richText = new RichText(
|
this.richText = new RichText(this.postRecord, {cleanNewlines: true})
|
||||||
this.postRecord.text,
|
|
||||||
this.postRecord.entities,
|
|
||||||
{cleanNewlines: true},
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
rootStore.log.warn(
|
rootStore.log.warn(
|
||||||
'Received an invalid app.bsky.feed.post record',
|
'Received an invalid app.bsky.feed.post record',
|
||||||
|
@ -74,14 +71,14 @@ export class PostThreadViewPostModel {
|
||||||
|
|
||||||
assignTreeModels(
|
assignTreeModels(
|
||||||
keyGen: Generator<string>,
|
keyGen: Generator<string>,
|
||||||
v: GetPostThread.ThreadViewPost,
|
v: AppBskyFeedDefs.ThreadViewPost,
|
||||||
higlightedPostUri: string,
|
higlightedPostUri: string,
|
||||||
includeParent = true,
|
includeParent = true,
|
||||||
includeChildren = true,
|
includeChildren = true,
|
||||||
) {
|
) {
|
||||||
// parents
|
// parents
|
||||||
if (includeParent && v.parent) {
|
if (includeParent && v.parent) {
|
||||||
if (GetPostThread.isThreadViewPost(v.parent)) {
|
if (AppBskyFeedDefs.isThreadViewPost(v.parent)) {
|
||||||
const parentModel = new PostThreadViewPostModel(
|
const parentModel = new PostThreadViewPostModel(
|
||||||
this.rootStore,
|
this.rootStore,
|
||||||
keyGen.next().value,
|
keyGen.next().value,
|
||||||
|
@ -100,7 +97,7 @@ export class PostThreadViewPostModel {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
this.parent = parentModel
|
this.parent = parentModel
|
||||||
} else if (GetPostThread.isNotFoundPost(v.parent)) {
|
} else if (AppBskyFeedDefs.isNotFoundPost(v.parent)) {
|
||||||
this.parent = v.parent
|
this.parent = v.parent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -108,7 +105,7 @@ export class PostThreadViewPostModel {
|
||||||
if (includeChildren && v.replies) {
|
if (includeChildren && v.replies) {
|
||||||
const replies = []
|
const replies = []
|
||||||
for (const item of v.replies) {
|
for (const item of v.replies) {
|
||||||
if (GetPostThread.isThreadViewPost(item)) {
|
if (AppBskyFeedDefs.isThreadViewPost(item)) {
|
||||||
const itemModel = new PostThreadViewPostModel(
|
const itemModel = new PostThreadViewPostModel(
|
||||||
this.rootStore,
|
this.rootStore,
|
||||||
keyGen.next().value,
|
keyGen.next().value,
|
||||||
|
@ -128,7 +125,7 @@ export class PostThreadViewPostModel {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
replies.push(itemModel)
|
replies.push(itemModel)
|
||||||
} else if (GetPostThread.isNotFoundPost(item)) {
|
} else if (AppBskyFeedDefs.isNotFoundPost(item)) {
|
||||||
replies.push(item)
|
replies.push(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -136,68 +133,43 @@ export class PostThreadViewPostModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async toggleUpvote() {
|
async toggleLike() {
|
||||||
const wasUpvoted = !!this.post.viewer.upvote
|
if (this.post.viewer?.like) {
|
||||||
const wasDownvoted = !!this.post.viewer.downvote
|
await this.rootStore.agent.deleteLike(this.post.viewer.like)
|
||||||
const res = await this.rootStore.api.app.bsky.feed.setVote({
|
|
||||||
subject: {
|
|
||||||
uri: this.post.uri,
|
|
||||||
cid: this.post.cid,
|
|
||||||
},
|
|
||||||
direction: wasUpvoted ? 'none' : 'up',
|
|
||||||
})
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
if (wasDownvoted) {
|
this.post.likeCount = this.post.likeCount || 0
|
||||||
this.post.downvoteCount--
|
this.post.viewer = this.post.viewer || {}
|
||||||
}
|
this.post.likeCount--
|
||||||
if (wasUpvoted) {
|
this.post.viewer.like = undefined
|
||||||
this.post.upvoteCount--
|
})
|
||||||
} else {
|
} else {
|
||||||
this.post.upvoteCount++
|
const res = await this.rootStore.agent.like(this.post.uri, this.post.cid)
|
||||||
}
|
|
||||||
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(() => {
|
runInAction(() => {
|
||||||
if (wasUpvoted) {
|
this.post.likeCount = this.post.likeCount || 0
|
||||||
this.post.upvoteCount--
|
this.post.viewer = this.post.viewer || {}
|
||||||
}
|
this.post.likeCount++
|
||||||
if (wasDownvoted) {
|
this.post.viewer.like = res.uri
|
||||||
this.post.downvoteCount--
|
|
||||||
} else {
|
|
||||||
this.post.downvoteCount++
|
|
||||||
}
|
|
||||||
this.post.viewer.upvote = res.data.upvote
|
|
||||||
this.post.viewer.downvote = res.data.downvote
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async toggleRepost() {
|
async toggleRepost() {
|
||||||
if (this.post.viewer.repost) {
|
if (this.post.viewer?.repost) {
|
||||||
await apilib.unrepost(this.rootStore, this.post.viewer.repost)
|
await this.rootStore.agent.deleteRepost(this.post.viewer.repost)
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
|
this.post.repostCount = this.post.repostCount || 0
|
||||||
|
this.post.viewer = this.post.viewer || {}
|
||||||
this.post.repostCount--
|
this.post.repostCount--
|
||||||
this.post.viewer.repost = undefined
|
this.post.viewer.repost = undefined
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const res = await apilib.repost(
|
const res = await this.rootStore.agent.repost(
|
||||||
this.rootStore,
|
|
||||||
this.post.uri,
|
this.post.uri,
|
||||||
this.post.cid,
|
this.post.cid,
|
||||||
)
|
)
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
|
this.post.repostCount = this.post.repostCount || 0
|
||||||
|
this.post.viewer = this.post.viewer || {}
|
||||||
this.post.repostCount++
|
this.post.repostCount++
|
||||||
this.post.viewer.repost = res.uri
|
this.post.viewer.repost = res.uri
|
||||||
})
|
})
|
||||||
|
@ -205,10 +177,7 @@ export class PostThreadViewPostModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete() {
|
async delete() {
|
||||||
await this.rootStore.api.app.bsky.feed.post.delete({
|
await this.rootStore.agent.deletePost(this.post.uri)
|
||||||
did: this.post.author.did,
|
|
||||||
rkey: new AtUri(this.post.uri).rkey,
|
|
||||||
})
|
|
||||||
this.rootStore.emitPostDeleted(this.post.uri)
|
this.rootStore.emitPostDeleted(this.post.uri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -301,14 +270,14 @@ export class PostThreadViewModel {
|
||||||
// state transitions
|
// state transitions
|
||||||
// =
|
// =
|
||||||
|
|
||||||
private _xLoading(isRefreshing = false) {
|
_xLoading(isRefreshing = false) {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
this.isRefreshing = isRefreshing
|
this.isRefreshing = isRefreshing
|
||||||
this.error = ''
|
this.error = ''
|
||||||
this.notFound = false
|
this.notFound = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private _xIdle(err?: any) {
|
_xIdle(err?: any) {
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
this.isRefreshing = false
|
this.isRefreshing = false
|
||||||
this.hasLoaded = true
|
this.hasLoaded = true
|
||||||
|
@ -322,7 +291,7 @@ export class PostThreadViewModel {
|
||||||
// loader functions
|
// loader functions
|
||||||
// =
|
// =
|
||||||
|
|
||||||
private async _resolveUri() {
|
async _resolveUri() {
|
||||||
const urip = new AtUri(this.params.uri)
|
const urip = new AtUri(this.params.uri)
|
||||||
if (!urip.host.startsWith('did:')) {
|
if (!urip.host.startsWith('did:')) {
|
||||||
try {
|
try {
|
||||||
|
@ -336,10 +305,10 @@ export class PostThreadViewModel {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _load(isRefreshing = false) {
|
async _load(isRefreshing = false) {
|
||||||
this._xLoading(isRefreshing)
|
this._xLoading(isRefreshing)
|
||||||
try {
|
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}),
|
Object.assign({}, this.params, {uri: this.resolvedUri}),
|
||||||
)
|
)
|
||||||
this._replaceAll(res)
|
this._replaceAll(res)
|
||||||
|
@ -349,18 +318,18 @@ export class PostThreadViewModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _replaceAll(res: GetPostThread.Response) {
|
_replaceAll(res: GetPostThread.Response) {
|
||||||
sortThread(res.data.thread)
|
sortThread(res.data.thread)
|
||||||
const keyGen = reactKeyGenerator()
|
const keyGen = reactKeyGenerator()
|
||||||
const thread = new PostThreadViewPostModel(
|
const thread = new PostThreadViewPostModel(
|
||||||
this.rootStore,
|
this.rootStore,
|
||||||
keyGen.next().value,
|
keyGen.next().value,
|
||||||
res.data.thread as GetPostThread.ThreadViewPost,
|
res.data.thread as AppBskyFeedDefs.ThreadViewPost,
|
||||||
)
|
)
|
||||||
thread._isHighlightedPost = true
|
thread._isHighlightedPost = true
|
||||||
thread.assignTreeModels(
|
thread.assignTreeModels(
|
||||||
keyGen,
|
keyGen,
|
||||||
res.data.thread as GetPostThread.ThreadViewPost,
|
res.data.thread as AppBskyFeedDefs.ThreadViewPost,
|
||||||
thread.uri,
|
thread.uri,
|
||||||
)
|
)
|
||||||
this.thread = thread
|
this.thread = thread
|
||||||
|
@ -368,25 +337,25 @@ export class PostThreadViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
type MaybePost =
|
type MaybePost =
|
||||||
| GetPostThread.ThreadViewPost
|
| AppBskyFeedDefs.ThreadViewPost
|
||||||
| GetPostThread.NotFoundPost
|
| AppBskyFeedDefs.NotFoundPost
|
||||||
| {[k: string]: unknown; $type: string}
|
| {[k: string]: unknown; $type: string}
|
||||||
function sortThread(post: MaybePost) {
|
function sortThread(post: MaybePost) {
|
||||||
if (post.notFound) {
|
if (post.notFound) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
post = post as GetPostThread.ThreadViewPost
|
post = post as AppBskyFeedDefs.ThreadViewPost
|
||||||
if (post.replies) {
|
if (post.replies) {
|
||||||
post.replies.sort((a: MaybePost, b: MaybePost) => {
|
post.replies.sort((a: MaybePost, b: MaybePost) => {
|
||||||
post = post as GetPostThread.ThreadViewPost
|
post = post as AppBskyFeedDefs.ThreadViewPost
|
||||||
if (a.notFound) {
|
if (a.notFound) {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
if (b.notFound) {
|
if (b.notFound) {
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
a = a as GetPostThread.ThreadViewPost
|
a = a as AppBskyFeedDefs.ThreadViewPost
|
||||||
b = b as GetPostThread.ThreadViewPost
|
b = b as AppBskyFeedDefs.ThreadViewPost
|
||||||
const aIsByOp = a.post.author.did === post.post.author.did
|
const aIsByOp = a.post.author.did === post.post.author.did
|
||||||
const bIsByOp = b.post.author.did === post.post.author.did
|
const bIsByOp = b.post.author.did === post.post.author.did
|
||||||
if (aIsByOp && bIsByOp) {
|
if (aIsByOp && bIsByOp) {
|
||||||
|
|
|
@ -58,12 +58,12 @@ export class PostModel implements RemoveIndex<Post.Record> {
|
||||||
// state transitions
|
// state transitions
|
||||||
// =
|
// =
|
||||||
|
|
||||||
private _xLoading() {
|
_xLoading() {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
this.error = ''
|
this.error = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
private _xIdle(err?: any) {
|
_xIdle(err?: any) {
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
this.hasLoaded = true
|
this.hasLoaded = true
|
||||||
this.error = cleanError(err)
|
this.error = cleanError(err)
|
||||||
|
@ -75,12 +75,12 @@ export class PostModel implements RemoveIndex<Post.Record> {
|
||||||
// loader functions
|
// loader functions
|
||||||
// =
|
// =
|
||||||
|
|
||||||
private async _load() {
|
async _load() {
|
||||||
this._xLoading()
|
this._xLoading()
|
||||||
try {
|
try {
|
||||||
const urip = new AtUri(this.uri)
|
const urip = new AtUri(this.uri)
|
||||||
const res = await this.rootStore.api.app.bsky.feed.post.get({
|
const res = await this.rootStore.agent.getPost({
|
||||||
user: urip.host,
|
repo: urip.host,
|
||||||
rkey: urip.rkey,
|
rkey: urip.rkey,
|
||||||
})
|
})
|
||||||
// TODO
|
// 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.text = res.text
|
||||||
this.entities = res.entities
|
this.entities = res.entities
|
||||||
this.reply = res.reply
|
this.reply = res.reply
|
||||||
|
|
|
@ -2,15 +2,12 @@ import {makeAutoObservable, runInAction} from 'mobx'
|
||||||
import {PickedMedia} from 'lib/media/picker'
|
import {PickedMedia} from 'lib/media/picker'
|
||||||
import {
|
import {
|
||||||
AppBskyActorGetProfile as GetProfile,
|
AppBskyActorGetProfile as GetProfile,
|
||||||
AppBskySystemDeclRef,
|
AppBskyActorProfile,
|
||||||
AppBskyActorUpdateProfile,
|
RichText,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
type DeclRef = AppBskySystemDeclRef.Main
|
|
||||||
import {extractEntities} from 'lib/strings/rich-text-detection'
|
|
||||||
import {RootStoreModel} from './root-store'
|
import {RootStoreModel} from './root-store'
|
||||||
import * as apilib from 'lib/api/index'
|
import * as apilib from 'lib/api/index'
|
||||||
import {cleanError} from 'lib/strings/errors'
|
import {cleanError} from 'lib/strings/errors'
|
||||||
import {RichText} from 'lib/strings/rich-text'
|
|
||||||
|
|
||||||
export const ACTOR_TYPE_USER = 'app.bsky.system.actorUser'
|
export const ACTOR_TYPE_USER = 'app.bsky.system.actorUser'
|
||||||
|
|
||||||
|
@ -35,22 +32,18 @@ export class ProfileViewModel {
|
||||||
// data
|
// data
|
||||||
did: string = ''
|
did: string = ''
|
||||||
handle: string = ''
|
handle: string = ''
|
||||||
declaration: DeclRef = {
|
|
||||||
cid: '',
|
|
||||||
actorType: '',
|
|
||||||
}
|
|
||||||
creator: string = ''
|
creator: string = ''
|
||||||
displayName?: string
|
displayName?: string = ''
|
||||||
description?: string
|
description?: string = ''
|
||||||
avatar?: string
|
avatar?: string = ''
|
||||||
banner?: string
|
banner?: string = ''
|
||||||
followersCount: number = 0
|
followersCount: number = 0
|
||||||
followsCount: number = 0
|
followsCount: number = 0
|
||||||
postsCount: number = 0
|
postsCount: number = 0
|
||||||
viewer = new ProfileViewViewerModel()
|
viewer = new ProfileViewViewerModel()
|
||||||
|
|
||||||
// added data
|
// added data
|
||||||
descriptionRichText?: RichText
|
descriptionRichText?: RichText = new RichText({text: ''})
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public rootStore: RootStoreModel,
|
public rootStore: RootStoreModel,
|
||||||
|
@ -79,10 +72,6 @@ export class ProfileViewModel {
|
||||||
return this.hasLoaded && !this.hasContent
|
return this.hasLoaded && !this.hasContent
|
||||||
}
|
}
|
||||||
|
|
||||||
get isUser() {
|
|
||||||
return this.declaration.actorType === ACTOR_TYPE_USER
|
|
||||||
}
|
|
||||||
|
|
||||||
// public api
|
// public api
|
||||||
// =
|
// =
|
||||||
|
|
||||||
|
@ -111,18 +100,14 @@ export class ProfileViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (followUri) {
|
if (followUri) {
|
||||||
await apilib.unfollow(this.rootStore, followUri)
|
await this.rootStore.agent.deleteFollow(followUri)
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.followersCount--
|
this.followersCount--
|
||||||
this.viewer.following = undefined
|
this.viewer.following = undefined
|
||||||
this.rootStore.me.follows.removeFollow(this.did)
|
this.rootStore.me.follows.removeFollow(this.did)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const res = await apilib.follow(
|
const res = await this.rootStore.agent.follow(this.did)
|
||||||
this.rootStore,
|
|
||||||
this.did,
|
|
||||||
this.declaration.cid,
|
|
||||||
)
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.followersCount++
|
this.followersCount++
|
||||||
this.viewer.following = res.uri
|
this.viewer.following = res.uri
|
||||||
|
@ -132,22 +117,23 @@ export class ProfileViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateProfile(
|
async updateProfile(
|
||||||
updates: AppBskyActorUpdateProfile.InputSchema,
|
updates: AppBskyActorProfile.Record,
|
||||||
newUserAvatar: PickedMedia | undefined | null,
|
newUserAvatar: PickedMedia | undefined | null,
|
||||||
newUserBanner: PickedMedia | undefined | null,
|
newUserBanner: PickedMedia | undefined | null,
|
||||||
) {
|
) {
|
||||||
|
await this.rootStore.agent.upsertProfile(async existing => {
|
||||||
|
existing = existing || {}
|
||||||
|
existing.displayName = updates.displayName
|
||||||
|
existing.description = updates.description
|
||||||
if (newUserAvatar) {
|
if (newUserAvatar) {
|
||||||
const res = await apilib.uploadBlob(
|
const res = await apilib.uploadBlob(
|
||||||
this.rootStore,
|
this.rootStore,
|
||||||
newUserAvatar.path,
|
newUserAvatar.path,
|
||||||
newUserAvatar.mime,
|
newUserAvatar.mime,
|
||||||
)
|
)
|
||||||
updates.avatar = {
|
existing.avatar = res.data.blob
|
||||||
cid: res.data.cid,
|
|
||||||
mimeType: newUserAvatar.mime,
|
|
||||||
}
|
|
||||||
} else if (newUserAvatar === null) {
|
} else if (newUserAvatar === null) {
|
||||||
updates.avatar = null
|
existing.avatar = undefined
|
||||||
}
|
}
|
||||||
if (newUserBanner) {
|
if (newUserBanner) {
|
||||||
const res = await apilib.uploadBlob(
|
const res = await apilib.uploadBlob(
|
||||||
|
@ -155,26 +141,24 @@ export class ProfileViewModel {
|
||||||
newUserBanner.path,
|
newUserBanner.path,
|
||||||
newUserBanner.mime,
|
newUserBanner.mime,
|
||||||
)
|
)
|
||||||
updates.banner = {
|
existing.banner = res.data.blob
|
||||||
cid: res.data.cid,
|
|
||||||
mimeType: newUserBanner.mime,
|
|
||||||
}
|
|
||||||
} else if (newUserBanner === null) {
|
} else if (newUserBanner === null) {
|
||||||
updates.banner = null
|
existing.banner = undefined
|
||||||
}
|
}
|
||||||
await this.rootStore.api.app.bsky.actor.updateProfile(updates)
|
return existing
|
||||||
|
})
|
||||||
await this.rootStore.me.load()
|
await this.rootStore.me.load()
|
||||||
await this.refresh()
|
await this.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
async muteAccount() {
|
async muteAccount() {
|
||||||
await this.rootStore.api.app.bsky.graph.mute({user: this.did})
|
await this.rootStore.agent.mute(this.did)
|
||||||
this.viewer.muted = true
|
this.viewer.muted = true
|
||||||
await this.refresh()
|
await this.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
async unmuteAccount() {
|
async unmuteAccount() {
|
||||||
await this.rootStore.api.app.bsky.graph.unmute({user: this.did})
|
await this.rootStore.agent.unmute(this.did)
|
||||||
this.viewer.muted = false
|
this.viewer.muted = false
|
||||||
await this.refresh()
|
await this.refresh()
|
||||||
}
|
}
|
||||||
|
@ -182,13 +166,13 @@ export class ProfileViewModel {
|
||||||
// state transitions
|
// state transitions
|
||||||
// =
|
// =
|
||||||
|
|
||||||
private _xLoading(isRefreshing = false) {
|
_xLoading(isRefreshing = false) {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
this.isRefreshing = isRefreshing
|
this.isRefreshing = isRefreshing
|
||||||
this.error = ''
|
this.error = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
private _xIdle(err?: any) {
|
_xIdle(err?: any) {
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
this.isRefreshing = false
|
this.isRefreshing = false
|
||||||
this.hasLoaded = true
|
this.hasLoaded = true
|
||||||
|
@ -201,40 +185,40 @@ export class ProfileViewModel {
|
||||||
// loader functions
|
// loader functions
|
||||||
// =
|
// =
|
||||||
|
|
||||||
private async _load(isRefreshing = false) {
|
async _load(isRefreshing = false) {
|
||||||
this._xLoading(isRefreshing)
|
this._xLoading(isRefreshing)
|
||||||
try {
|
try {
|
||||||
const res = await this.rootStore.api.app.bsky.actor.getProfile(
|
const res = await this.rootStore.agent.getProfile(this.params)
|
||||||
this.params,
|
|
||||||
)
|
|
||||||
this.rootStore.profiles.overwrite(this.params.actor, res) // cache invalidation
|
this.rootStore.profiles.overwrite(this.params.actor, res) // cache invalidation
|
||||||
this._replaceAll(res)
|
this._replaceAll(res)
|
||||||
|
await this._createRichText()
|
||||||
this._xIdle()
|
this._xIdle()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this._xIdle(e)
|
this._xIdle(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _replaceAll(res: GetProfile.Response) {
|
_replaceAll(res: GetProfile.Response) {
|
||||||
this.did = res.data.did
|
this.did = res.data.did
|
||||||
this.handle = res.data.handle
|
this.handle = res.data.handle
|
||||||
Object.assign(this.declaration, res.data.declaration)
|
|
||||||
this.creator = res.data.creator
|
|
||||||
this.displayName = res.data.displayName
|
this.displayName = res.data.displayName
|
||||||
this.description = res.data.description
|
this.description = res.data.description
|
||||||
this.avatar = res.data.avatar
|
this.avatar = res.data.avatar
|
||||||
this.banner = res.data.banner
|
this.banner = res.data.banner
|
||||||
this.followersCount = res.data.followersCount
|
this.followersCount = res.data.followersCount || 0
|
||||||
this.followsCount = res.data.followsCount
|
this.followsCount = res.data.followsCount || 0
|
||||||
this.postsCount = res.data.postsCount
|
this.postsCount = res.data.postsCount || 0
|
||||||
if (res.data.viewer) {
|
if (res.data.viewer) {
|
||||||
Object.assign(this.viewer, res.data.viewer)
|
Object.assign(this.viewer, res.data.viewer)
|
||||||
this.rootStore.me.follows.hydrate(this.did, res.data.viewer.following)
|
this.rootStore.me.follows.hydrate(this.did, res.data.viewer.following)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _createRichText() {
|
||||||
this.descriptionRichText = new RichText(
|
this.descriptionRichText = new RichText(
|
||||||
this.description || '',
|
{text: this.description || ''},
|
||||||
extractEntities(this.description || ''),
|
|
||||||
{cleanNewlines: true},
|
{cleanNewlines: true},
|
||||||
)
|
)
|
||||||
|
await this.descriptionRichText.detectFacets(this.rootStore.agent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ export class ProfilesViewModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const promise = this.rootStore.api.app.bsky.actor.getProfile({
|
const promise = this.rootStore.agent.getProfile({
|
||||||
actor: did,
|
actor: did,
|
||||||
})
|
})
|
||||||
this.cache.set(did, promise)
|
this.cache.set(did, promise)
|
||||||
|
|
|
@ -2,7 +2,7 @@ import {makeAutoObservable, runInAction} from 'mobx'
|
||||||
import {AtUri} from '../../third-party/uri'
|
import {AtUri} from '../../third-party/uri'
|
||||||
import {
|
import {
|
||||||
AppBskyFeedGetRepostedBy as GetRepostedBy,
|
AppBskyFeedGetRepostedBy as GetRepostedBy,
|
||||||
AppBskyActorRef as ActorRef,
|
AppBskyActorDefs,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {RootStoreModel} from './root-store'
|
import {RootStoreModel} from './root-store'
|
||||||
import {bundleAsync} from 'lib/async/bundle'
|
import {bundleAsync} from 'lib/async/bundle'
|
||||||
|
@ -11,7 +11,7 @@ import * as apilib from 'lib/api/index'
|
||||||
|
|
||||||
const PAGE_SIZE = 30
|
const PAGE_SIZE = 30
|
||||||
|
|
||||||
export type RepostedByItem = ActorRef.WithInfo
|
export type RepostedByItem = AppBskyActorDefs.ProfileViewBasic
|
||||||
|
|
||||||
export class RepostedByViewModel {
|
export class RepostedByViewModel {
|
||||||
// state
|
// state
|
||||||
|
@ -71,9 +71,9 @@ export class RepostedByViewModel {
|
||||||
const params = Object.assign({}, this.params, {
|
const params = Object.assign({}, this.params, {
|
||||||
uri: this.resolvedUri,
|
uri: this.resolvedUri,
|
||||||
limit: PAGE_SIZE,
|
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) {
|
if (replace) {
|
||||||
this._replaceAll(res)
|
this._replaceAll(res)
|
||||||
} else {
|
} else {
|
||||||
|
@ -88,13 +88,13 @@ export class RepostedByViewModel {
|
||||||
// state transitions
|
// state transitions
|
||||||
// =
|
// =
|
||||||
|
|
||||||
private _xLoading(isRefreshing = false) {
|
_xLoading(isRefreshing = false) {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
this.isRefreshing = isRefreshing
|
this.isRefreshing = isRefreshing
|
||||||
this.error = ''
|
this.error = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
private _xIdle(err?: any) {
|
_xIdle(err?: any) {
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
this.isRefreshing = false
|
this.isRefreshing = false
|
||||||
this.hasLoaded = true
|
this.hasLoaded = true
|
||||||
|
@ -107,7 +107,7 @@ export class RepostedByViewModel {
|
||||||
// helper functions
|
// helper functions
|
||||||
// =
|
// =
|
||||||
|
|
||||||
private async _resolveUri() {
|
async _resolveUri() {
|
||||||
const urip = new AtUri(this.params.uri)
|
const urip = new AtUri(this.params.uri)
|
||||||
if (!urip.host.startsWith('did:')) {
|
if (!urip.host.startsWith('did:')) {
|
||||||
try {
|
try {
|
||||||
|
@ -121,12 +121,12 @@ export class RepostedByViewModel {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private _replaceAll(res: GetRepostedBy.Response) {
|
_replaceAll(res: GetRepostedBy.Response) {
|
||||||
this.repostedBy = []
|
this.repostedBy = []
|
||||||
this._appendAll(res)
|
this._appendAll(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
private _appendAll(res: GetRepostedBy.Response) {
|
_appendAll(res: GetRepostedBy.Response) {
|
||||||
this.loadMoreCursor = res.data.cursor
|
this.loadMoreCursor = res.data.cursor
|
||||||
this.hasMore = !!this.loadMoreCursor
|
this.hasMore = !!this.loadMoreCursor
|
||||||
this.repostedBy = this.repostedBy.concat(res.data.repostedBy)
|
this.repostedBy = this.repostedBy.concat(res.data.repostedBy)
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
* The root store is the base of all modeled state.
|
* The root store is the base of all modeled state.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {makeAutoObservable, runInAction} from 'mobx'
|
import {makeAutoObservable} from 'mobx'
|
||||||
import {AtpAgent} from '@atproto/api'
|
import {BskyAgent} from '@atproto/api'
|
||||||
import {createContext, useContext} from 'react'
|
import {createContext, useContext} from 'react'
|
||||||
import {DeviceEventEmitter, EmitterSubscription} from 'react-native'
|
import {DeviceEventEmitter, EmitterSubscription} from 'react-native'
|
||||||
import * as BgScheduler from 'lib/bg-scheduler'
|
import * as BgScheduler from 'lib/bg-scheduler'
|
||||||
|
@ -29,7 +29,7 @@ export const appInfo = z.object({
|
||||||
export type AppInfo = z.infer<typeof appInfo>
|
export type AppInfo = z.infer<typeof appInfo>
|
||||||
|
|
||||||
export class RootStoreModel {
|
export class RootStoreModel {
|
||||||
agent: AtpAgent
|
agent: BskyAgent
|
||||||
appInfo?: AppInfo
|
appInfo?: AppInfo
|
||||||
log = new LogModel()
|
log = new LogModel()
|
||||||
session = new SessionModel(this)
|
session = new SessionModel(this)
|
||||||
|
@ -40,41 +40,16 @@ export class RootStoreModel {
|
||||||
linkMetas = new LinkMetasCache(this)
|
linkMetas = new LinkMetasCache(this)
|
||||||
imageSizes = new ImageSizesCache()
|
imageSizes = new ImageSizesCache()
|
||||||
|
|
||||||
// HACK
|
constructor(agent: BskyAgent) {
|
||||||
// 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) {
|
|
||||||
this.agent = agent
|
this.agent = agent
|
||||||
makeAutoObservable(this, {
|
makeAutoObservable(this, {
|
||||||
api: false,
|
agent: false,
|
||||||
serialize: false,
|
serialize: false,
|
||||||
hydrate: false,
|
hydrate: false,
|
||||||
})
|
})
|
||||||
this.initBgFetch()
|
this.initBgFetch()
|
||||||
}
|
}
|
||||||
|
|
||||||
get api() {
|
|
||||||
return this.agent.api
|
|
||||||
}
|
|
||||||
|
|
||||||
setAppInfo(info: AppInfo) {
|
setAppInfo(info: AppInfo) {
|
||||||
this.appInfo = info
|
this.appInfo = info
|
||||||
}
|
}
|
||||||
|
@ -131,7 +106,7 @@ export class RootStoreModel {
|
||||||
/**
|
/**
|
||||||
* Called by the session model. Refreshes session-oriented state.
|
* Called by the session model. Refreshes session-oriented state.
|
||||||
*/
|
*/
|
||||||
async handleSessionChange(agent: AtpAgent) {
|
async handleSessionChange(agent: BskyAgent) {
|
||||||
this.log.debug('RootStoreModel:handleSessionChange')
|
this.log.debug('RootStoreModel:handleSessionChange')
|
||||||
this.agent = agent
|
this.agent = agent
|
||||||
this.me.clear()
|
this.me.clear()
|
||||||
|
@ -259,7 +234,7 @@ export class RootStoreModel {
|
||||||
async onBgFetch(taskId: string) {
|
async onBgFetch(taskId: string) {
|
||||||
this.log.debug(`Background fetch fired for task ${taskId}`)
|
this.log.debug(`Background fetch fired for task ${taskId}`)
|
||||||
if (this.session.hasSession) {
|
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
|
const hasNewNotifs = this.me.notifications.unreadCount !== res.data.count
|
||||||
this.emitUnreadNotifications(res.data.count)
|
this.emitUnreadNotifications(res.data.count)
|
||||||
this.log.debug(
|
this.log.debug(
|
||||||
|
@ -286,7 +261,7 @@ export class RootStoreModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
const throwawayInst = new 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
|
) // this will be replaced by the loader, we just need to supply a value at init
|
||||||
const RootStoreContext = createContext<RootStoreModel>(throwawayInst)
|
const RootStoreContext = createContext<RootStoreModel>(throwawayInst)
|
||||||
export const RootStoreProvider = RootStoreContext.Provider
|
export const RootStoreProvider = RootStoreContext.Provider
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import {makeAutoObservable, runInAction} from 'mobx'
|
import {makeAutoObservable, runInAction} from 'mobx'
|
||||||
import {
|
import {
|
||||||
AtpAgent,
|
BskyAgent,
|
||||||
AtpSessionEvent,
|
AtpSessionEvent,
|
||||||
AtpSessionData,
|
AtpSessionData,
|
||||||
ComAtprotoServerGetAccountsConfig as GetAccountsConfig,
|
ComAtprotoServerDescribeServer as DescribeServer,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import normalizeUrl from 'normalize-url'
|
import normalizeUrl from 'normalize-url'
|
||||||
import {isObj, hasProp} from 'lib/type-guards'
|
import {isObj, hasProp} from 'lib/type-guards'
|
||||||
|
@ -11,7 +11,7 @@ import {networkRetry} from 'lib/async/retry'
|
||||||
import {z} from 'zod'
|
import {z} from 'zod'
|
||||||
import {RootStoreModel} from './root-store'
|
import {RootStoreModel} from './root-store'
|
||||||
|
|
||||||
export type ServiceDescription = GetAccountsConfig.OutputSchema
|
export type ServiceDescription = DescribeServer.OutputSchema
|
||||||
|
|
||||||
export const activeSession = z.object({
|
export const activeSession = z.object({
|
||||||
service: z.string(),
|
service: z.string(),
|
||||||
|
@ -40,7 +40,7 @@ export class SessionModel {
|
||||||
// emergency log facility to help us track down this logout issue
|
// emergency log facility to help us track down this logout issue
|
||||||
// remove when resolved
|
// remove when resolved
|
||||||
// -prf
|
// -prf
|
||||||
private _log(message: string, details?: Record<string, any>) {
|
_log(message: string, details?: Record<string, any>) {
|
||||||
details = details || {}
|
details = details || {}
|
||||||
details.state = {
|
details.state = {
|
||||||
data: this.data,
|
data: this.data,
|
||||||
|
@ -73,6 +73,7 @@ export class SessionModel {
|
||||||
rootStore: false,
|
rootStore: false,
|
||||||
serialize: false,
|
serialize: false,
|
||||||
hydrate: false,
|
hydrate: false,
|
||||||
|
hasSession: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -154,7 +155,7 @@ export class SessionModel {
|
||||||
/**
|
/**
|
||||||
* Sets the active session
|
* Sets the active session
|
||||||
*/
|
*/
|
||||||
async setActiveSession(agent: AtpAgent, did: string) {
|
async setActiveSession(agent: BskyAgent, did: string) {
|
||||||
this._log('SessionModel:setActiveSession')
|
this._log('SessionModel:setActiveSession')
|
||||||
this.data = {
|
this.data = {
|
||||||
service: agent.service.toString(),
|
service: agent.service.toString(),
|
||||||
|
@ -166,7 +167,7 @@ export class SessionModel {
|
||||||
/**
|
/**
|
||||||
* Upserts a session into the accounts
|
* Upserts a session into the accounts
|
||||||
*/
|
*/
|
||||||
private persistSession(
|
persistSession(
|
||||||
service: string,
|
service: string,
|
||||||
did: string,
|
did: string,
|
||||||
event: AtpSessionEvent,
|
event: AtpSessionEvent,
|
||||||
|
@ -225,7 +226,7 @@ export class SessionModel {
|
||||||
/**
|
/**
|
||||||
* Clears any session tokens from the accounts; used on logout.
|
* Clears any session tokens from the accounts; used on logout.
|
||||||
*/
|
*/
|
||||||
private clearSessionTokens() {
|
clearSessionTokens() {
|
||||||
this._log('SessionModel:clearSessionTokens')
|
this._log('SessionModel:clearSessionTokens')
|
||||||
this.accounts = this.accounts.map(acct => ({
|
this.accounts = this.accounts.map(acct => ({
|
||||||
service: acct.service,
|
service: acct.service,
|
||||||
|
@ -239,10 +240,8 @@ export class SessionModel {
|
||||||
/**
|
/**
|
||||||
* Fetches additional information about an account on load.
|
* Fetches additional information about an account on load.
|
||||||
*/
|
*/
|
||||||
private async loadAccountInfo(agent: AtpAgent, did: string) {
|
async loadAccountInfo(agent: BskyAgent, did: string) {
|
||||||
const res = await agent.api.app.bsky.actor
|
const res = await agent.getProfile({actor: did}).catch(_e => undefined)
|
||||||
.getProfile({actor: did})
|
|
||||||
.catch(_e => undefined)
|
|
||||||
if (res) {
|
if (res) {
|
||||||
return {
|
return {
|
||||||
dispayName: res.data.displayName,
|
dispayName: res.data.displayName,
|
||||||
|
@ -255,8 +254,8 @@ export class SessionModel {
|
||||||
* Helper to fetch the accounts config settings from an account.
|
* Helper to fetch the accounts config settings from an account.
|
||||||
*/
|
*/
|
||||||
async describeService(service: string): Promise<ServiceDescription> {
|
async describeService(service: string): Promise<ServiceDescription> {
|
||||||
const agent = new AtpAgent({service})
|
const agent = new BskyAgent({service})
|
||||||
const res = await agent.api.com.atproto.server.getAccountsConfig({})
|
const res = await agent.com.atproto.server.describeServer({})
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -272,7 +271,7 @@ export class SessionModel {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const agent = new AtpAgent({
|
const agent = new BskyAgent({
|
||||||
service: account.service,
|
service: account.service,
|
||||||
persistSession: (evt: AtpSessionEvent, sess?: AtpSessionData) => {
|
persistSession: (evt: AtpSessionEvent, sess?: AtpSessionData) => {
|
||||||
this.persistSession(account.service, account.did, evt, sess)
|
this.persistSession(account.service, account.did, evt, sess)
|
||||||
|
@ -321,7 +320,7 @@ export class SessionModel {
|
||||||
password: string
|
password: string
|
||||||
}) {
|
}) {
|
||||||
this._log('SessionModel:login')
|
this._log('SessionModel:login')
|
||||||
const agent = new AtpAgent({service})
|
const agent = new BskyAgent({service})
|
||||||
await agent.login({identifier, password})
|
await agent.login({identifier, password})
|
||||||
if (!agent.session) {
|
if (!agent.session) {
|
||||||
throw new Error('Failed to establish session')
|
throw new Error('Failed to establish session')
|
||||||
|
@ -355,7 +354,7 @@ export class SessionModel {
|
||||||
inviteCode?: string
|
inviteCode?: string
|
||||||
}) {
|
}) {
|
||||||
this._log('SessionModel:createAccount')
|
this._log('SessionModel:createAccount')
|
||||||
const agent = new AtpAgent({service})
|
const agent = new BskyAgent({service})
|
||||||
await agent.createAccount({
|
await agent.createAccount({
|
||||||
handle,
|
handle,
|
||||||
password,
|
password,
|
||||||
|
@ -389,7 +388,7 @@ export class SessionModel {
|
||||||
// need to evaluate why deleting the session has caused errors at times
|
// need to evaluate why deleting the session has caused errors at times
|
||||||
// -prf
|
// -prf
|
||||||
/*if (this.hasSession) {
|
/*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(
|
this.rootStore.log.warn(
|
||||||
'(Minor issue) Failed to delete session on the server',
|
'(Minor issue) Failed to delete session on the server',
|
||||||
e,
|
e,
|
||||||
|
@ -415,7 +414,7 @@ export class SessionModel {
|
||||||
if (!sess) {
|
if (!sess) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const res = await this.rootStore.api.app.bsky.actor
|
const res = await this.rootStore.agent
|
||||||
.getProfile({actor: sess.did})
|
.getProfile({actor: sess.did})
|
||||||
.catch(_e => undefined)
|
.catch(_e => undefined)
|
||||||
if (res?.success) {
|
if (res?.success) {
|
||||||
|
|
|
@ -72,12 +72,12 @@ export class SuggestedPostsView {
|
||||||
// state transitions
|
// state transitions
|
||||||
// =
|
// =
|
||||||
|
|
||||||
private _xLoading() {
|
_xLoading() {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
this.error = ''
|
this.error = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
private _xIdle(err?: any) {
|
_xIdle(err?: any) {
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
this.hasLoaded = true
|
this.hasLoaded = true
|
||||||
this.error = cleanError(err)
|
this.error = cleanError(err)
|
||||||
|
|
|
@ -2,7 +2,7 @@ import {makeAutoObservable} from 'mobx'
|
||||||
import {RootStoreModel} from '../root-store'
|
import {RootStoreModel} from '../root-store'
|
||||||
import {ServiceDescription} from '../session'
|
import {ServiceDescription} from '../session'
|
||||||
import {DEFAULT_SERVICE} from 'state/index'
|
import {DEFAULT_SERVICE} from 'state/index'
|
||||||
import {ComAtprotoAccountCreate} from '@atproto/api'
|
import {ComAtprotoServerCreateAccount} from '@atproto/api'
|
||||||
import * as EmailValidator from 'email-validator'
|
import * as EmailValidator from 'email-validator'
|
||||||
import {createFullHandle} from 'lib/strings/handles'
|
import {createFullHandle} from 'lib/strings/handles'
|
||||||
import {cleanError} from 'lib/strings/errors'
|
import {cleanError} from 'lib/strings/errors'
|
||||||
|
@ -99,7 +99,7 @@ export class CreateAccountModel {
|
||||||
})
|
})
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
let errMsg = e.toString()
|
let errMsg = e.toString()
|
||||||
if (e instanceof ComAtprotoAccountCreate.InvalidInviteCodeError) {
|
if (e instanceof ComAtprotoServerCreateAccount.InvalidInviteCodeError) {
|
||||||
errMsg =
|
errMsg =
|
||||||
'Invite code not accepted. Check that you input it correctly and try again.'
|
'Invite code not accepted. Check that you input it correctly and try again.'
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ export class ProfileUiModel {
|
||||||
)
|
)
|
||||||
this.profile = new ProfileViewModel(rootStore, {actor: params.user})
|
this.profile = new ProfileViewModel(rootStore, {actor: params.user})
|
||||||
this.feed = new FeedModel(rootStore, 'author', {
|
this.feed = new FeedModel(rootStore, 'author', {
|
||||||
author: params.user,
|
actor: params.user,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -64,16 +64,8 @@ export class ProfileUiModel {
|
||||||
return this.profile.isRefreshing || this.currentView.isRefreshing
|
return this.profile.isRefreshing || this.currentView.isRefreshing
|
||||||
}
|
}
|
||||||
|
|
||||||
get isUser() {
|
|
||||||
return this.profile.isUser
|
|
||||||
}
|
|
||||||
|
|
||||||
get selectorItems() {
|
get selectorItems() {
|
||||||
if (this.isUser) {
|
|
||||||
return USER_SELECTOR_ITEMS
|
return USER_SELECTOR_ITEMS
|
||||||
} else {
|
|
||||||
return USER_SELECTOR_ITEMS
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get selectedView() {
|
get selectedView() {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import {makeAutoObservable, runInAction} from 'mobx'
|
import {makeAutoObservable, runInAction} from 'mobx'
|
||||||
import {searchProfiles, searchPosts} from 'lib/api/search'
|
import {searchProfiles, searchPosts} from 'lib/api/search'
|
||||||
import {AppBskyActorProfile as Profile} from '@atproto/api'
|
import {AppBskyActorDefs} from '@atproto/api'
|
||||||
import {RootStoreModel} from '../root-store'
|
import {RootStoreModel} from '../root-store'
|
||||||
|
|
||||||
export class SearchUIModel {
|
export class SearchUIModel {
|
||||||
|
@ -8,7 +8,7 @@ export class SearchUIModel {
|
||||||
isProfilesLoading = false
|
isProfilesLoading = false
|
||||||
query: string = ''
|
query: string = ''
|
||||||
postUris: string[] = []
|
postUris: string[] = []
|
||||||
profiles: Profile.View[] = []
|
profiles: AppBskyActorDefs.ProfileView[] = []
|
||||||
|
|
||||||
constructor(public rootStore: RootStoreModel) {
|
constructor(public rootStore: RootStoreModel) {
|
||||||
makeAutoObservable(this)
|
makeAutoObservable(this)
|
||||||
|
@ -34,10 +34,10 @@ export class SearchUIModel {
|
||||||
this.isPostsLoading = false
|
this.isPostsLoading = false
|
||||||
})
|
})
|
||||||
|
|
||||||
let profiles: Profile.View[] = []
|
let profiles: AppBskyActorDefs.ProfileView[] = []
|
||||||
if (profilesSearch?.length) {
|
if (profilesSearch?.length) {
|
||||||
do {
|
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),
|
actors: profilesSearch.splice(0, 25).map(p => p.did),
|
||||||
})
|
})
|
||||||
profiles = profiles.concat(res.data.profiles)
|
profiles = profiles.concat(res.data.profiles)
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import {AppBskyEmbedRecord} from '@atproto/api'
|
||||||
import {RootStoreModel} from '../root-store'
|
import {RootStoreModel} from '../root-store'
|
||||||
import {makeAutoObservable} from 'mobx'
|
import {makeAutoObservable} from 'mobx'
|
||||||
import {ProfileViewModel} from '../profile-view'
|
import {ProfileViewModel} from '../profile-view'
|
||||||
|
@ -111,6 +112,7 @@ export interface ComposerOptsQuote {
|
||||||
displayName?: string
|
displayName?: string
|
||||||
avatar?: string
|
avatar?: string
|
||||||
}
|
}
|
||||||
|
embeds?: AppBskyEmbedRecord.ViewRecord['embeds']
|
||||||
}
|
}
|
||||||
export interface ComposerOpts {
|
export interface ComposerOpts {
|
||||||
replyTo?: ComposerOptsPostRef
|
replyTo?: ComposerOptsPostRef
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import {makeAutoObservable, runInAction} from 'mobx'
|
import {makeAutoObservable, runInAction} from 'mobx'
|
||||||
import {AppBskyActorRef} from '@atproto/api'
|
import {AppBskyActorDefs} from '@atproto/api'
|
||||||
import AwaitLock from 'await-lock'
|
import AwaitLock from 'await-lock'
|
||||||
import {RootStoreModel} from './root-store'
|
import {RootStoreModel} from './root-store'
|
||||||
|
|
||||||
|
@ -11,8 +11,8 @@ export class UserAutocompleteViewModel {
|
||||||
lock = new AwaitLock()
|
lock = new AwaitLock()
|
||||||
|
|
||||||
// data
|
// data
|
||||||
follows: AppBskyActorRef.WithInfo[] = []
|
follows: AppBskyActorDefs.ProfileViewBasic[] = []
|
||||||
searchRes: AppBskyActorRef.WithInfo[] = []
|
searchRes: AppBskyActorDefs.ProfileViewBasic[] = []
|
||||||
knownHandles: Set<string> = new Set()
|
knownHandles: Set<string> = new Set()
|
||||||
|
|
||||||
constructor(public rootStore: RootStoreModel) {
|
constructor(public rootStore: RootStoreModel) {
|
||||||
|
@ -76,9 +76,9 @@ export class UserAutocompleteViewModel {
|
||||||
// internal
|
// internal
|
||||||
// =
|
// =
|
||||||
|
|
||||||
private async _getFollows() {
|
async _getFollows() {
|
||||||
const res = await this.rootStore.api.app.bsky.graph.getFollows({
|
const res = await this.rootStore.agent.getFollows({
|
||||||
user: this.rootStore.me.did || '',
|
actor: this.rootStore.me.did || '',
|
||||||
})
|
})
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.follows = res.data.follows
|
this.follows = res.data.follows
|
||||||
|
@ -88,13 +88,13 @@ export class UserAutocompleteViewModel {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _search() {
|
async _search() {
|
||||||
const res = await this.rootStore.api.app.bsky.actor.searchTypeahead({
|
const res = await this.rootStore.agent.searchActorsTypeahead({
|
||||||
term: this.prefix,
|
term: this.prefix,
|
||||||
limit: 8,
|
limit: 8,
|
||||||
})
|
})
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.searchRes = res.data.users
|
this.searchRes = res.data.actors
|
||||||
for (const u of this.searchRes) {
|
for (const u of this.searchRes) {
|
||||||
this.knownHandles.add(u.handle)
|
this.knownHandles.add(u.handle)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {makeAutoObservable} from 'mobx'
|
import {makeAutoObservable} from 'mobx'
|
||||||
import {
|
import {
|
||||||
AppBskyGraphGetFollowers as GetFollowers,
|
AppBskyGraphGetFollowers as GetFollowers,
|
||||||
AppBskyActorRef as ActorRef,
|
AppBskyActorDefs as ActorDefs,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {RootStoreModel} from './root-store'
|
import {RootStoreModel} from './root-store'
|
||||||
import {cleanError} from 'lib/strings/errors'
|
import {cleanError} from 'lib/strings/errors'
|
||||||
|
@ -9,7 +9,7 @@ import {bundleAsync} from 'lib/async/bundle'
|
||||||
|
|
||||||
const PAGE_SIZE = 30
|
const PAGE_SIZE = 30
|
||||||
|
|
||||||
export type FollowerItem = ActorRef.WithInfo
|
export type FollowerItem = ActorDefs.ProfileViewBasic
|
||||||
|
|
||||||
export class UserFollowersViewModel {
|
export class UserFollowersViewModel {
|
||||||
// state
|
// state
|
||||||
|
@ -22,10 +22,9 @@ export class UserFollowersViewModel {
|
||||||
loadMoreCursor?: string
|
loadMoreCursor?: string
|
||||||
|
|
||||||
// data
|
// data
|
||||||
subject: ActorRef.WithInfo = {
|
subject: ActorDefs.ProfileViewBasic = {
|
||||||
did: '',
|
did: '',
|
||||||
handle: '',
|
handle: '',
|
||||||
declaration: {cid: '', actorType: ''},
|
|
||||||
}
|
}
|
||||||
followers: FollowerItem[] = []
|
followers: FollowerItem[] = []
|
||||||
|
|
||||||
|
@ -71,9 +70,9 @@ export class UserFollowersViewModel {
|
||||||
try {
|
try {
|
||||||
const params = Object.assign({}, this.params, {
|
const params = Object.assign({}, this.params, {
|
||||||
limit: PAGE_SIZE,
|
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) {
|
if (replace) {
|
||||||
this._replaceAll(res)
|
this._replaceAll(res)
|
||||||
} else {
|
} else {
|
||||||
|
@ -88,13 +87,13 @@ export class UserFollowersViewModel {
|
||||||
// state transitions
|
// state transitions
|
||||||
// =
|
// =
|
||||||
|
|
||||||
private _xLoading(isRefreshing = false) {
|
_xLoading(isRefreshing = false) {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
this.isRefreshing = isRefreshing
|
this.isRefreshing = isRefreshing
|
||||||
this.error = ''
|
this.error = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
private _xIdle(err?: any) {
|
_xIdle(err?: any) {
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
this.isRefreshing = false
|
this.isRefreshing = false
|
||||||
this.hasLoaded = true
|
this.hasLoaded = true
|
||||||
|
@ -107,12 +106,12 @@ export class UserFollowersViewModel {
|
||||||
// helper functions
|
// helper functions
|
||||||
// =
|
// =
|
||||||
|
|
||||||
private _replaceAll(res: GetFollowers.Response) {
|
_replaceAll(res: GetFollowers.Response) {
|
||||||
this.followers = []
|
this.followers = []
|
||||||
this._appendAll(res)
|
this._appendAll(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
private _appendAll(res: GetFollowers.Response) {
|
_appendAll(res: GetFollowers.Response) {
|
||||||
this.loadMoreCursor = res.data.cursor
|
this.loadMoreCursor = res.data.cursor
|
||||||
this.hasMore = !!this.loadMoreCursor
|
this.hasMore = !!this.loadMoreCursor
|
||||||
this.followers = this.followers.concat(res.data.followers)
|
this.followers = this.followers.concat(res.data.followers)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {makeAutoObservable} from 'mobx'
|
import {makeAutoObservable} from 'mobx'
|
||||||
import {
|
import {
|
||||||
AppBskyGraphGetFollows as GetFollows,
|
AppBskyGraphGetFollows as GetFollows,
|
||||||
AppBskyActorRef as ActorRef,
|
AppBskyActorDefs as ActorDefs,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {RootStoreModel} from './root-store'
|
import {RootStoreModel} from './root-store'
|
||||||
import {cleanError} from 'lib/strings/errors'
|
import {cleanError} from 'lib/strings/errors'
|
||||||
|
@ -9,7 +9,7 @@ import {bundleAsync} from 'lib/async/bundle'
|
||||||
|
|
||||||
const PAGE_SIZE = 30
|
const PAGE_SIZE = 30
|
||||||
|
|
||||||
export type FollowItem = ActorRef.WithInfo
|
export type FollowItem = ActorDefs.ProfileViewBasic
|
||||||
|
|
||||||
export class UserFollowsViewModel {
|
export class UserFollowsViewModel {
|
||||||
// state
|
// state
|
||||||
|
@ -22,10 +22,9 @@ export class UserFollowsViewModel {
|
||||||
loadMoreCursor?: string
|
loadMoreCursor?: string
|
||||||
|
|
||||||
// data
|
// data
|
||||||
subject: ActorRef.WithInfo = {
|
subject: ActorDefs.ProfileViewBasic = {
|
||||||
did: '',
|
did: '',
|
||||||
handle: '',
|
handle: '',
|
||||||
declaration: {cid: '', actorType: ''},
|
|
||||||
}
|
}
|
||||||
follows: FollowItem[] = []
|
follows: FollowItem[] = []
|
||||||
|
|
||||||
|
@ -71,9 +70,9 @@ export class UserFollowsViewModel {
|
||||||
try {
|
try {
|
||||||
const params = Object.assign({}, this.params, {
|
const params = Object.assign({}, this.params, {
|
||||||
limit: PAGE_SIZE,
|
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) {
|
if (replace) {
|
||||||
this._replaceAll(res)
|
this._replaceAll(res)
|
||||||
} else {
|
} else {
|
||||||
|
@ -88,13 +87,13 @@ export class UserFollowsViewModel {
|
||||||
// state transitions
|
// state transitions
|
||||||
// =
|
// =
|
||||||
|
|
||||||
private _xLoading(isRefreshing = false) {
|
_xLoading(isRefreshing = false) {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
this.isRefreshing = isRefreshing
|
this.isRefreshing = isRefreshing
|
||||||
this.error = ''
|
this.error = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
private _xIdle(err?: any) {
|
_xIdle(err?: any) {
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
this.isRefreshing = false
|
this.isRefreshing = false
|
||||||
this.hasLoaded = true
|
this.hasLoaded = true
|
||||||
|
@ -107,12 +106,12 @@ export class UserFollowsViewModel {
|
||||||
// helper functions
|
// helper functions
|
||||||
// =
|
// =
|
||||||
|
|
||||||
private _replaceAll(res: GetFollows.Response) {
|
_replaceAll(res: GetFollows.Response) {
|
||||||
this.follows = []
|
this.follows = []
|
||||||
this._appendAll(res)
|
this._appendAll(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
private _appendAll(res: GetFollows.Response) {
|
_appendAll(res: GetFollows.Response) {
|
||||||
this.loadMoreCursor = res.data.cursor
|
this.loadMoreCursor = res.data.cursor
|
||||||
this.hasMore = !!this.loadMoreCursor
|
this.hasMore = !!this.loadMoreCursor
|
||||||
this.follows = this.follows.concat(res.data.follows)
|
this.follows = this.follows.concat(res.data.follows)
|
||||||
|
|
|
@ -75,16 +75,14 @@ export const CreateAccount = observer(
|
||||||
{model.step === 3 && <Step3 model={model} />}
|
{model.step === 3 && <Step3 model={model} />}
|
||||||
</View>
|
</View>
|
||||||
<View style={[s.flexRow, s.pl20, s.pr20]}>
|
<View style={[s.flexRow, s.pl20, s.pr20]}>
|
||||||
<TouchableOpacity onPress={onPressBackInner}>
|
<TouchableOpacity onPress={onPressBackInner} testID="backBtn">
|
||||||
<Text type="xl" style={pal.link}>
|
<Text type="xl" style={pal.link}>
|
||||||
Back
|
Back
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<View style={s.flex1} />
|
<View style={s.flex1} />
|
||||||
{model.canNext ? (
|
{model.canNext ? (
|
||||||
<TouchableOpacity
|
<TouchableOpacity testID="nextBtn" onPress={onPressNext}>
|
||||||
testID="createAccountButton"
|
|
||||||
onPress={onPressNext}>
|
|
||||||
{model.isProcessing ? (
|
{model.isProcessing ? (
|
||||||
<ActivityIndicator />
|
<ActivityIndicator />
|
||||||
) : (
|
) : (
|
||||||
|
@ -95,7 +93,7 @@ export const CreateAccount = observer(
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
) : model.didServiceDescriptionFetchFail ? (
|
) : model.didServiceDescriptionFetchFail ? (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="registerRetryButton"
|
testID="retryConnectBtn"
|
||||||
onPress={onPressRetryConnect}>
|
onPress={onPressRetryConnect}>
|
||||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
||||||
Retry
|
Retry
|
||||||
|
|
|
@ -60,12 +60,14 @@ export const Step1 = observer(({model}: {model: CreateAccountModel}) => {
|
||||||
This is the company that keeps you online.
|
This is the company that keeps you online.
|
||||||
</Text>
|
</Text>
|
||||||
<Option
|
<Option
|
||||||
|
testID="blueskyServerBtn"
|
||||||
isSelected={isDefaultSelected}
|
isSelected={isDefaultSelected}
|
||||||
label="Bluesky"
|
label="Bluesky"
|
||||||
help=" (default)"
|
help=" (default)"
|
||||||
onPress={onPressDefault}
|
onPress={onPressDefault}
|
||||||
/>
|
/>
|
||||||
<Option
|
<Option
|
||||||
|
testID="otherServerBtn"
|
||||||
isSelected={!isDefaultSelected}
|
isSelected={!isDefaultSelected}
|
||||||
label="Other"
|
label="Other"
|
||||||
onPress={onPressOther}>
|
onPress={onPressOther}>
|
||||||
|
@ -74,6 +76,7 @@ export const Step1 = observer(({model}: {model: CreateAccountModel}) => {
|
||||||
Enter the address of your provider:
|
Enter the address of your provider:
|
||||||
</Text>
|
</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
testID="customServerInput"
|
||||||
icon="globe"
|
icon="globe"
|
||||||
placeholder="Hosting provider address"
|
placeholder="Hosting provider address"
|
||||||
value={model.serviceUrl}
|
value={model.serviceUrl}
|
||||||
|
@ -83,12 +86,14 @@ export const Step1 = observer(({model}: {model: CreateAccountModel}) => {
|
||||||
{LOGIN_INCLUDE_DEV_SERVERS && (
|
{LOGIN_INCLUDE_DEV_SERVERS && (
|
||||||
<View style={[s.flexRow, s.mt10]}>
|
<View style={[s.flexRow, s.mt10]}>
|
||||||
<Button
|
<Button
|
||||||
|
testID="stagingServerBtn"
|
||||||
type="default"
|
type="default"
|
||||||
style={s.mr5}
|
style={s.mr5}
|
||||||
label="Staging"
|
label="Staging"
|
||||||
onPress={() => onDebugChangeServiceUrl(STAGING_SERVICE)}
|
onPress={() => onDebugChangeServiceUrl(STAGING_SERVICE)}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
testID="localDevServerBtn"
|
||||||
type="default"
|
type="default"
|
||||||
label="Dev Server"
|
label="Dev Server"
|
||||||
onPress={() => onDebugChangeServiceUrl(LOCAL_DEV_SERVICE)}
|
onPress={() => onDebugChangeServiceUrl(LOCAL_DEV_SERVICE)}
|
||||||
|
@ -112,11 +117,13 @@ function Option({
|
||||||
label,
|
label,
|
||||||
help,
|
help,
|
||||||
onPress,
|
onPress,
|
||||||
|
testID,
|
||||||
}: React.PropsWithChildren<{
|
}: React.PropsWithChildren<{
|
||||||
isSelected: boolean
|
isSelected: boolean
|
||||||
label: string
|
label: string
|
||||||
help?: string
|
help?: string
|
||||||
onPress: () => void
|
onPress: () => void
|
||||||
|
testID?: string
|
||||||
}>) {
|
}>) {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
@ -129,7 +136,7 @@ function Option({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.option, pal.border]}>
|
<View style={[styles.option, pal.border]}>
|
||||||
<TouchableWithoutFeedback onPress={onPress}>
|
<TouchableWithoutFeedback onPress={onPress} testID={testID}>
|
||||||
<View style={styles.optionHeading}>
|
<View style={styles.optionHeading}>
|
||||||
<View style={[styles.circle, pal.border]}>
|
<View style={[styles.circle, pal.border]}>
|
||||||
{isSelected ? (
|
{isSelected ? (
|
||||||
|
|
|
@ -59,6 +59,7 @@ export const Step2 = observer(({model}: {model: CreateAccountModel}) => {
|
||||||
Email address
|
Email address
|
||||||
</Text>
|
</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
testID="emailInput"
|
||||||
icon="envelope"
|
icon="envelope"
|
||||||
placeholder="Enter your email address"
|
placeholder="Enter your email address"
|
||||||
value={model.email}
|
value={model.email}
|
||||||
|
@ -72,6 +73,7 @@ export const Step2 = observer(({model}: {model: CreateAccountModel}) => {
|
||||||
Password
|
Password
|
||||||
</Text>
|
</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
testID="passwordInput"
|
||||||
icon="lock"
|
icon="lock"
|
||||||
placeholder="Choose your password"
|
placeholder="Choose your password"
|
||||||
value={model.password}
|
value={model.password}
|
||||||
|
@ -86,7 +88,7 @@ export const Step2 = observer(({model}: {model: CreateAccountModel}) => {
|
||||||
Legal check
|
Legal check
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="registerIs13Input"
|
testID="is13Input"
|
||||||
style={[styles.toggleBtn, pal.border]}
|
style={[styles.toggleBtn, pal.border]}
|
||||||
onPress={() => model.setIs13(!model.is13)}>
|
onPress={() => model.setIs13(!model.is13)}>
|
||||||
<View style={[pal.borderDark, styles.checkbox]}>
|
<View style={[pal.borderDark, styles.checkbox]}>
|
||||||
|
|
|
@ -17,6 +17,7 @@ export const Step3 = observer(({model}: {model: CreateAccountModel}) => {
|
||||||
<StepHeader step="3" title="Your user handle" />
|
<StepHeader step="3" title="Your user handle" />
|
||||||
<View style={s.pb10}>
|
<View style={s.pb10}>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
testID="handleInput"
|
||||||
icon="at"
|
icon="at"
|
||||||
placeholder="eg alice"
|
placeholder="eg alice"
|
||||||
value={model.handle}
|
value={model.handle}
|
||||||
|
|
|
@ -13,7 +13,7 @@ import {
|
||||||
FontAwesomeIconStyle,
|
FontAwesomeIconStyle,
|
||||||
} from '@fortawesome/react-native-fontawesome'
|
} from '@fortawesome/react-native-fontawesome'
|
||||||
import * as EmailValidator from 'email-validator'
|
import * as EmailValidator from 'email-validator'
|
||||||
import AtpAgent from '@atproto/api'
|
import {BskyAgent} from '@atproto/api'
|
||||||
import {useAnalytics} from 'lib/analytics'
|
import {useAnalytics} from 'lib/analytics'
|
||||||
import {Text} from '../../util/text/Text'
|
import {Text} from '../../util/text/Text'
|
||||||
import {UserAvatar} from '../../util/UserAvatar'
|
import {UserAvatar} from '../../util/UserAvatar'
|
||||||
|
@ -506,8 +506,8 @@ const ForgotPasswordForm = ({
|
||||||
setIsProcessing(true)
|
setIsProcessing(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const agent = new AtpAgent({service: serviceUrl})
|
const agent = new BskyAgent({service: serviceUrl})
|
||||||
await agent.api.com.atproto.account.requestPasswordReset({email})
|
await agent.com.atproto.server.requestPasswordReset({email})
|
||||||
onEmailSent()
|
onEmailSent()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
const errMsg = e.toString()
|
const errMsg = e.toString()
|
||||||
|
@ -648,8 +648,8 @@ const SetNewPasswordForm = ({
|
||||||
setIsProcessing(true)
|
setIsProcessing(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const agent = new AtpAgent({service: serviceUrl})
|
const agent = new BskyAgent({service: serviceUrl})
|
||||||
await agent.api.com.atproto.account.resetPassword({
|
await agent.com.atproto.server.resetPassword({
|
||||||
token: resetCode,
|
token: resetCode,
|
||||||
password,
|
password,
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, {useEffect, useRef, useState} from 'react'
|
import React from 'react'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
|
@ -13,6 +13,7 @@ import {
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import LinearGradient from 'react-native-linear-gradient'
|
import LinearGradient from 'react-native-linear-gradient'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
|
import {RichText} from '@atproto/api'
|
||||||
import {useAnalytics} from 'lib/analytics'
|
import {useAnalytics} from 'lib/analytics'
|
||||||
import {UserAutocompleteViewModel} from 'state/models/user-autocomplete-view'
|
import {UserAutocompleteViewModel} from 'state/models/user-autocomplete-view'
|
||||||
import {ExternalEmbed} from './ExternalEmbed'
|
import {ExternalEmbed} from './ExternalEmbed'
|
||||||
|
@ -30,11 +31,11 @@ import {SelectPhotoBtn} from './photos/SelectPhotoBtn'
|
||||||
import {OpenCameraBtn} from './photos/OpenCameraBtn'
|
import {OpenCameraBtn} from './photos/OpenCameraBtn'
|
||||||
import {SelectedPhotos} from './photos/SelectedPhotos'
|
import {SelectedPhotos} from './photos/SelectedPhotos'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import QuoteEmbed from '../util/PostEmbeds/QuoteEmbed'
|
import QuoteEmbed from '../util/post-embeds/QuoteEmbed'
|
||||||
import {useExternalLinkFetch} from './useExternalLinkFetch'
|
import {useExternalLinkFetch} from './useExternalLinkFetch'
|
||||||
import {isDesktopWeb} from 'platform/detection'
|
import {isDesktopWeb} from 'platform/detection'
|
||||||
|
|
||||||
const MAX_TEXT_LENGTH = 256
|
const MAX_GRAPHEME_LENGTH = 300
|
||||||
|
|
||||||
export const ComposePost = observer(function ComposePost({
|
export const ComposePost = observer(function ComposePost({
|
||||||
replyTo,
|
replyTo,
|
||||||
|
@ -50,17 +51,23 @@ export const ComposePost = observer(function ComposePost({
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const textInput = useRef<TextInputRef>(null)
|
const textInput = React.useRef<TextInputRef>(null)
|
||||||
const [isProcessing, setIsProcessing] = useState(false)
|
const [isProcessing, setIsProcessing] = React.useState(false)
|
||||||
const [processingState, setProcessingState] = useState('')
|
const [processingState, setProcessingState] = React.useState('')
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = React.useState('')
|
||||||
const [text, setText] = useState('')
|
const [richtext, setRichText] = React.useState(new RichText({text: ''}))
|
||||||
const [quote, setQuote] = useState<ComposerOpts['quote'] | undefined>(
|
const graphemeLength = React.useMemo(
|
||||||
|
() => richtext.graphemeLength,
|
||||||
|
[richtext],
|
||||||
|
)
|
||||||
|
const [quote, setQuote] = React.useState<ComposerOpts['quote'] | undefined>(
|
||||||
initQuote,
|
initQuote,
|
||||||
)
|
)
|
||||||
const {extLink, setExtLink} = useExternalLinkFetch({setQuote})
|
const {extLink, setExtLink} = useExternalLinkFetch({setQuote})
|
||||||
const [suggestedLinks, setSuggestedLinks] = useState<Set<string>>(new Set())
|
const [suggestedLinks, setSuggestedLinks] = React.useState<Set<string>>(
|
||||||
const [selectedPhotos, setSelectedPhotos] = useState<string[]>([])
|
new Set(),
|
||||||
|
)
|
||||||
|
const [selectedPhotos, setSelectedPhotos] = React.useState<string[]>([])
|
||||||
|
|
||||||
const autocompleteView = React.useMemo<UserAutocompleteViewModel>(
|
const autocompleteView = React.useMemo<UserAutocompleteViewModel>(
|
||||||
() => new UserAutocompleteViewModel(store),
|
() => new UserAutocompleteViewModel(store),
|
||||||
|
@ -78,11 +85,11 @@ export const ComposePost = observer(function ComposePost({
|
||||||
}, [textInput, onClose])
|
}, [textInput, onClose])
|
||||||
|
|
||||||
// initial setup
|
// initial setup
|
||||||
useEffect(() => {
|
React.useEffect(() => {
|
||||||
autocompleteView.setup()
|
autocompleteView.setup()
|
||||||
}, [autocompleteView])
|
}, [autocompleteView])
|
||||||
|
|
||||||
useEffect(() => {
|
React.useEffect(() => {
|
||||||
// HACK
|
// HACK
|
||||||
// wait a moment before focusing the input to resolve some layout bugs with the keyboard-avoiding-view
|
// wait a moment before focusing the input to resolve some layout bugs with the keyboard-avoiding-view
|
||||||
// -prf
|
// -prf
|
||||||
|
@ -132,18 +139,18 @@ export const ComposePost = observer(function ComposePost({
|
||||||
if (isProcessing) {
|
if (isProcessing) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (text.length > MAX_TEXT_LENGTH) {
|
if (richtext.graphemeLength > MAX_GRAPHEME_LENGTH) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setError('')
|
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?')
|
setError('Did you want to say anything?')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
setIsProcessing(true)
|
setIsProcessing(true)
|
||||||
try {
|
try {
|
||||||
await apilib.post(store, {
|
await apilib.post(store, {
|
||||||
rawText: text,
|
rawText: richtext.text,
|
||||||
replyTo: replyTo?.uri,
|
replyTo: replyTo?.uri,
|
||||||
images: selectedPhotos,
|
images: selectedPhotos,
|
||||||
quote: quote,
|
quote: quote,
|
||||||
|
@ -172,7 +179,7 @@ export const ComposePost = observer(function ComposePost({
|
||||||
Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`)
|
Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`)
|
||||||
}, [
|
}, [
|
||||||
isProcessing,
|
isProcessing,
|
||||||
text,
|
richtext,
|
||||||
setError,
|
setError,
|
||||||
setIsProcessing,
|
setIsProcessing,
|
||||||
replyTo,
|
replyTo,
|
||||||
|
@ -187,7 +194,7 @@ export const ComposePost = observer(function ComposePost({
|
||||||
track,
|
track,
|
||||||
])
|
])
|
||||||
|
|
||||||
const canPost = text.length <= MAX_TEXT_LENGTH
|
const canPost = graphemeLength <= MAX_GRAPHEME_LENGTH
|
||||||
|
|
||||||
const selectTextInputPlaceholder = replyTo
|
const selectTextInputPlaceholder = replyTo
|
||||||
? 'Write your reply'
|
? 'Write your reply'
|
||||||
|
@ -215,7 +222,7 @@ export const ComposePost = observer(function ComposePost({
|
||||||
</View>
|
</View>
|
||||||
) : canPost ? (
|
) : canPost ? (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="composerPublishButton"
|
testID="composerPublishBtn"
|
||||||
onPress={onPressPublish}>
|
onPress={onPressPublish}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||||
|
@ -271,42 +278,41 @@ export const ComposePost = observer(function ComposePost({
|
||||||
<UserAvatar avatar={store.me.avatar} size={50} />
|
<UserAvatar avatar={store.me.avatar} size={50} />
|
||||||
<TextInput
|
<TextInput
|
||||||
ref={textInput}
|
ref={textInput}
|
||||||
text={text}
|
richtext={richtext}
|
||||||
placeholder={selectTextInputPlaceholder}
|
placeholder={selectTextInputPlaceholder}
|
||||||
suggestedLinks={suggestedLinks}
|
suggestedLinks={suggestedLinks}
|
||||||
autocompleteView={autocompleteView}
|
autocompleteView={autocompleteView}
|
||||||
onTextChanged={setText}
|
setRichText={setRichText}
|
||||||
onPhotoPasted={onPhotoPasted}
|
onPhotoPasted={onPhotoPasted}
|
||||||
onSuggestedLinksChanged={setSuggestedLinks}
|
onSuggestedLinksChanged={setSuggestedLinks}
|
||||||
onError={setError}
|
onError={setError}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{quote ? (
|
|
||||||
<View style={s.mt5}>
|
|
||||||
<QuoteEmbed quote={quote} />
|
|
||||||
</View>
|
|
||||||
) : undefined}
|
|
||||||
|
|
||||||
<SelectedPhotos
|
<SelectedPhotos
|
||||||
selectedPhotos={selectedPhotos}
|
selectedPhotos={selectedPhotos}
|
||||||
onSelectPhotos={onSelectPhotos}
|
onSelectPhotos={onSelectPhotos}
|
||||||
/>
|
/>
|
||||||
{!selectedPhotos.length && extLink && (
|
{selectedPhotos.length === 0 && extLink && (
|
||||||
<ExternalEmbed
|
<ExternalEmbed
|
||||||
link={extLink}
|
link={extLink}
|
||||||
onRemove={() => setExtLink(undefined)}
|
onRemove={() => setExtLink(undefined)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{quote ? (
|
||||||
|
<View style={s.mt5}>
|
||||||
|
<QuoteEmbed quote={quote} />
|
||||||
|
</View>
|
||||||
|
) : undefined}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
{!extLink &&
|
{!extLink &&
|
||||||
selectedPhotos.length === 0 &&
|
selectedPhotos.length === 0 &&
|
||||||
suggestedLinks.size > 0 &&
|
suggestedLinks.size > 0 ? (
|
||||||
!quote ? (
|
|
||||||
<View style={s.mb5}>
|
<View style={s.mb5}>
|
||||||
{Array.from(suggestedLinks).map(url => (
|
{Array.from(suggestedLinks).map(url => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={`suggested-${url}`}
|
key={`suggested-${url}`}
|
||||||
|
testID="addLinkCardBtn"
|
||||||
style={[pal.borderDark, styles.addExtLinkBtn]}
|
style={[pal.borderDark, styles.addExtLinkBtn]}
|
||||||
onPress={() => onPressAddLinkCard(url)}>
|
onPress={() => onPressAddLinkCard(url)}>
|
||||||
<Text style={pal.text}>
|
<Text style={pal.text}>
|
||||||
|
@ -318,17 +324,17 @@ export const ComposePost = observer(function ComposePost({
|
||||||
) : null}
|
) : null}
|
||||||
<View style={[pal.border, styles.bottomBar]}>
|
<View style={[pal.border, styles.bottomBar]}>
|
||||||
<SelectPhotoBtn
|
<SelectPhotoBtn
|
||||||
enabled={!quote && selectedPhotos.length < 4}
|
enabled={selectedPhotos.length < 4}
|
||||||
selectedPhotos={selectedPhotos}
|
selectedPhotos={selectedPhotos}
|
||||||
onSelectPhotos={setSelectedPhotos}
|
onSelectPhotos={setSelectedPhotos}
|
||||||
/>
|
/>
|
||||||
<OpenCameraBtn
|
<OpenCameraBtn
|
||||||
enabled={!quote && selectedPhotos.length < 4}
|
enabled={selectedPhotos.length < 4}
|
||||||
selectedPhotos={selectedPhotos}
|
selectedPhotos={selectedPhotos}
|
||||||
onSelectPhotos={setSelectedPhotos}
|
onSelectPhotos={setSelectedPhotos}
|
||||||
/>
|
/>
|
||||||
<View style={s.flex1} />
|
<View style={s.flex1} />
|
||||||
<CharProgress count={text.length} />
|
<CharProgress count={graphemeLength} />
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
</TouchableWithoutFeedback>
|
</TouchableWithoutFeedback>
|
||||||
|
@ -408,6 +414,7 @@ const styles = StyleSheet.create({
|
||||||
borderRadius: 24,
|
borderRadius: 24,
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
paddingVertical: 12,
|
paddingVertical: 12,
|
||||||
|
marginHorizontal: 10,
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
},
|
},
|
||||||
bottomBar: {
|
bottomBar: {
|
||||||
|
|
|
@ -8,26 +8,24 @@ import ProgressPie from 'react-native-progress/Pie'
|
||||||
import {s} from 'lib/styles'
|
import {s} from 'lib/styles'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
|
||||||
const MAX_TEXT_LENGTH = 256
|
const MAX_LENGTH = 300
|
||||||
const DANGER_TEXT_LENGTH = MAX_TEXT_LENGTH
|
const DANGER_LENGTH = MAX_LENGTH
|
||||||
|
|
||||||
export function CharProgress({count}: {count: number}) {
|
export function CharProgress({count}: {count: number}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const textColor = count > DANGER_TEXT_LENGTH ? '#e60000' : pal.colors.text
|
const textColor = count > DANGER_LENGTH ? '#e60000' : pal.colors.text
|
||||||
const circleColor = count > DANGER_TEXT_LENGTH ? '#e60000' : pal.colors.link
|
const circleColor = count > DANGER_LENGTH ? '#e60000' : pal.colors.link
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Text style={[s.mr10, {color: textColor}]}>
|
<Text style={[s.mr10, {color: textColor}]}>{MAX_LENGTH - count}</Text>
|
||||||
{MAX_TEXT_LENGTH - count}
|
|
||||||
</Text>
|
|
||||||
<View>
|
<View>
|
||||||
{count > DANGER_TEXT_LENGTH ? (
|
{count > DANGER_LENGTH ? (
|
||||||
<ProgressPie
|
<ProgressPie
|
||||||
size={30}
|
size={30}
|
||||||
borderWidth={4}
|
borderWidth={4}
|
||||||
borderColor={circleColor}
|
borderColor={circleColor}
|
||||||
color={circleColor}
|
color={circleColor}
|
||||||
progress={Math.min((count - MAX_TEXT_LENGTH) / MAX_TEXT_LENGTH, 1)}
|
progress={Math.min((count - MAX_LENGTH) / MAX_LENGTH, 1)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ProgressCircle
|
<ProgressCircle
|
||||||
|
@ -35,7 +33,7 @@ export function CharProgress({count}: {count: number}) {
|
||||||
borderWidth={1}
|
borderWidth={1}
|
||||||
borderColor={pal.colors.border}
|
borderColor={pal.colors.border}
|
||||||
color={circleColor}
|
color={circleColor}
|
||||||
progress={count / MAX_TEXT_LENGTH}
|
progress={count / MAX_LENGTH}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -76,7 +76,11 @@ export function OpenCameraBtn({
|
||||||
hitSlop={HITSLOP}>
|
hitSlop={HITSLOP}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="camera"
|
icon="camera"
|
||||||
style={(enabled ? pal.link : pal.textLight) as FontAwesomeIconStyle}
|
style={
|
||||||
|
(enabled
|
||||||
|
? pal.link
|
||||||
|
: [pal.textLight, s.dimmed]) as FontAwesomeIconStyle
|
||||||
|
}
|
||||||
size={24}
|
size={24}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
|
@ -86,7 +86,11 @@ export function SelectPhotoBtn({
|
||||||
hitSlop={HITSLOP}>
|
hitSlop={HITSLOP}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={['far', 'image']}
|
icon={['far', 'image']}
|
||||||
style={(enabled ? pal.link : pal.textLight) as FontAwesomeIconStyle}
|
style={
|
||||||
|
(enabled
|
||||||
|
? pal.link
|
||||||
|
: [pal.textLight, s.dimmed]) as FontAwesomeIconStyle
|
||||||
|
}
|
||||||
size={24}
|
size={24}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
|
@ -9,13 +9,13 @@ import PasteInput, {
|
||||||
PastedFile,
|
PastedFile,
|
||||||
PasteInputRef,
|
PasteInputRef,
|
||||||
} from '@mattermost/react-native-paste-input'
|
} from '@mattermost/react-native-paste-input'
|
||||||
|
import {AppBskyRichtextFacet, RichText} from '@atproto/api'
|
||||||
import isEqual from 'lodash.isequal'
|
import isEqual from 'lodash.isequal'
|
||||||
import {UserAutocompleteViewModel} from 'state/models/user-autocomplete-view'
|
import {UserAutocompleteViewModel} from 'state/models/user-autocomplete-view'
|
||||||
import {Autocomplete} from './mobile/Autocomplete'
|
import {Autocomplete} from './mobile/Autocomplete'
|
||||||
import {Text} from 'view/com/util/text/Text'
|
import {Text} from 'view/com/util/text/Text'
|
||||||
import {useStores} from 'state/index'
|
import {useStores} from 'state/index'
|
||||||
import {cleanError} from 'lib/strings/errors'
|
import {cleanError} from 'lib/strings/errors'
|
||||||
import {detectLinkables, extractEntities} from 'lib/strings/rich-text-detection'
|
|
||||||
import {getImageDim} from 'lib/media/manip'
|
import {getImageDim} from 'lib/media/manip'
|
||||||
import {cropAndCompressFlow} from 'lib/media/picker'
|
import {cropAndCompressFlow} from 'lib/media/picker'
|
||||||
import {getMentionAt, insertMentionAt} from 'lib/strings/mention-manip'
|
import {getMentionAt, insertMentionAt} from 'lib/strings/mention-manip'
|
||||||
|
@ -33,11 +33,11 @@ export interface TextInputRef {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TextInputProps {
|
interface TextInputProps {
|
||||||
text: string
|
richtext: RichText
|
||||||
placeholder: string
|
placeholder: string
|
||||||
suggestedLinks: Set<string>
|
suggestedLinks: Set<string>
|
||||||
autocompleteView: UserAutocompleteViewModel
|
autocompleteView: UserAutocompleteViewModel
|
||||||
onTextChanged: (v: string) => void
|
setRichText: (v: RichText) => void
|
||||||
onPhotoPasted: (uri: string) => void
|
onPhotoPasted: (uri: string) => void
|
||||||
onSuggestedLinksChanged: (uris: Set<string>) => void
|
onSuggestedLinksChanged: (uris: Set<string>) => void
|
||||||
onError: (err: string) => void
|
onError: (err: string) => void
|
||||||
|
@ -51,11 +51,11 @@ interface Selection {
|
||||||
export const TextInput = React.forwardRef(
|
export const TextInput = React.forwardRef(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
text,
|
richtext,
|
||||||
placeholder,
|
placeholder,
|
||||||
suggestedLinks,
|
suggestedLinks,
|
||||||
autocompleteView,
|
autocompleteView,
|
||||||
onTextChanged,
|
setRichText,
|
||||||
onPhotoPasted,
|
onPhotoPasted,
|
||||||
onSuggestedLinksChanged,
|
onSuggestedLinksChanged,
|
||||||
onError,
|
onError,
|
||||||
|
@ -92,7 +92,9 @@ export const TextInput = React.forwardRef(
|
||||||
|
|
||||||
const onChangeText = React.useCallback(
|
const onChangeText = React.useCallback(
|
||||||
(newText: string) => {
|
(newText: string) => {
|
||||||
onTextChanged(newText)
|
const newRt = new RichText({text: newText})
|
||||||
|
newRt.detectFacetsWithoutResolution()
|
||||||
|
setRichText(newRt)
|
||||||
|
|
||||||
const prefix = getMentionAt(
|
const prefix = getMentionAt(
|
||||||
newText,
|
newText,
|
||||||
|
@ -105,20 +107,21 @@ export const TextInput = React.forwardRef(
|
||||||
autocompleteView.setActive(false)
|
autocompleteView.setActive(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ents = extractEntities(newText)?.filter(
|
const set: Set<string> = new Set()
|
||||||
ent => ent.type === 'link',
|
if (newRt.facets) {
|
||||||
)
|
for (const facet of newRt.facets) {
|
||||||
const set = new Set(ents ? ents.map(e => e.value) : [])
|
for (const feature of facet.features) {
|
||||||
|
if (AppBskyRichtextFacet.isLink(feature)) {
|
||||||
|
set.add(feature.uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!isEqual(set, suggestedLinks)) {
|
if (!isEqual(set, suggestedLinks)) {
|
||||||
onSuggestedLinksChanged(set)
|
onSuggestedLinksChanged(set)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[setRichText, autocompleteView, suggestedLinks, onSuggestedLinksChanged],
|
||||||
onTextChanged,
|
|
||||||
autocompleteView,
|
|
||||||
suggestedLinks,
|
|
||||||
onSuggestedLinksChanged,
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const onPaste = React.useCallback(
|
const onPaste = React.useCallback(
|
||||||
|
@ -159,31 +162,35 @@ export const TextInput = React.forwardRef(
|
||||||
const onSelectAutocompleteItem = React.useCallback(
|
const onSelectAutocompleteItem = React.useCallback(
|
||||||
(item: string) => {
|
(item: string) => {
|
||||||
onChangeText(
|
onChangeText(
|
||||||
insertMentionAt(text, textInputSelection.current?.start || 0, item),
|
insertMentionAt(
|
||||||
|
richtext.text,
|
||||||
|
textInputSelection.current?.start || 0,
|
||||||
|
item,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
autocompleteView.setActive(false)
|
autocompleteView.setActive(false)
|
||||||
},
|
},
|
||||||
[onChangeText, text, autocompleteView],
|
[onChangeText, richtext, autocompleteView],
|
||||||
)
|
)
|
||||||
|
|
||||||
const textDecorated = React.useMemo(() => {
|
const textDecorated = React.useMemo(() => {
|
||||||
let i = 0
|
let i = 0
|
||||||
return detectLinkables(text).map(v => {
|
return Array.from(richtext.segments()).map(segment => {
|
||||||
if (typeof v === 'string') {
|
if (!segment.facet) {
|
||||||
return (
|
return (
|
||||||
<Text key={i++} style={[pal.text, styles.textInputFormatting]}>
|
<Text key={i++} style={[pal.text, styles.textInputFormatting]}>
|
||||||
{v}
|
{segment.text}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Text key={i++} style={[pal.link, styles.textInputFormatting]}>
|
<Text key={i++} style={[pal.link, styles.textInputFormatting]}>
|
||||||
{v.link}
|
{segment.text}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [text, pal.link, pal.text])
|
}, [richtext, pal.link, pal.text])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {StyleSheet, View} from 'react-native'
|
import {StyleSheet, View} from 'react-native'
|
||||||
|
import {RichText} from '@atproto/api'
|
||||||
import {useEditor, EditorContent, JSONContent} from '@tiptap/react'
|
import {useEditor, EditorContent, JSONContent} from '@tiptap/react'
|
||||||
import {Document} from '@tiptap/extension-document'
|
import {Document} from '@tiptap/extension-document'
|
||||||
import {Link} from '@tiptap/extension-link'
|
import {Link} from '@tiptap/extension-link'
|
||||||
|
@ -17,11 +18,11 @@ export interface TextInputRef {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TextInputProps {
|
interface TextInputProps {
|
||||||
text: string
|
richtext: RichText
|
||||||
placeholder: string
|
placeholder: string
|
||||||
suggestedLinks: Set<string>
|
suggestedLinks: Set<string>
|
||||||
autocompleteView: UserAutocompleteViewModel
|
autocompleteView: UserAutocompleteViewModel
|
||||||
onTextChanged: (v: string) => void
|
setRichText: (v: RichText) => void
|
||||||
onPhotoPasted: (uri: string) => void
|
onPhotoPasted: (uri: string) => void
|
||||||
onSuggestedLinksChanged: (uris: Set<string>) => void
|
onSuggestedLinksChanged: (uris: Set<string>) => void
|
||||||
onError: (err: string) => void
|
onError: (err: string) => void
|
||||||
|
@ -30,11 +31,11 @@ interface TextInputProps {
|
||||||
export const TextInput = React.forwardRef(
|
export const TextInput = React.forwardRef(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
text,
|
richtext,
|
||||||
placeholder,
|
placeholder,
|
||||||
suggestedLinks,
|
suggestedLinks,
|
||||||
autocompleteView,
|
autocompleteView,
|
||||||
onTextChanged,
|
setRichText,
|
||||||
// onPhotoPasted, TODO
|
// onPhotoPasted, TODO
|
||||||
onSuggestedLinksChanged,
|
onSuggestedLinksChanged,
|
||||||
}: // onError, TODO
|
}: // onError, TODO
|
||||||
|
@ -60,15 +61,15 @@ export const TextInput = React.forwardRef(
|
||||||
}),
|
}),
|
||||||
Text,
|
Text,
|
||||||
],
|
],
|
||||||
content: text,
|
content: richtext.text.toString(),
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
editable: true,
|
editable: true,
|
||||||
injectCSS: true,
|
injectCSS: true,
|
||||||
onUpdate({editor: editorProp}) {
|
onUpdate({editor: editorProp}) {
|
||||||
const json = editorProp.getJSON()
|
const json = editorProp.getJSON()
|
||||||
|
|
||||||
const newText = editorJsonToText(json).trim()
|
const newRt = new RichText({text: editorJsonToText(json).trim()})
|
||||||
onTextChanged(newText)
|
setRichText(newRt)
|
||||||
|
|
||||||
const newSuggestedLinks = new Set(editorJsonToLinks(json))
|
const newSuggestedLinks = new Set(editorJsonToLinks(json))
|
||||||
if (!isEqual(newSuggestedLinks, suggestedLinks)) {
|
if (!isEqual(newSuggestedLinks, suggestedLinks)) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {StyleSheet, View} from 'react-native'
|
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 {RefWithInfoAndFollowers} from 'state/models/discovery/foafs'
|
||||||
import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
|
import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
|
||||||
import {Text} from '../util/text/Text'
|
import {Text} from '../util/text/Text'
|
||||||
|
@ -12,9 +12,9 @@ export const SuggestedFollows = ({
|
||||||
}: {
|
}: {
|
||||||
title: string
|
title: string
|
||||||
suggestions: (
|
suggestions: (
|
||||||
| AppBskyActorRef.WithInfo
|
| AppBskyActorDefs.ProfileViewBasic
|
||||||
|
| AppBskyActorDefs.ProfileView
|
||||||
| RefWithInfoAndFollowers
|
| RefWithInfoAndFollowers
|
||||||
| AppBskyActorProfile.View
|
|
||||||
)[]
|
)[]
|
||||||
}) => {
|
}) => {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
@ -28,7 +28,6 @@ export const SuggestedFollows = ({
|
||||||
<ProfileCardWithFollowBtn
|
<ProfileCardWithFollowBtn
|
||||||
key={item.did}
|
key={item.did}
|
||||||
did={item.did}
|
did={item.did}
|
||||||
declarationCid={item.declaration.cid}
|
|
||||||
handle={item.handle}
|
handle={item.handle}
|
||||||
displayName={item.displayName}
|
displayName={item.displayName}
|
||||||
avatar={item.avatar}
|
avatar={item.avatar}
|
||||||
|
@ -36,12 +35,12 @@ export const SuggestedFollows = ({
|
||||||
noBorder
|
noBorder
|
||||||
description={
|
description={
|
||||||
item.description
|
item.description
|
||||||
? (item as AppBskyActorProfile.View).description
|
? (item as AppBskyActorDefs.ProfileView).description
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
followers={
|
followers={
|
||||||
item.followers
|
item.followers
|
||||||
? (item.followers as AppBskyActorProfile.View[])
|
? (item.followers as AppBskyActorDefs.ProfileView[])
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -105,7 +105,7 @@ export function Component({onChanged}: {onChanged: () => void}) {
|
||||||
track('EditHandle:SetNewHandle')
|
track('EditHandle:SetNewHandle')
|
||||||
const newHandle = isCustom ? handle : createFullHandle(handle, userDomain)
|
const newHandle = isCustom ? handle : createFullHandle(handle, userDomain)
|
||||||
store.log.debug(`Updating handle to ${newHandle}`)
|
store.log.debug(`Updating handle to ${newHandle}`)
|
||||||
await store.api.com.atproto.handle.update({
|
await store.agent.updateHandle({
|
||||||
handle: newHandle,
|
handle: newHandle,
|
||||||
})
|
})
|
||||||
store.shell.closeModal()
|
store.shell.closeModal()
|
||||||
|
@ -310,7 +310,7 @@ function CustomHandleForm({
|
||||||
try {
|
try {
|
||||||
setIsVerifying(true)
|
setIsVerifying(true)
|
||||||
setError('')
|
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) {
|
if (res.data.did === store.me.did) {
|
||||||
setCanSave(true)
|
setCanSave(true)
|
||||||
} else {
|
} else {
|
||||||
|
@ -331,7 +331,7 @@ function CustomHandleForm({
|
||||||
canSave,
|
canSave,
|
||||||
onPressSave,
|
onPressSave,
|
||||||
store.log,
|
store.log,
|
||||||
store.api,
|
store.agent,
|
||||||
])
|
])
|
||||||
|
|
||||||
// rendering
|
// rendering
|
||||||
|
|
|
@ -39,7 +39,7 @@ export function Component({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
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>
|
<Text style={styles.title}>{title}</Text>
|
||||||
{typeof message === 'string' ? (
|
{typeof message === 'string' ? (
|
||||||
<Text style={styles.description}>{message}</Text>
|
<Text style={styles.description}>{message}</Text>
|
||||||
|
@ -56,7 +56,7 @@ export function Component({
|
||||||
<ActivityIndicator />
|
<ActivityIndicator />
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<TouchableOpacity style={s.mt10} onPress={onPress}>
|
<TouchableOpacity testID="confirmBtn" style={s.mt10} onPress={onPress}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||||
start={{x: 0, y: 0}}
|
start={{x: 0, y: 0}}
|
||||||
|
|
|
@ -32,7 +32,7 @@ export function Component({}: {}) {
|
||||||
setError('')
|
setError('')
|
||||||
setIsProcessing(true)
|
setIsProcessing(true)
|
||||||
try {
|
try {
|
||||||
await store.api.com.atproto.account.requestDelete()
|
await store.agent.com.atproto.server.requestAccountDelete()
|
||||||
setIsEmailSent(true)
|
setIsEmailSent(true)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setError(cleanError(e))
|
setError(cleanError(e))
|
||||||
|
@ -43,7 +43,7 @@ export function Component({}: {}) {
|
||||||
setError('')
|
setError('')
|
||||||
setIsProcessing(true)
|
setIsProcessing(true)
|
||||||
try {
|
try {
|
||||||
await store.api.com.atproto.account.delete({
|
await store.agent.com.atproto.server.deleteAccount({
|
||||||
did: store.me.did,
|
did: store.me.did,
|
||||||
password,
|
password,
|
||||||
token: confirmCode,
|
token: confirmCode,
|
||||||
|
|
|
@ -123,7 +123,7 @@ export function Component({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[s.flex1, pal.view]}>
|
<View style={[s.flex1, pal.view]} testID="editProfileModal">
|
||||||
<ScrollView style={styles.inner}>
|
<ScrollView style={styles.inner}>
|
||||||
<Text style={[styles.title, pal.text]}>Edit my profile</Text>
|
<Text style={[styles.title, pal.text]}>Edit my profile</Text>
|
||||||
<View style={styles.photos}>
|
<View style={styles.photos}>
|
||||||
|
@ -147,6 +147,7 @@ export function Component({
|
||||||
<View>
|
<View>
|
||||||
<Text style={[styles.label, pal.text]}>Display Name</Text>
|
<Text style={[styles.label, pal.text]}>Display Name</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
testID="editProfileDisplayNameInput"
|
||||||
style={[styles.textInput, pal.text]}
|
style={[styles.textInput, pal.text]}
|
||||||
placeholder="e.g. Alice Roberts"
|
placeholder="e.g. Alice Roberts"
|
||||||
placeholderTextColor={colors.gray4}
|
placeholderTextColor={colors.gray4}
|
||||||
|
@ -157,6 +158,7 @@ export function Component({
|
||||||
<View style={s.pb10}>
|
<View style={s.pb10}>
|
||||||
<Text style={[styles.label, pal.text]}>Description</Text>
|
<Text style={[styles.label, pal.text]}>Description</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
testID="editProfileDescriptionInput"
|
||||||
style={[styles.textArea, pal.text]}
|
style={[styles.textArea, pal.text]}
|
||||||
placeholder="e.g. Artist, dog-lover, and memelord."
|
placeholder="e.g. Artist, dog-lover, and memelord."
|
||||||
placeholderTextColor={colors.gray4}
|
placeholderTextColor={colors.gray4}
|
||||||
|
@ -171,7 +173,10 @@ export function Component({
|
||||||
<ActivityIndicator />
|
<ActivityIndicator />
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<TouchableOpacity style={s.mt10} onPress={onPressSave}>
|
<TouchableOpacity
|
||||||
|
testID="editProfileSaveBtn"
|
||||||
|
style={s.mt10}
|
||||||
|
onPress={onPressSave}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||||
start={{x: 0, y: 0}}
|
start={{x: 0, y: 0}}
|
||||||
|
@ -181,7 +186,10 @@ export function Component({
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
<TouchableOpacity style={s.mt5} onPress={onPressCancel}>
|
<TouchableOpacity
|
||||||
|
testID="editProfileCancelBtn"
|
||||||
|
style={s.mt5}
|
||||||
|
onPress={onPressCancel}>
|
||||||
<View style={[styles.btn]}>
|
<View style={[styles.btn]}>
|
||||||
<Text style={[s.black, s.bold, pal.text]}>Cancel</Text>
|
<Text style={[s.black, s.bold, pal.text]}>Cancel</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {ComAtprotoReportReasonType} from '@atproto/api'
|
import {ComAtprotoModerationDefs} from '@atproto/api'
|
||||||
import LinearGradient from 'react-native-linear-gradient'
|
import LinearGradient from 'react-native-linear-gradient'
|
||||||
import {useStores} from 'state/index'
|
import {useStores} from 'state/index'
|
||||||
import {s, colors, gradients} from 'lib/styles'
|
import {s, colors, gradients} from 'lib/styles'
|
||||||
|
@ -39,16 +39,16 @@ export function Component({did}: {did: string}) {
|
||||||
setIsProcessing(true)
|
setIsProcessing(true)
|
||||||
try {
|
try {
|
||||||
// NOTE: we should update the lexicon of reasontype to include more options -prf
|
// NOTE: we should update the lexicon of reasontype to include more options -prf
|
||||||
let reasonType = ComAtprotoReportReasonType.OTHER
|
let reasonType = ComAtprotoModerationDefs.REASONOTHER
|
||||||
if (issue === 'spam') {
|
if (issue === 'spam') {
|
||||||
reasonType = ComAtprotoReportReasonType.SPAM
|
reasonType = ComAtprotoModerationDefs.REASONSPAM
|
||||||
}
|
}
|
||||||
const reason = ITEMS.find(item => item.key === issue)?.label || ''
|
const reason = ITEMS.find(item => item.key === issue)?.label || ''
|
||||||
await store.api.com.atproto.report.create({
|
await store.agent.com.atproto.moderation.createReport({
|
||||||
reasonType,
|
reasonType,
|
||||||
reason,
|
reason,
|
||||||
subject: {
|
subject: {
|
||||||
$type: 'com.atproto.repo.repoRef',
|
$type: 'com.atproto.admin.defs#repoRef',
|
||||||
did,
|
did,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -61,12 +61,18 @@ export function Component({did}: {did: string}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
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.text, styles.title]}>Report account</Text>
|
||||||
<Text style={[pal.textLight, styles.description]}>
|
<Text style={[pal.textLight, styles.description]}>
|
||||||
What is the issue with this account?
|
What is the issue with this account?
|
||||||
</Text>
|
</Text>
|
||||||
<RadioGroup items={ITEMS} onSelect={onSelectIssue} />
|
<RadioGroup
|
||||||
|
testID="reportAccountRadios"
|
||||||
|
items={ITEMS}
|
||||||
|
onSelect={onSelectIssue}
|
||||||
|
/>
|
||||||
{error ? (
|
{error ? (
|
||||||
<View style={s.mt10}>
|
<View style={s.mt10}>
|
||||||
<ErrorMessage message={error} />
|
<ErrorMessage message={error} />
|
||||||
|
@ -77,7 +83,10 @@ export function Component({did}: {did: string}) {
|
||||||
<ActivityIndicator />
|
<ActivityIndicator />
|
||||||
</View>
|
</View>
|
||||||
) : issue ? (
|
) : issue ? (
|
||||||
<TouchableOpacity style={s.mt10} onPress={onPress}>
|
<TouchableOpacity
|
||||||
|
testID="sendReportBtn"
|
||||||
|
style={s.mt10}
|
||||||
|
onPress={onPress}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||||
start={{x: 0, y: 0}}
|
start={{x: 0, y: 0}}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {ComAtprotoReportReasonType} from '@atproto/api'
|
import {ComAtprotoModerationDefs} from '@atproto/api'
|
||||||
import LinearGradient from 'react-native-linear-gradient'
|
import LinearGradient from 'react-native-linear-gradient'
|
||||||
import {useStores} from 'state/index'
|
import {useStores} from 'state/index'
|
||||||
import {s, colors, gradients} from 'lib/styles'
|
import {s, colors, gradients} from 'lib/styles'
|
||||||
|
@ -46,16 +46,16 @@ export function Component({
|
||||||
setIsProcessing(true)
|
setIsProcessing(true)
|
||||||
try {
|
try {
|
||||||
// NOTE: we should update the lexicon of reasontype to include more options -prf
|
// NOTE: we should update the lexicon of reasontype to include more options -prf
|
||||||
let reasonType = ComAtprotoReportReasonType.OTHER
|
let reasonType = ComAtprotoModerationDefs.REASONOTHER
|
||||||
if (issue === 'spam') {
|
if (issue === 'spam') {
|
||||||
reasonType = ComAtprotoReportReasonType.SPAM
|
reasonType = ComAtprotoModerationDefs.REASONSPAM
|
||||||
}
|
}
|
||||||
const reason = ITEMS.find(item => item.key === issue)?.label || ''
|
const reason = ITEMS.find(item => item.key === issue)?.label || ''
|
||||||
await store.api.com.atproto.report.create({
|
await store.agent.createModerationReport({
|
||||||
reasonType,
|
reasonType,
|
||||||
reason,
|
reason,
|
||||||
subject: {
|
subject: {
|
||||||
$type: 'com.atproto.repo.recordRef',
|
$type: 'com.atproto.repo.strongRef',
|
||||||
uri: postUri,
|
uri: postUri,
|
||||||
cid: postCid,
|
cid: postCid,
|
||||||
},
|
},
|
||||||
|
@ -69,12 +69,16 @@ export function Component({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
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.text, styles.title]}>Report post</Text>
|
||||||
<Text style={[pal.textLight, styles.description]}>
|
<Text style={[pal.textLight, styles.description]}>
|
||||||
What is the issue with this post?
|
What is the issue with this post?
|
||||||
</Text>
|
</Text>
|
||||||
<RadioGroup items={ITEMS} onSelect={onSelectIssue} />
|
<RadioGroup
|
||||||
|
testID="reportPostRadios"
|
||||||
|
items={ITEMS}
|
||||||
|
onSelect={onSelectIssue}
|
||||||
|
/>
|
||||||
{error ? (
|
{error ? (
|
||||||
<View style={s.mt10}>
|
<View style={s.mt10}>
|
||||||
<ErrorMessage message={error} />
|
<ErrorMessage message={error} />
|
||||||
|
@ -85,7 +89,10 @@ export function Component({
|
||||||
<ActivityIndicator />
|
<ActivityIndicator />
|
||||||
</View>
|
</View>
|
||||||
) : issue ? (
|
) : issue ? (
|
||||||
<TouchableOpacity style={s.mt10} onPress={onPress}>
|
<TouchableOpacity
|
||||||
|
testID="sendReportBtn"
|
||||||
|
style={s.mt10}
|
||||||
|
onPress={onPress}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||||
start={{x: 0, y: 0}}
|
start={{x: 0, y: 0}}
|
||||||
|
|
|
@ -26,22 +26,28 @@ export function Component({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[s.flex1, pal.view, styles.container]}>
|
<View testID="repostModal" style={[s.flex1, pal.view, styles.container]}>
|
||||||
<View style={s.pb20}>
|
<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} />
|
<RepostIcon strokeWidth={2} size={24} style={s.blue3} />
|
||||||
<Text type="title-lg" style={[styles.actionBtnLabel, pal.text]}>
|
<Text type="title-lg" style={[styles.actionBtnLabel, pal.text]}>
|
||||||
{!isReposted ? 'Repost' : 'Undo repost'}
|
{!isReposted ? 'Repost' : 'Undo repost'}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity style={[styles.actionBtn]} onPress={onQuote}>
|
<TouchableOpacity
|
||||||
|
testID="quoteBtn"
|
||||||
|
style={[styles.actionBtn]}
|
||||||
|
onPress={onQuote}>
|
||||||
<FontAwesomeIcon icon="quote-left" size={24} style={s.blue3} />
|
<FontAwesomeIcon icon="quote-left" size={24} style={s.blue3} />
|
||||||
<Text type="title-lg" style={[styles.actionBtnLabel, pal.text]}>
|
<Text type="title-lg" style={[styles.actionBtnLabel, pal.text]}>
|
||||||
Quote Post
|
Quote Post
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity onPress={onPress}>
|
<TouchableOpacity testID="cancelBtn" onPress={onPress}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||||
start={{x: 0, y: 0}}
|
start={{x: 0, y: 0}}
|
||||||
|
|
|
@ -47,10 +47,10 @@ export const FeedItem = observer(function FeedItem({
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const [isAuthorsExpanded, setAuthorsExpanded] = React.useState<boolean>(false)
|
const [isAuthorsExpanded, setAuthorsExpanded] = React.useState<boolean>(false)
|
||||||
const itemHref = React.useMemo(() => {
|
const itemHref = React.useMemo(() => {
|
||||||
if (item.isUpvote || item.isRepost) {
|
if (item.isLike || item.isRepost) {
|
||||||
const urip = new AtUri(item.subjectUri)
|
const urip = new AtUri(item.subjectUri)
|
||||||
return `/profile/${urip.host}/post/${urip.rkey}`
|
return `/profile/${urip.host}/post/${urip.rkey}`
|
||||||
} else if (item.isFollow || item.isAssertion) {
|
} else if (item.isFollow) {
|
||||||
return `/profile/${item.author.handle}`
|
return `/profile/${item.author.handle}`
|
||||||
} else if (item.isReply) {
|
} else if (item.isReply) {
|
||||||
const urip = new AtUri(item.uri)
|
const urip = new AtUri(item.uri)
|
||||||
|
@ -59,9 +59,9 @@ export const FeedItem = observer(function FeedItem({
|
||||||
return ''
|
return ''
|
||||||
}, [item])
|
}, [item])
|
||||||
const itemTitle = React.useMemo(() => {
|
const itemTitle = React.useMemo(() => {
|
||||||
if (item.isUpvote || item.isRepost) {
|
if (item.isLike || item.isRepost) {
|
||||||
return 'Post'
|
return 'Post'
|
||||||
} else if (item.isFollow || item.isAssertion) {
|
} else if (item.isFollow) {
|
||||||
return item.author.handle
|
return item.author.handle
|
||||||
} else if (item.isReply) {
|
} else if (item.isReply) {
|
||||||
return 'Post'
|
return 'Post'
|
||||||
|
@ -77,7 +77,7 @@ export const FeedItem = observer(function FeedItem({
|
||||||
return <View />
|
return <View />
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.isReply || item.isMention) {
|
if (item.isReply || item.isMention || item.isQuote) {
|
||||||
if (item.additionalPost?.error) {
|
if (item.additionalPost?.error) {
|
||||||
// hide errors - it doesnt help the user to show them
|
// hide errors - it doesnt help the user to show them
|
||||||
return <View />
|
return <View />
|
||||||
|
@ -103,7 +103,7 @@ export const FeedItem = observer(function FeedItem({
|
||||||
let action = ''
|
let action = ''
|
||||||
let icon: Props['icon'] | 'HeartIconSolid'
|
let icon: Props['icon'] | 'HeartIconSolid'
|
||||||
let iconStyle: Props['style'] = []
|
let iconStyle: Props['style'] = []
|
||||||
if (item.isUpvote) {
|
if (item.isLike) {
|
||||||
action = 'liked your post'
|
action = 'liked your post'
|
||||||
icon = 'HeartIconSolid'
|
icon = 'HeartIconSolid'
|
||||||
iconStyle = [
|
iconStyle = [
|
||||||
|
@ -114,9 +114,6 @@ export const FeedItem = observer(function FeedItem({
|
||||||
action = 'reposted your post'
|
action = 'reposted your post'
|
||||||
icon = 'retweet'
|
icon = 'retweet'
|
||||||
iconStyle = [s.green3 as FontAwesomeIconStyle]
|
iconStyle = [s.green3 as FontAwesomeIconStyle]
|
||||||
} else if (item.isReply) {
|
|
||||||
action = 'replied to your post'
|
|
||||||
icon = ['far', 'comment']
|
|
||||||
} else if (item.isFollow) {
|
} else if (item.isFollow) {
|
||||||
action = 'followed you'
|
action = 'followed you'
|
||||||
icon = 'user-plus'
|
icon = 'user-plus'
|
||||||
|
@ -208,7 +205,7 @@ export const FeedItem = observer(function FeedItem({
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</TouchableWithoutFeedback>
|
</TouchableWithoutFeedback>
|
||||||
{item.isUpvote || item.isRepost ? (
|
{item.isLike || item.isRepost || item.isQuote ? (
|
||||||
<AdditionalPostText additionalPost={item.additionalPost} />
|
<AdditionalPostText additionalPost={item.additionalPost} />
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
|
@ -352,9 +349,9 @@ function AdditionalPostText({
|
||||||
return <View />
|
return <View />
|
||||||
}
|
}
|
||||||
const text = additionalPost.thread?.postRecord.text
|
const text = additionalPost.thread?.postRecord.text
|
||||||
const images = (
|
const images = AppBskyEmbedImages.isView(additionalPost.thread.post.embed)
|
||||||
additionalPost.thread.post.embed as AppBskyEmbedImages.Presented
|
? additionalPost.thread.post.embed.images
|
||||||
)?.images
|
: undefined
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{text?.length > 0 && <Text style={pal.textLight}>{text}</Text>}
|
{text?.length > 0 && <Text style={pal.textLight}>{text}</Text>}
|
||||||
|
|
|
@ -9,7 +9,9 @@ import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
|
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
|
||||||
|
|
||||||
export const FeedsTabBar = observer(
|
export const FeedsTabBar = observer(
|
||||||
(props: RenderTabBarFnProps & {onPressSelected: () => void}) => {
|
(
|
||||||
|
props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
|
||||||
|
) => {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const interp = useAnimatedValue(0)
|
const interp = useAnimatedValue(0)
|
||||||
|
@ -32,7 +34,10 @@ export const FeedsTabBar = observer(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View style={[pal.view, styles.tabBar, transform]}>
|
<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} />
|
<UserAvatar avatar={store.me.avatar} size={30} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TabBar
|
<TabBar
|
||||||
|
|
|
@ -20,6 +20,7 @@ interface Props {
|
||||||
initialPage?: number
|
initialPage?: number
|
||||||
renderTabBar: RenderTabBarFn
|
renderTabBar: RenderTabBarFn
|
||||||
onPageSelected?: (index: number) => void
|
onPageSelected?: (index: number) => void
|
||||||
|
testID?: string
|
||||||
}
|
}
|
||||||
export const Pager = ({
|
export const Pager = ({
|
||||||
children,
|
children,
|
||||||
|
@ -27,6 +28,7 @@ export const Pager = ({
|
||||||
initialPage = 0,
|
initialPage = 0,
|
||||||
renderTabBar,
|
renderTabBar,
|
||||||
onPageSelected,
|
onPageSelected,
|
||||||
|
testID,
|
||||||
}: React.PropsWithChildren<Props>) => {
|
}: React.PropsWithChildren<Props>) => {
|
||||||
const [selectedPage, setSelectedPage] = React.useState(0)
|
const [selectedPage, setSelectedPage] = React.useState(0)
|
||||||
const position = useAnimatedValue(0)
|
const position = useAnimatedValue(0)
|
||||||
|
@ -49,7 +51,7 @@ export const Pager = ({
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View testID={testID}>
|
||||||
{tabBarPosition === 'top' &&
|
{tabBarPosition === 'top' &&
|
||||||
renderTabBar({
|
renderTabBar({
|
||||||
selectedPage,
|
selectedPage,
|
||||||
|
|
|
@ -15,6 +15,7 @@ interface Layout {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TabBarProps {
|
export interface TabBarProps {
|
||||||
|
testID?: string
|
||||||
selectedPage: number
|
selectedPage: number
|
||||||
items: string[]
|
items: string[]
|
||||||
position: Animated.Value
|
position: Animated.Value
|
||||||
|
@ -26,6 +27,7 @@ export interface TabBarProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TabBar({
|
export function TabBar({
|
||||||
|
testID,
|
||||||
selectedPage,
|
selectedPage,
|
||||||
items,
|
items,
|
||||||
position,
|
position,
|
||||||
|
@ -92,12 +94,15 @@ export function TabBar({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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]} />
|
<Animated.View style={[styles.indicator, indicatorStyle]} />
|
||||||
{items.map((item, i) => {
|
{items.map((item, i) => {
|
||||||
const selected = i === selectedPage
|
const selected = i === selectedPage
|
||||||
return (
|
return (
|
||||||
<TouchableWithoutFeedback key={i} onPress={() => onPressItem(i)}>
|
<TouchableWithoutFeedback
|
||||||
|
key={i}
|
||||||
|
testID={testID ? `${testID}-${item}` : undefined}
|
||||||
|
onPress={() => onPressItem(i)}>
|
||||||
<View
|
<View
|
||||||
style={
|
style={
|
||||||
indicatorPosition === 'top' ? styles.itemTop : styles.itemBottom
|
indicatorPosition === 'top' ? styles.itemTop : styles.itemBottom
|
||||||
|
|
|
@ -2,24 +2,18 @@ import React, {useEffect} from 'react'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native'
|
import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native'
|
||||||
import {CenteredView, FlatList} from '../util/Views'
|
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 {ErrorMessage} from '../util/error/ErrorMessage'
|
||||||
import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
|
import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
|
||||||
import {useStores} from 'state/index'
|
import {useStores} from 'state/index'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
|
||||||
export const PostVotedBy = observer(function PostVotedBy({
|
export const PostLikedBy = observer(function PostVotedBy({uri}: {uri: string}) {
|
||||||
uri,
|
|
||||||
direction,
|
|
||||||
}: {
|
|
||||||
uri: string
|
|
||||||
direction: 'up' | 'down'
|
|
||||||
}) {
|
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const view = React.useMemo(
|
const view = React.useMemo(
|
||||||
() => new VotesViewModel(store, {uri, direction}),
|
() => new LikesViewModel(store, {uri}),
|
||||||
[store, uri, direction],
|
[store, uri],
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -55,11 +49,10 @@ export const PostVotedBy = observer(function PostVotedBy({
|
||||||
|
|
||||||
// loaded
|
// loaded
|
||||||
// =
|
// =
|
||||||
const renderItem = ({item}: {item: VoteItem}) => (
|
const renderItem = ({item}: {item: LikeItem}) => (
|
||||||
<ProfileCardWithFollowBtn
|
<ProfileCardWithFollowBtn
|
||||||
key={item.actor.did}
|
key={item.actor.did}
|
||||||
did={item.actor.did}
|
did={item.actor.did}
|
||||||
declarationCid={item.actor.declaration.cid}
|
|
||||||
handle={item.actor.handle}
|
handle={item.actor.handle}
|
||||||
displayName={item.actor.displayName}
|
displayName={item.actor.displayName}
|
||||||
avatar={item.actor.avatar}
|
avatar={item.actor.avatar}
|
||||||
|
@ -68,7 +61,7 @@ export const PostVotedBy = observer(function PostVotedBy({
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<FlatList
|
<FlatList
|
||||||
data={view.votes}
|
data={view.likes}
|
||||||
keyExtractor={item => item.actor.did}
|
keyExtractor={item => item.actor.did}
|
||||||
refreshControl={
|
refreshControl={
|
||||||
<RefreshControl
|
<RefreshControl
|
|
@ -64,7 +64,6 @@ export const PostRepostedBy = observer(function PostRepostedBy({
|
||||||
<ProfileCardWithFollowBtn
|
<ProfileCardWithFollowBtn
|
||||||
key={item.did}
|
key={item.did}
|
||||||
did={item.did}
|
did={item.did}
|
||||||
declarationCid={item.declaration.cid}
|
|
||||||
handle={item.handle}
|
handle={item.handle}
|
||||||
displayName={item.displayName}
|
displayName={item.displayName}
|
||||||
avatar={item.avatar}
|
avatar={item.avatar}
|
||||||
|
|
|
@ -1,17 +1,30 @@
|
||||||
import React, {useRef} from 'react'
|
import React, {useRef} from 'react'
|
||||||
import {observer} from 'mobx-react-lite'
|
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 {CenteredView, FlatList} from '../util/Views'
|
||||||
import {
|
import {
|
||||||
PostThreadViewModel,
|
PostThreadViewModel,
|
||||||
PostThreadViewPostModel,
|
PostThreadViewPostModel,
|
||||||
} from 'state/models/post-thread-view'
|
} from 'state/models/post-thread-view'
|
||||||
|
import {
|
||||||
|
FontAwesomeIcon,
|
||||||
|
FontAwesomeIconStyle,
|
||||||
|
} from '@fortawesome/react-native-fontawesome'
|
||||||
import {PostThreadItem} from './PostThreadItem'
|
import {PostThreadItem} from './PostThreadItem'
|
||||||
import {ComposePrompt} from '../composer/Prompt'
|
import {ComposePrompt} from '../composer/Prompt'
|
||||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||||
|
import {Text} from '../util/text/Text'
|
||||||
import {s} from 'lib/styles'
|
import {s} from 'lib/styles'
|
||||||
import {isDesktopWeb} from 'platform/detection'
|
import {isDesktopWeb} from 'platform/detection'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
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 REPLY_PROMPT = {_reactKey: '__reply__', _isHighlightedPost: false}
|
||||||
const BOTTOM_BORDER = {
|
const BOTTOM_BORDER = {
|
||||||
|
@ -32,6 +45,7 @@ export const PostThread = observer(function PostThread({
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const ref = useRef<FlatList>(null)
|
const ref = useRef<FlatList>(null)
|
||||||
const [isRefreshing, setIsRefreshing] = React.useState(false)
|
const [isRefreshing, setIsRefreshing] = React.useState(false)
|
||||||
|
const navigation = useNavigation<NavigationProp>()
|
||||||
const posts = React.useMemo(() => {
|
const posts = React.useMemo(() => {
|
||||||
if (view.thread) {
|
if (view.thread) {
|
||||||
return Array.from(flattenThread(view.thread)).concat([BOTTOM_BORDER])
|
return Array.from(flattenThread(view.thread)).concat([BOTTOM_BORDER])
|
||||||
|
@ -41,6 +55,7 @@ export const PostThread = observer(function PostThread({
|
||||||
|
|
||||||
// events
|
// events
|
||||||
// =
|
// =
|
||||||
|
|
||||||
const onRefresh = React.useCallback(async () => {
|
const onRefresh = React.useCallback(async () => {
|
||||||
setIsRefreshing(true)
|
setIsRefreshing(true)
|
||||||
try {
|
try {
|
||||||
|
@ -50,6 +65,7 @@ export const PostThread = observer(function PostThread({
|
||||||
}
|
}
|
||||||
setIsRefreshing(false)
|
setIsRefreshing(false)
|
||||||
}, [view, setIsRefreshing])
|
}, [view, setIsRefreshing])
|
||||||
|
|
||||||
const onLayout = React.useCallback(() => {
|
const onLayout = React.useCallback(() => {
|
||||||
const index = posts.findIndex(post => post._isHighlightedPost)
|
const index = posts.findIndex(post => post._isHighlightedPost)
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
|
@ -60,6 +76,7 @@ export const PostThread = observer(function PostThread({
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [posts, ref])
|
}, [posts, ref])
|
||||||
|
|
||||||
const onScrollToIndexFailed = React.useCallback(
|
const onScrollToIndexFailed = React.useCallback(
|
||||||
(info: {
|
(info: {
|
||||||
index: number
|
index: number
|
||||||
|
@ -73,6 +90,15 @@ export const PostThread = observer(function PostThread({
|
||||||
},
|
},
|
||||||
[ref],
|
[ref],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const onPressBack = React.useCallback(() => {
|
||||||
|
if (navigation.canGoBack()) {
|
||||||
|
navigation.goBack()
|
||||||
|
} else {
|
||||||
|
navigation.navigate('Home')
|
||||||
|
}
|
||||||
|
}, [navigation])
|
||||||
|
|
||||||
const renderItem = React.useCallback(
|
const renderItem = React.useCallback(
|
||||||
({item}: {item: YieldedItem}) => {
|
({item}: {item: YieldedItem}) => {
|
||||||
if (item === REPLY_PROMPT) {
|
if (item === REPLY_PROMPT) {
|
||||||
|
@ -104,6 +130,30 @@ export const PostThread = observer(function PostThread({
|
||||||
// error
|
// error
|
||||||
// =
|
// =
|
||||||
if (view.hasError) {
|
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 (
|
return (
|
||||||
<CenteredView>
|
<CenteredView>
|
||||||
<ErrorMessage message={view.error} onPressTryAgain={onRefresh} />
|
<ErrorMessage message={view.error} onPressTryAgain={onRefresh} />
|
||||||
|
@ -159,12 +209,18 @@ function* flattenThread(
|
||||||
yield* flattenThread(reply as PostThreadViewPostModel)
|
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
|
post._hasMore = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
notFoundContainer: {
|
||||||
|
margin: 10,
|
||||||
|
paddingHorizontal: 18,
|
||||||
|
paddingVertical: 14,
|
||||||
|
borderRadius: 6,
|
||||||
|
},
|
||||||
bottomBorder: {
|
bottomBorder: {
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
},
|
},
|
||||||
|
|
|
@ -19,7 +19,7 @@ import {ago} from 'lib/strings/time'
|
||||||
import {pluralize} from 'lib/strings/helpers'
|
import {pluralize} from 'lib/strings/helpers'
|
||||||
import {useStores} from 'state/index'
|
import {useStores} from 'state/index'
|
||||||
import {PostMeta} from '../util/PostMeta'
|
import {PostMeta} from '../util/PostMeta'
|
||||||
import {PostEmbeds} from '../util/PostEmbeds'
|
import {PostEmbeds} from '../util/post-embeds'
|
||||||
import {PostCtrls} from '../util/PostCtrls'
|
import {PostCtrls} from '../util/PostCtrls'
|
||||||
import {PostMutedWrapper} from '../util/PostMuted'
|
import {PostMutedWrapper} from '../util/PostMuted'
|
||||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||||
|
@ -38,7 +38,7 @@ export const PostThreadItem = observer(function PostThreadItem({
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const [deleted, setDeleted] = React.useState(false)
|
const [deleted, setDeleted] = React.useState(false)
|
||||||
const record = item.postRecord
|
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 itemUri = item.post.uri
|
||||||
const itemCid = item.post.cid
|
const itemCid = item.post.cid
|
||||||
|
@ -49,11 +49,11 @@ export const PostThreadItem = observer(function PostThreadItem({
|
||||||
const itemTitle = `Post by ${item.post.author.handle}`
|
const itemTitle = `Post by ${item.post.author.handle}`
|
||||||
const authorHref = `/profile/${item.post.author.handle}`
|
const authorHref = `/profile/${item.post.author.handle}`
|
||||||
const authorTitle = 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)
|
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])
|
}, [item.post.uri, item.post.author.handle])
|
||||||
const upvotesTitle = 'Likes on this post'
|
const likesTitle = 'Likes on this post'
|
||||||
const repostsHref = React.useMemo(() => {
|
const repostsHref = React.useMemo(() => {
|
||||||
const urip = new AtUri(item.post.uri)
|
const urip = new AtUri(item.post.uri)
|
||||||
return `/profile/${item.post.author.handle}/post/${urip.rkey}/reposted-by`
|
return `/profile/${item.post.author.handle}/post/${urip.rkey}/reposted-by`
|
||||||
|
@ -80,10 +80,10 @@ export const PostThreadItem = observer(function PostThreadItem({
|
||||||
.toggleRepost()
|
.toggleRepost()
|
||||||
.catch(e => store.log.error('Failed to toggle repost', e))
|
.catch(e => store.log.error('Failed to toggle repost', e))
|
||||||
}, [item, store])
|
}, [item, store])
|
||||||
const onPressToggleUpvote = React.useCallback(() => {
|
const onPressToggleLike = React.useCallback(() => {
|
||||||
return item
|
return item
|
||||||
.toggleUpvote()
|
.toggleLike()
|
||||||
.catch(e => store.log.error('Failed to toggle upvote', e))
|
.catch(e => store.log.error('Failed to toggle like', e))
|
||||||
}, [item, store])
|
}, [item, store])
|
||||||
const onCopyPostText = React.useCallback(() => {
|
const onCopyPostText = React.useCallback(() => {
|
||||||
Clipboard.setString(record?.text || '')
|
Clipboard.setString(record?.text || '')
|
||||||
|
@ -125,8 +125,8 @@ export const PostThreadItem = observer(function PostThreadItem({
|
||||||
|
|
||||||
if (item._isHighlightedPost) {
|
if (item._isHighlightedPost) {
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<View
|
<View
|
||||||
|
testID={`postThreadItem-by-${item.post.author.handle}`}
|
||||||
style={[
|
style={[
|
||||||
styles.outer,
|
styles.outer,
|
||||||
styles.outerHighlighted,
|
styles.outerHighlighted,
|
||||||
|
@ -160,6 +160,7 @@ export const PostThreadItem = observer(function PostThreadItem({
|
||||||
</View>
|
</View>
|
||||||
<View style={s.flex1} />
|
<View style={s.flex1} />
|
||||||
<PostDropdownBtn
|
<PostDropdownBtn
|
||||||
|
testID="postDropdownBtn"
|
||||||
style={styles.metaItem}
|
style={styles.metaItem}
|
||||||
itemUri={itemUri}
|
itemUri={itemUri}
|
||||||
itemCid={itemCid}
|
itemCid={itemCid}
|
||||||
|
@ -191,10 +192,7 @@ export const PostThreadItem = observer(function PostThreadItem({
|
||||||
<View style={[s.pl10, s.pr10, s.pb10]}>
|
<View style={[s.pl10, s.pr10, s.pb10]}>
|
||||||
{item.richText?.text ? (
|
{item.richText?.text ? (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[styles.postTextContainer, styles.postTextLargeContainer]}>
|
||||||
styles.postTextContainer,
|
|
||||||
styles.postTextLargeContainer,
|
|
||||||
]}>
|
|
||||||
<RichText
|
<RichText
|
||||||
type="post-text-lg"
|
type="post-text-lg"
|
||||||
richText={item.richText}
|
richText={item.richText}
|
||||||
|
@ -210,7 +208,7 @@ export const PostThreadItem = observer(function PostThreadItem({
|
||||||
style={styles.expandedInfoItem}
|
style={styles.expandedInfoItem}
|
||||||
href={repostsHref}
|
href={repostsHref}
|
||||||
title={repostsTitle}>
|
title={repostsTitle}>
|
||||||
<Text type="lg" style={pal.textLight}>
|
<Text testID="repostCount" type="lg" style={pal.textLight}>
|
||||||
<Text type="xl-bold" style={pal.text}>
|
<Text type="xl-bold" style={pal.text}>
|
||||||
{item.post.repostCount}
|
{item.post.repostCount}
|
||||||
</Text>{' '}
|
</Text>{' '}
|
||||||
|
@ -220,16 +218,16 @@ export const PostThreadItem = observer(function PostThreadItem({
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
{item.post.upvoteCount ? (
|
{item.post.likeCount ? (
|
||||||
<Link
|
<Link
|
||||||
style={styles.expandedInfoItem}
|
style={styles.expandedInfoItem}
|
||||||
href={upvotesHref}
|
href={likesHref}
|
||||||
title={upvotesTitle}>
|
title={likesTitle}>
|
||||||
<Text type="lg" style={pal.textLight}>
|
<Text testID="likeCount" type="lg" style={pal.textLight}>
|
||||||
<Text type="xl-bold" style={pal.text}>
|
<Text type="xl-bold" style={pal.text}>
|
||||||
{item.post.upvoteCount}
|
{item.post.likeCount}
|
||||||
</Text>{' '}
|
</Text>{' '}
|
||||||
{pluralize(item.post.upvoteCount, 'like')}
|
{pluralize(item.post.likeCount, 'like')}
|
||||||
</Text>
|
</Text>
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
|
@ -254,11 +252,11 @@ export const PostThreadItem = observer(function PostThreadItem({
|
||||||
text={item.richText?.text || record.text}
|
text={item.richText?.text || record.text}
|
||||||
indexedAt={item.post.indexedAt}
|
indexedAt={item.post.indexedAt}
|
||||||
isAuthor={item.post.author.did === store.me.did}
|
isAuthor={item.post.author.did === store.me.did}
|
||||||
isReposted={!!item.post.viewer.repost}
|
isReposted={!!item.post.viewer?.repost}
|
||||||
isUpvoted={!!item.post.viewer.upvote}
|
isLiked={!!item.post.viewer?.like}
|
||||||
onPressReply={onPressReply}
|
onPressReply={onPressReply}
|
||||||
onPressToggleRepost={onPressToggleRepost}
|
onPressToggleRepost={onPressToggleRepost}
|
||||||
onPressToggleUpvote={onPressToggleUpvote}
|
onPressToggleLike={onPressToggleLike}
|
||||||
onCopyPostText={onCopyPostText}
|
onCopyPostText={onCopyPostText}
|
||||||
onOpenTranslate={onOpenTranslate}
|
onOpenTranslate={onOpenTranslate}
|
||||||
onDeletePost={onDeletePost}
|
onDeletePost={onDeletePost}
|
||||||
|
@ -266,12 +264,12 @@ export const PostThreadItem = observer(function PostThreadItem({
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<PostMutedWrapper isMuted={item.post.author.viewer?.muted === true}>
|
<PostMutedWrapper isMuted={item.post.author.viewer?.muted === true}>
|
||||||
<Link
|
<Link
|
||||||
|
testID={`postThreadItem-by-${item.post.author.handle}`}
|
||||||
style={[styles.outer, {borderTopColor: pal.colors.border}, pal.view]}
|
style={[styles.outer, {borderTopColor: pal.colors.border}, pal.view]}
|
||||||
href={itemHref}
|
href={itemHref}
|
||||||
title={itemTitle}
|
title={itemTitle}
|
||||||
|
@ -305,7 +303,6 @@ export const PostThreadItem = observer(function PostThreadItem({
|
||||||
timestamp={item.post.indexedAt}
|
timestamp={item.post.indexedAt}
|
||||||
postHref={itemHref}
|
postHref={itemHref}
|
||||||
did={item.post.author.did}
|
did={item.post.author.did}
|
||||||
declarationCid={item.post.author.declaration.cid}
|
|
||||||
/>
|
/>
|
||||||
{item.richText?.text ? (
|
{item.richText?.text ? (
|
||||||
<View style={styles.postTextContainer}>
|
<View style={styles.postTextContainer}>
|
||||||
|
@ -333,12 +330,12 @@ export const PostThreadItem = observer(function PostThreadItem({
|
||||||
isAuthor={item.post.author.did === store.me.did}
|
isAuthor={item.post.author.did === store.me.did}
|
||||||
replyCount={item.post.replyCount}
|
replyCount={item.post.replyCount}
|
||||||
repostCount={item.post.repostCount}
|
repostCount={item.post.repostCount}
|
||||||
upvoteCount={item.post.upvoteCount}
|
likeCount={item.post.likeCount}
|
||||||
isReposted={!!item.post.viewer.repost}
|
isReposted={!!item.post.viewer?.repost}
|
||||||
isUpvoted={!!item.post.viewer.upvote}
|
isLiked={!!item.post.viewer?.like}
|
||||||
onPressReply={onPressReply}
|
onPressReply={onPressReply}
|
||||||
onPressToggleRepost={onPressToggleRepost}
|
onPressToggleRepost={onPressToggleRepost}
|
||||||
onPressToggleUpvote={onPressToggleUpvote}
|
onPressToggleLike={onPressToggleLike}
|
||||||
onCopyPostText={onCopyPostText}
|
onCopyPostText={onCopyPostText}
|
||||||
onOpenTranslate={onOpenTranslate}
|
onOpenTranslate={onOpenTranslate}
|
||||||
onDeletePost={onDeletePost}
|
onDeletePost={onDeletePost}
|
||||||
|
|
|
@ -15,7 +15,7 @@ import {PostThreadViewModel} from 'state/models/post-thread-view'
|
||||||
import {Link} from '../util/Link'
|
import {Link} from '../util/Link'
|
||||||
import {UserInfoText} from '../util/UserInfoText'
|
import {UserInfoText} from '../util/UserInfoText'
|
||||||
import {PostMeta} from '../util/PostMeta'
|
import {PostMeta} from '../util/PostMeta'
|
||||||
import {PostEmbeds} from '../util/PostEmbeds'
|
import {PostEmbeds} from '../util/post-embeds'
|
||||||
import {PostCtrls} from '../util/PostCtrls'
|
import {PostCtrls} from '../util/PostCtrls'
|
||||||
import {PostMutedWrapper} from '../util/PostMuted'
|
import {PostMutedWrapper} from '../util/PostMuted'
|
||||||
import {Text} from '../util/text/Text'
|
import {Text} from '../util/text/Text'
|
||||||
|
@ -118,10 +118,10 @@ export const Post = observer(function Post({
|
||||||
.toggleRepost()
|
.toggleRepost()
|
||||||
.catch(e => store.log.error('Failed to toggle repost', e))
|
.catch(e => store.log.error('Failed to toggle repost', e))
|
||||||
}
|
}
|
||||||
const onPressToggleUpvote = () => {
|
const onPressToggleLike = () => {
|
||||||
return item
|
return item
|
||||||
.toggleUpvote()
|
.toggleLike()
|
||||||
.catch(e => store.log.error('Failed to toggle upvote', e))
|
.catch(e => store.log.error('Failed to toggle like', e))
|
||||||
}
|
}
|
||||||
const onCopyPostText = () => {
|
const onCopyPostText = () => {
|
||||||
Clipboard.setString(record.text)
|
Clipboard.setString(record.text)
|
||||||
|
@ -166,7 +166,6 @@ export const Post = observer(function Post({
|
||||||
timestamp={item.post.indexedAt}
|
timestamp={item.post.indexedAt}
|
||||||
postHref={itemHref}
|
postHref={itemHref}
|
||||||
did={item.post.author.did}
|
did={item.post.author.did}
|
||||||
declarationCid={item.post.author.declaration.cid}
|
|
||||||
/>
|
/>
|
||||||
{replyAuthorDid !== '' && (
|
{replyAuthorDid !== '' && (
|
||||||
<View style={[s.flexRow, s.mb2, s.alignCenter]}>
|
<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}
|
isAuthor={item.post.author.did === store.me.did}
|
||||||
replyCount={item.post.replyCount}
|
replyCount={item.post.replyCount}
|
||||||
repostCount={item.post.repostCount}
|
repostCount={item.post.repostCount}
|
||||||
upvoteCount={item.post.upvoteCount}
|
likeCount={item.post.likeCount}
|
||||||
isReposted={!!item.post.viewer.repost}
|
isReposted={!!item.post.viewer?.repost}
|
||||||
isUpvoted={!!item.post.viewer.upvote}
|
isLiked={!!item.post.viewer?.like}
|
||||||
onPressReply={onPressReply}
|
onPressReply={onPressReply}
|
||||||
onPressToggleRepost={onPressToggleRepost}
|
onPressToggleRepost={onPressToggleRepost}
|
||||||
onPressToggleUpvote={onPressToggleUpvote}
|
onPressToggleLike={onPressToggleLike}
|
||||||
onCopyPostText={onCopyPostText}
|
onCopyPostText={onCopyPostText}
|
||||||
onOpenTranslate={onOpenTranslate}
|
onOpenTranslate={onOpenTranslate}
|
||||||
onDeletePost={onDeletePost}
|
onDeletePost={onDeletePost}
|
||||||
|
|
|
@ -128,6 +128,7 @@ export const Feed = observer(function Feed({
|
||||||
<View testID={testID} style={style}>
|
<View testID={testID} style={style}>
|
||||||
{data.length > 0 && (
|
{data.length > 0 && (
|
||||||
<FlatList
|
<FlatList
|
||||||
|
testID={testID ? `${testID}-flatlist` : undefined}
|
||||||
ref={scrollElRef}
|
ref={scrollElRef}
|
||||||
data={data}
|
data={data}
|
||||||
keyExtractor={item => item._reactKey}
|
keyExtractor={item => item._reactKey}
|
||||||
|
|
|
@ -13,7 +13,7 @@ import {Text} from '../util/text/Text'
|
||||||
import {UserInfoText} from '../util/UserInfoText'
|
import {UserInfoText} from '../util/UserInfoText'
|
||||||
import {PostMeta} from '../util/PostMeta'
|
import {PostMeta} from '../util/PostMeta'
|
||||||
import {PostCtrls} from '../util/PostCtrls'
|
import {PostCtrls} from '../util/PostCtrls'
|
||||||
import {PostEmbeds} from '../util/PostEmbeds'
|
import {PostEmbeds} from '../util/post-embeds'
|
||||||
import {PostMutedWrapper} from '../util/PostMuted'
|
import {PostMutedWrapper} from '../util/PostMuted'
|
||||||
import {RichText} from '../util/text/RichText'
|
import {RichText} from '../util/text/RichText'
|
||||||
import * as Toast from '../util/Toast'
|
import * as Toast from '../util/Toast'
|
||||||
|
@ -79,11 +79,11 @@ export const FeedItem = observer(function ({
|
||||||
.toggleRepost()
|
.toggleRepost()
|
||||||
.catch(e => store.log.error('Failed to toggle repost', e))
|
.catch(e => store.log.error('Failed to toggle repost', e))
|
||||||
}
|
}
|
||||||
const onPressToggleUpvote = () => {
|
const onPressToggleLike = () => {
|
||||||
track('FeedItem:PostLike')
|
track('FeedItem:PostLike')
|
||||||
return item
|
return item
|
||||||
.toggleUpvote()
|
.toggleLike()
|
||||||
.catch(e => store.log.error('Failed to toggle upvote', e))
|
.catch(e => store.log.error('Failed to toggle like', e))
|
||||||
}
|
}
|
||||||
const onCopyPostText = () => {
|
const onCopyPostText = () => {
|
||||||
Clipboard.setString(record?.text || '')
|
Clipboard.setString(record?.text || '')
|
||||||
|
@ -127,7 +127,12 @@ export const FeedItem = observer(function ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PostMutedWrapper isMuted={isMuted}>
|
<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 && (
|
{isThreadChild && (
|
||||||
<View
|
<View
|
||||||
style={[styles.topReplyLine, {borderColor: pal.colors.replyLine}]}
|
style={[styles.topReplyLine, {borderColor: pal.colors.replyLine}]}
|
||||||
|
@ -189,7 +194,6 @@ export const FeedItem = observer(function ({
|
||||||
timestamp={item.post.indexedAt}
|
timestamp={item.post.indexedAt}
|
||||||
postHref={itemHref}
|
postHref={itemHref}
|
||||||
did={item.post.author.did}
|
did={item.post.author.did}
|
||||||
declarationCid={item.post.author.declaration.cid}
|
|
||||||
showFollowBtn={showFollowBtn}
|
showFollowBtn={showFollowBtn}
|
||||||
/>
|
/>
|
||||||
{!isThreadChild && replyAuthorDid !== '' && (
|
{!isThreadChild && replyAuthorDid !== '' && (
|
||||||
|
@ -239,12 +243,12 @@ export const FeedItem = observer(function ({
|
||||||
isAuthor={item.post.author.did === store.me.did}
|
isAuthor={item.post.author.did === store.me.did}
|
||||||
replyCount={item.post.replyCount}
|
replyCount={item.post.replyCount}
|
||||||
repostCount={item.post.repostCount}
|
repostCount={item.post.repostCount}
|
||||||
upvoteCount={item.post.upvoteCount}
|
likeCount={item.post.likeCount}
|
||||||
isReposted={!!item.post.viewer.repost}
|
isReposted={!!item.post.viewer?.repost}
|
||||||
isUpvoted={!!item.post.viewer.upvote}
|
isLiked={!!item.post.viewer?.like}
|
||||||
onPressReply={onPressReply}
|
onPressReply={onPressReply}
|
||||||
onPressToggleRepost={onPressToggleRepost}
|
onPressToggleRepost={onPressToggleRepost}
|
||||||
onPressToggleUpvote={onPressToggleUpvote}
|
onPressToggleLike={onPressToggleLike}
|
||||||
onCopyPostText={onCopyPostText}
|
onCopyPostText={onCopyPostText}
|
||||||
onOpenTranslate={onOpenTranslate}
|
onOpenTranslate={onOpenTranslate}
|
||||||
onDeletePost={onDeletePost}
|
onDeletePost={onDeletePost}
|
||||||
|
|
|
@ -2,19 +2,16 @@ import React from 'react'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {Button, ButtonType} from '../util/forms/Button'
|
import {Button, ButtonType} from '../util/forms/Button'
|
||||||
import {useStores} from 'state/index'
|
import {useStores} from 'state/index'
|
||||||
import * as apilib from 'lib/api/index'
|
|
||||||
import * as Toast from '../util/Toast'
|
import * as Toast from '../util/Toast'
|
||||||
|
|
||||||
const FollowButton = observer(
|
const FollowButton = observer(
|
||||||
({
|
({
|
||||||
type = 'inverted',
|
type = 'inverted',
|
||||||
did,
|
did,
|
||||||
declarationCid,
|
|
||||||
onToggleFollow,
|
onToggleFollow,
|
||||||
}: {
|
}: {
|
||||||
type?: ButtonType
|
type?: ButtonType
|
||||||
did: string
|
did: string
|
||||||
declarationCid: string
|
|
||||||
onToggleFollow?: (v: boolean) => void
|
onToggleFollow?: (v: boolean) => void
|
||||||
}) => {
|
}) => {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
|
@ -23,7 +20,7 @@ const FollowButton = observer(
|
||||||
const onToggleFollowInner = async () => {
|
const onToggleFollowInner = async () => {
|
||||||
if (store.me.follows.isFollowing(did)) {
|
if (store.me.follows.isFollowing(did)) {
|
||||||
try {
|
try {
|
||||||
await apilib.unfollow(store, store.me.follows.getFollowUri(did))
|
await store.agent.deleteFollow(store.me.follows.getFollowUri(did))
|
||||||
store.me.follows.removeFollow(did)
|
store.me.follows.removeFollow(did)
|
||||||
onToggleFollow?.(false)
|
onToggleFollow?.(false)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
@ -32,7 +29,7 @@ const FollowButton = observer(
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
const res = await apilib.follow(store, did, declarationCid)
|
const res = await store.agent.follow(did)
|
||||||
store.me.follows.addFollow(did, res.uri)
|
store.me.follows.addFollow(did, res.uri)
|
||||||
onToggleFollow?.(true)
|
onToggleFollow?.(true)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {StyleSheet, View} from 'react-native'
|
import {StyleSheet, View} from 'react-native'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {AppBskyActorProfile} from '@atproto/api'
|
import {AppBskyActorDefs} from '@atproto/api'
|
||||||
import {Link} from '../util/Link'
|
import {Link} from '../util/Link'
|
||||||
import {Text} from '../util/text/Text'
|
import {Text} from '../util/text/Text'
|
||||||
import {UserAvatar} from '../util/UserAvatar'
|
import {UserAvatar} from '../util/UserAvatar'
|
||||||
|
@ -11,6 +11,7 @@ import {useStores} from 'state/index'
|
||||||
import FollowButton from './FollowButton'
|
import FollowButton from './FollowButton'
|
||||||
|
|
||||||
export function ProfileCard({
|
export function ProfileCard({
|
||||||
|
testID,
|
||||||
handle,
|
handle,
|
||||||
displayName,
|
displayName,
|
||||||
avatar,
|
avatar,
|
||||||
|
@ -21,6 +22,7 @@ export function ProfileCard({
|
||||||
followers,
|
followers,
|
||||||
renderButton,
|
renderButton,
|
||||||
}: {
|
}: {
|
||||||
|
testID?: string
|
||||||
handle: string
|
handle: string
|
||||||
displayName?: string
|
displayName?: string
|
||||||
avatar?: string
|
avatar?: string
|
||||||
|
@ -28,12 +30,13 @@ export function ProfileCard({
|
||||||
isFollowedBy?: boolean
|
isFollowedBy?: boolean
|
||||||
noBg?: boolean
|
noBg?: boolean
|
||||||
noBorder?: boolean
|
noBorder?: boolean
|
||||||
followers?: AppBskyActorProfile.View[] | undefined
|
followers?: AppBskyActorDefs.ProfileView[] | undefined
|
||||||
renderButton?: () => JSX.Element
|
renderButton?: () => JSX.Element
|
||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
|
testID={testID}
|
||||||
style={[
|
style={[
|
||||||
styles.outer,
|
styles.outer,
|
||||||
pal.border,
|
pal.border,
|
||||||
|
@ -106,7 +109,6 @@ export function ProfileCard({
|
||||||
export const ProfileCardWithFollowBtn = observer(
|
export const ProfileCardWithFollowBtn = observer(
|
||||||
({
|
({
|
||||||
did,
|
did,
|
||||||
declarationCid,
|
|
||||||
handle,
|
handle,
|
||||||
displayName,
|
displayName,
|
||||||
avatar,
|
avatar,
|
||||||
|
@ -117,7 +119,6 @@ export const ProfileCardWithFollowBtn = observer(
|
||||||
followers,
|
followers,
|
||||||
}: {
|
}: {
|
||||||
did: string
|
did: string
|
||||||
declarationCid: string
|
|
||||||
handle: string
|
handle: string
|
||||||
displayName?: string
|
displayName?: string
|
||||||
avatar?: string
|
avatar?: string
|
||||||
|
@ -125,7 +126,7 @@ export const ProfileCardWithFollowBtn = observer(
|
||||||
isFollowedBy?: boolean
|
isFollowedBy?: boolean
|
||||||
noBg?: boolean
|
noBg?: boolean
|
||||||
noBorder?: boolean
|
noBorder?: boolean
|
||||||
followers?: AppBskyActorProfile.View[] | undefined
|
followers?: AppBskyActorDefs.ProfileView[] | undefined
|
||||||
}) => {
|
}) => {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const isMe = store.me.handle === handle
|
const isMe = store.me.handle === handle
|
||||||
|
@ -140,11 +141,7 @@ export const ProfileCardWithFollowBtn = observer(
|
||||||
noBg={noBg}
|
noBg={noBg}
|
||||||
noBorder={noBorder}
|
noBorder={noBorder}
|
||||||
followers={followers}
|
followers={followers}
|
||||||
renderButton={
|
renderButton={isMe ? undefined : () => <FollowButton did={did} />}
|
||||||
isMe
|
|
||||||
? undefined
|
|
||||||
: () => <FollowButton did={did} declarationCid={declarationCid} />
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
@ -19,7 +19,7 @@ export const ProfileFollowers = observer(function ProfileFollowers({
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const view = React.useMemo(
|
const view = React.useMemo(
|
||||||
() => new UserFollowersViewModel(store, {user: name}),
|
() => new UserFollowersViewModel(store, {actor: name}),
|
||||||
[store, name],
|
[store, name],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -64,7 +64,6 @@ export const ProfileFollowers = observer(function ProfileFollowers({
|
||||||
<ProfileCardWithFollowBtn
|
<ProfileCardWithFollowBtn
|
||||||
key={item.did}
|
key={item.did}
|
||||||
did={item.did}
|
did={item.did}
|
||||||
declarationCid={item.declaration.cid}
|
|
||||||
handle={item.handle}
|
handle={item.handle}
|
||||||
displayName={item.displayName}
|
displayName={item.displayName}
|
||||||
avatar={item.avatar}
|
avatar={item.avatar}
|
||||||
|
|
|
@ -16,7 +16,7 @@ export const ProfileFollows = observer(function ProfileFollows({
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const view = React.useMemo(
|
const view = React.useMemo(
|
||||||
() => new UserFollowsViewModel(store, {user: name}),
|
() => new UserFollowsViewModel(store, {actor: name}),
|
||||||
[store, name],
|
[store, name],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -61,7 +61,6 @@ export const ProfileFollows = observer(function ProfileFollows({
|
||||||
<ProfileCardWithFollowBtn
|
<ProfileCardWithFollowBtn
|
||||||
key={item.did}
|
key={item.did}
|
||||||
did={item.did}
|
did={item.did}
|
||||||
declarationCid={item.declaration.cid}
|
|
||||||
handle={item.handle}
|
handle={item.handle}
|
||||||
displayName={item.displayName}
|
displayName={item.displayName}
|
||||||
avatar={item.avatar}
|
avatar={item.avatar}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue