Prefilter the mergefeed to ensure a better mix of following and custom feeds (#1498)
* Prefilter the mergefeed to ensure a better mix of following and custom feeds * Test suite improvements & tests for the mergefeed (#1499) * Disable invite codes test for now * Update test sim to latest iphone * Introduce TestCtrls driver * Add mergefeed testszio/stable
parent
68dd3210d1
commit
5a945c2024
|
@ -41,7 +41,7 @@ module.exports = {
|
||||||
simulator: {
|
simulator: {
|
||||||
type: 'ios.simulator',
|
type: 'ios.simulator',
|
||||||
device: {
|
device: {
|
||||||
type: 'iPhone 14',
|
type: 'iPhone 15',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
attached: {
|
attached: {
|
||||||
|
|
|
@ -55,7 +55,7 @@ async function main() {
|
||||||
}
|
}
|
||||||
if ('feeds' in url.query) {
|
if ('feeds' in url.query) {
|
||||||
console.log('Generating mock feed')
|
console.log('Generating mock feed')
|
||||||
await server.mocker.createFeed('alice')
|
await server.mocker.createFeed('alice', 'alice-favs', [])
|
||||||
}
|
}
|
||||||
if ('thread' in url.query) {
|
if ('thread' in url.query) {
|
||||||
console.log('Generating mock posts')
|
console.log('Generating mock posts')
|
||||||
|
@ -70,6 +70,82 @@ async function main() {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if ('mergefeed' 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.createUser('dan')
|
||||||
|
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',
|
||||||
|
}))
|
||||||
|
await server.mocker.users.dan.agent.upsertProfile(() => ({
|
||||||
|
displayName: 'Dan',
|
||||||
|
description: 'Test user 4',
|
||||||
|
}))
|
||||||
|
console.log('Generating mock follows')
|
||||||
|
await server.mocker.follow('alice', 'bob')
|
||||||
|
await server.mocker.follow('alice', 'carla')
|
||||||
|
console.log('Generating mock posts')
|
||||||
|
let posts: Record<string, any[]> = {
|
||||||
|
alice: [],
|
||||||
|
bob: [],
|
||||||
|
carla: [],
|
||||||
|
dan: [],
|
||||||
|
}
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
for (let user in server.mocker.users) {
|
||||||
|
if (user === 'alice') continue
|
||||||
|
posts[user].push(
|
||||||
|
await server.mocker.createPost(user, `Post ${i}`),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
for (let user in server.mocker.users) {
|
||||||
|
if (user === 'alice') continue
|
||||||
|
if (i % 5 === 0) {
|
||||||
|
await server.mocker.createReply(user, 'Self reply', {
|
||||||
|
cid: posts[user][i].cid,
|
||||||
|
uri: posts[user][i].uri,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (i % 5 === 1) {
|
||||||
|
await server.mocker.createReply(user, 'Reply to bob', {
|
||||||
|
cid: posts.bob[i].cid,
|
||||||
|
uri: posts.bob[i].uri,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (i % 5 === 2) {
|
||||||
|
await server.mocker.createReply(user, 'Reply to dan', {
|
||||||
|
cid: posts.dan[i].cid,
|
||||||
|
uri: posts.dan[i].uri,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await server.mocker.users[user].agent.post({text: `Post ${i}`})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('Generating mock feeds')
|
||||||
|
await server.mocker.createFeed(
|
||||||
|
'alice',
|
||||||
|
'alice-favs',
|
||||||
|
posts.dan.map(p => p.uri),
|
||||||
|
)
|
||||||
|
await server.mocker.createFeed(
|
||||||
|
'alice',
|
||||||
|
'alice-favs2',
|
||||||
|
posts.dan.map(p => p.uri),
|
||||||
|
)
|
||||||
|
}
|
||||||
if ('labels' in url.query) {
|
if ('labels' in url.query) {
|
||||||
console.log('Generating naughty users with labels')
|
console.log('Generating naughty users with labels')
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,17 @@
|
||||||
/* eslint-env detox/detox */
|
/* eslint-env detox/detox */
|
||||||
|
|
||||||
import {openApp, login, createServer, sleep} from '../util'
|
import {openApp, loginAsAlice, createServer, sleep} from '../util'
|
||||||
|
|
||||||
describe('Composer', () => {
|
describe('Composer', () => {
|
||||||
let service: string
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
service = await createServer('?users')
|
await createServer('?users')
|
||||||
await openApp({
|
await openApp({
|
||||||
permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
|
permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Login', async () => {
|
it('Login', async () => {
|
||||||
await login(service, 'alice', 'hunter2')
|
await loginAsAlice()
|
||||||
await element(by.id('homeScreenFeedTabs-Following')).tap()
|
await element(by.id('homeScreenFeedTabs-Following')).tap()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
/* eslint-env detox/detox */
|
/* eslint-env detox/detox */
|
||||||
|
|
||||||
import {openApp, login, createServer} from '../util'
|
import {openApp, loginAsAlice, createServer} from '../util'
|
||||||
|
|
||||||
describe('Home screen', () => {
|
describe('Home screen', () => {
|
||||||
let service: string
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
service = await createServer('?users&follows&posts')
|
await createServer('?users&follows&posts')
|
||||||
await openApp({permissions: {notifications: 'YES'}})
|
await openApp({permissions: {notifications: 'YES'}})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Login', async () => {
|
it('Login', async () => {
|
||||||
await login(service, 'alice', 'hunter2')
|
await loginAsAlice()
|
||||||
await element(by.id('homeScreenFeedTabs-Following')).tap()
|
await element(by.id('homeScreenFeedTabs-Following')).tap()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
/* eslint-env detox/detox */
|
/* eslint-env detox/detox */
|
||||||
|
|
||||||
import {openApp, login, createServer} from '../util'
|
/**
|
||||||
|
* This test is being skipped until we can resolve the detox crash issue
|
||||||
|
* with the side drawer.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {openApp, loginAsAlice, createServer} from '../util'
|
||||||
|
|
||||||
describe('invite-codes', () => {
|
describe('invite-codes', () => {
|
||||||
let service: string
|
let service: string
|
||||||
|
@ -12,7 +17,7 @@ describe('invite-codes', () => {
|
||||||
|
|
||||||
it('I can fetch invite codes', async () => {
|
it('I can fetch invite codes', async () => {
|
||||||
await expect(element(by.id('signInButton'))).toBeVisible()
|
await expect(element(by.id('signInButton'))).toBeVisible()
|
||||||
await login(service, 'alice', 'hunter2')
|
await loginAsAlice()
|
||||||
await element(by.id('viewHeaderDrawerBtn')).tap()
|
await element(by.id('viewHeaderDrawerBtn')).tap()
|
||||||
await expect(element(by.id('drawer'))).toBeVisible()
|
await expect(element(by.id('drawer'))).toBeVisible()
|
||||||
await element(by.id('menuItemInviteCodes')).tap()
|
await element(by.id('menuItemInviteCodes')).tap()
|
||||||
|
@ -47,15 +52,10 @@ describe('invite-codes', () => {
|
||||||
await expect(element(by.id('recommendedFeedsOnboarding'))).toBeVisible()
|
await expect(element(by.id('recommendedFeedsOnboarding'))).toBeVisible()
|
||||||
await element(by.id('continueBtn')).tap()
|
await element(by.id('continueBtn')).tap()
|
||||||
await expect(element(by.id('homeScreen'))).toBeVisible()
|
await expect(element(by.id('homeScreen'))).toBeVisible()
|
||||||
await element(by.id('viewHeaderDrawerBtn')).tap()
|
|
||||||
await element(by.id('menuItemButton-Settings')).tap()
|
|
||||||
await element(by.id('signOutBtn')).tap()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('I get a notification for the new user', async () => {
|
it('I get a notification for the new user', async () => {
|
||||||
await expect(element(by.id('signInButton'))).toBeVisible()
|
await loginAsAlice()
|
||||||
await login(service, 'alice', 'hunter2')
|
|
||||||
await element(by.id('viewHeaderDrawerBtn')).tap()
|
|
||||||
await element(by.id('menuItemButton-Notifications')).tap()
|
await element(by.id('menuItemButton-Notifications')).tap()
|
||||||
await expect(element(by.id('invitedUser'))).toBeVisible()
|
await expect(element(by.id('invitedUser'))).toBeVisible()
|
||||||
})
|
})
|
|
@ -0,0 +1,157 @@
|
||||||
|
/* eslint-env detox/detox */
|
||||||
|
|
||||||
|
import {openApp, loginAsAlice, createServer} from '../util'
|
||||||
|
|
||||||
|
describe('Mergefeed', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await createServer('?mergefeed')
|
||||||
|
await openApp({permissions: {notifications: 'YES'}})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Login', async () => {
|
||||||
|
await loginAsAlice()
|
||||||
|
await element(by.id('e2eToggleMergefeed')).tap()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Sees the expected mix of posts with default filters', async () => {
|
||||||
|
await element(by.id('followingFeedPage-feed-flatlist')).swipe(
|
||||||
|
'down',
|
||||||
|
'slow',
|
||||||
|
1,
|
||||||
|
0.5,
|
||||||
|
0.5,
|
||||||
|
)
|
||||||
|
// followed users
|
||||||
|
await expect(
|
||||||
|
element(
|
||||||
|
by.id('postText').withAncestor(by.id('feedItem-by-carla.test')),
|
||||||
|
).atIndex(0),
|
||||||
|
).toHaveText('Post 9')
|
||||||
|
await expect(
|
||||||
|
element(
|
||||||
|
by.id('postText').withAncestor(by.id('feedItem-by-bob.test')),
|
||||||
|
).atIndex(0),
|
||||||
|
).toHaveText('Post 9')
|
||||||
|
await element(by.id('followingFeedPage-feed-flatlist')).swipe(
|
||||||
|
'up',
|
||||||
|
'fast',
|
||||||
|
1,
|
||||||
|
0.5,
|
||||||
|
0.5,
|
||||||
|
)
|
||||||
|
// feed users
|
||||||
|
await expect(
|
||||||
|
element(
|
||||||
|
by.id('postText').withAncestor(by.id('feedItem-by-dan.test')),
|
||||||
|
).atIndex(0),
|
||||||
|
).toHaveText('Post 0')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Sees the expected mix of posts with replies disabled', async () => {
|
||||||
|
await element(by.id('followingFeedPage-feed-flatlist')).swipe(
|
||||||
|
'down',
|
||||||
|
'fast',
|
||||||
|
1,
|
||||||
|
0.5,
|
||||||
|
0.5,
|
||||||
|
)
|
||||||
|
await element(by.id('followingFeedPage-feed-flatlist')).swipe(
|
||||||
|
'down',
|
||||||
|
'fast',
|
||||||
|
1,
|
||||||
|
0.5,
|
||||||
|
0.5,
|
||||||
|
)
|
||||||
|
await element(by.id('viewHeaderHomeFeedPrefsBtn')).tap()
|
||||||
|
await element(by.id('toggleRepliesBtn')).tap()
|
||||||
|
await element(by.id('confirmBtn')).tap()
|
||||||
|
await element(by.id('followingFeedPage-feed-flatlist')).swipe(
|
||||||
|
'down',
|
||||||
|
'slow',
|
||||||
|
1,
|
||||||
|
0.5,
|
||||||
|
0.5,
|
||||||
|
)
|
||||||
|
|
||||||
|
// followed users
|
||||||
|
await expect(
|
||||||
|
element(
|
||||||
|
by.id('postText').withAncestor(by.id('feedItem-by-carla.test')),
|
||||||
|
).atIndex(0),
|
||||||
|
).toHaveText('Post 9')
|
||||||
|
await expect(
|
||||||
|
element(
|
||||||
|
by.id('postText').withAncestor(by.id('feedItem-by-bob.test')),
|
||||||
|
).atIndex(0),
|
||||||
|
).toHaveText('Post 9')
|
||||||
|
await element(by.id('followingFeedPage-feed-flatlist')).swipe(
|
||||||
|
'up',
|
||||||
|
'fast',
|
||||||
|
1,
|
||||||
|
0.5,
|
||||||
|
0.5,
|
||||||
|
)
|
||||||
|
|
||||||
|
// feed users
|
||||||
|
await expect(
|
||||||
|
element(
|
||||||
|
by.id('postText').withAncestor(by.id('feedItem-by-dan.test')),
|
||||||
|
).atIndex(0),
|
||||||
|
).toHaveText('Post 0')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Sees the expected mix of posts with no follows', async () => {
|
||||||
|
await element(by.id('followingFeedPage-feed-flatlist')).swipe(
|
||||||
|
'down',
|
||||||
|
'fast',
|
||||||
|
1,
|
||||||
|
0.5,
|
||||||
|
0.5,
|
||||||
|
)
|
||||||
|
|
||||||
|
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()
|
||||||
|
await element(by.id('unfollowBtn')).tap()
|
||||||
|
await element(by.id('profileHeaderBackBtn')).tap()
|
||||||
|
|
||||||
|
// have to wait for the toast to clear
|
||||||
|
await waitFor(element(by.id('searchTextInputClearBtn')))
|
||||||
|
.toBeVisible()
|
||||||
|
.withTimeout(5000)
|
||||||
|
await element(by.id('searchTextInputClearBtn')).tap()
|
||||||
|
await element(by.id('searchTextInput')).typeText('carla')
|
||||||
|
await element(by.id('searchAutoCompleteResult-carla.test')).tap()
|
||||||
|
await expect(element(by.id('profileView'))).toBeVisible()
|
||||||
|
await element(by.id('unfollowBtn')).tap()
|
||||||
|
await element(by.id('profileHeaderBackBtn')).tap()
|
||||||
|
|
||||||
|
await element(by.id('bottomBarHomeBtn')).tap()
|
||||||
|
await element(by.id('followingFeedPage-feed-flatlist')).swipe(
|
||||||
|
'down',
|
||||||
|
'slow',
|
||||||
|
1,
|
||||||
|
0.5,
|
||||||
|
0.5,
|
||||||
|
)
|
||||||
|
await element(by.id('followingFeedPage-feed-flatlist')).swipe(
|
||||||
|
'down',
|
||||||
|
'slow',
|
||||||
|
1,
|
||||||
|
0.5,
|
||||||
|
0.5,
|
||||||
|
)
|
||||||
|
|
||||||
|
// followed users NOT present
|
||||||
|
await expect(element(by.id('feedItem-by-carla.test'))).not.toExist()
|
||||||
|
await expect(element(by.id('feedItem-by-bob.test'))).not.toExist()
|
||||||
|
|
||||||
|
// feed users
|
||||||
|
await expect(
|
||||||
|
element(
|
||||||
|
by.id('postText').withAncestor(by.id('feedItem-by-dan.test')),
|
||||||
|
).atIndex(0),
|
||||||
|
).toHaveText('Post 0')
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,11 +1,10 @@
|
||||||
/* eslint-env detox/detox */
|
/* eslint-env detox/detox */
|
||||||
|
|
||||||
import {openApp, login, createServer, sleep} from '../util'
|
import {openApp, loginAsAlice, loginAsBob, createServer, sleep} from '../util'
|
||||||
|
|
||||||
describe('Mute lists', () => {
|
describe('Mute lists', () => {
|
||||||
let service: string
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
service = await createServer('?users&follows&labels')
|
await createServer('?users&follows&labels')
|
||||||
await openApp({
|
await openApp({
|
||||||
permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
|
permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
|
||||||
})
|
})
|
||||||
|
@ -13,10 +12,8 @@ describe('Mute lists', () => {
|
||||||
|
|
||||||
it('Login and view my mutelists', async () => {
|
it('Login and view my mutelists', async () => {
|
||||||
await expect(element(by.id('signInButton'))).toBeVisible()
|
await expect(element(by.id('signInButton'))).toBeVisible()
|
||||||
await login(service, 'alice', 'hunter2')
|
await loginAsAlice()
|
||||||
await element(by.id('viewHeaderDrawerBtn')).tap()
|
await element(by.id('e2eGotoModeration')).tap()
|
||||||
await expect(element(by.id('drawer'))).toBeVisible()
|
|
||||||
await element(by.id('menuItemButton-Moderation')).tap()
|
|
||||||
await element(by.id('mutelistsBtn')).tap()
|
await element(by.id('mutelistsBtn')).tap()
|
||||||
await expect(element(by.id('list-Muted Users'))).toBeVisible()
|
await expect(element(by.id('list-Muted Users'))).toBeVisible()
|
||||||
await element(by.id('list-Muted Users')).tap()
|
await element(by.id('list-Muted Users')).tap()
|
||||||
|
@ -141,19 +138,9 @@ describe('Mute lists', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Can report a mute list', async () => {
|
it('Can report a mute list', async () => {
|
||||||
await element(by.id('bottomBarHomeBtn')).tap()
|
await element(by.id('e2eGotoSettings')).tap()
|
||||||
// Last test leaves us in the list view so we are going back 1 screen to the lists list screen
|
|
||||||
await element(by.id('viewHeaderDrawerBtn')).tap()
|
|
||||||
// then to the moderation screen
|
|
||||||
await element(by.id('viewHeaderDrawerBtn')).tap()
|
|
||||||
// then to the home screen
|
|
||||||
await element(by.id('viewHeaderDrawerBtn')).tap()
|
|
||||||
// then open the drawer to go to settings
|
|
||||||
await element(by.id('viewHeaderDrawerBtn')).tap()
|
|
||||||
await element(by.id('menuItemButton-Settings')).tap()
|
|
||||||
await element(by.id('signOutBtn')).tap()
|
await element(by.id('signOutBtn')).tap()
|
||||||
await expect(element(by.id('signInButton'))).toBeVisible()
|
await loginAsBob()
|
||||||
await login(service, 'bob.test', 'hunter2')
|
|
||||||
await element(by.id('bottomBarSearchBtn')).tap()
|
await element(by.id('bottomBarSearchBtn')).tap()
|
||||||
await element(by.id('searchTextInput')).typeText('alice')
|
await element(by.id('searchTextInput')).typeText('alice')
|
||||||
await element(by.id('searchAutoCompleteResult-alice.test')).tap()
|
await element(by.id('searchAutoCompleteResult-alice.test')).tap()
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
/* eslint-env detox/detox */
|
/* eslint-env detox/detox */
|
||||||
|
|
||||||
import {openApp, login, createServer, sleep} from '../util'
|
import {openApp, loginAsAlice, createServer, sleep} from '../util'
|
||||||
|
|
||||||
describe('Profile screen', () => {
|
describe('Profile screen', () => {
|
||||||
let service: string
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
service = await createServer('?users&posts&feeds')
|
await createServer('?users&posts&feeds')
|
||||||
await openApp({
|
await openApp({
|
||||||
permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
|
permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
|
||||||
})
|
})
|
||||||
|
@ -13,7 +12,7 @@ describe('Profile screen', () => {
|
||||||
|
|
||||||
it('Login and navigate to my profile', async () => {
|
it('Login and navigate to my profile', async () => {
|
||||||
await expect(element(by.id('signInButton'))).toBeVisible()
|
await expect(element(by.id('signInButton'))).toBeVisible()
|
||||||
await login(service, 'alice', 'hunter2')
|
await loginAsAlice()
|
||||||
await element(by.id('bottomBarProfileBtn')).tap()
|
await element(by.id('bottomBarProfileBtn')).tap()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,17 @@
|
||||||
/* eslint-env detox/detox */
|
/* eslint-env detox/detox */
|
||||||
|
|
||||||
import {openApp, login, createServer} from '../util'
|
import {openApp, loginAsAlice, createServer} from '../util'
|
||||||
|
|
||||||
describe('Search screen', () => {
|
describe('Search screen', () => {
|
||||||
let service: string
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
service = await createServer('?users')
|
await createServer('?users')
|
||||||
await openApp({
|
await openApp({
|
||||||
permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
|
permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Login', async () => {
|
it('Login', async () => {
|
||||||
await login(service, 'alice', 'hunter2')
|
await loginAsAlice()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Navigate to another user profile via autocomplete', async () => {
|
it('Navigate to another user profile via autocomplete', async () => {
|
||||||
|
|
|
@ -1,18 +1,17 @@
|
||||||
/* eslint-env detox/detox */
|
/* eslint-env detox/detox */
|
||||||
|
|
||||||
import {openApp, login, createServer, sleep} from '../util'
|
import {openApp, loginAsAlice, createServer, sleep} from '../util'
|
||||||
|
|
||||||
describe('Self-labeling', () => {
|
describe('Self-labeling', () => {
|
||||||
let service: string
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
service = await createServer('?users')
|
await createServer('?users')
|
||||||
await openApp({
|
await openApp({
|
||||||
permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
|
permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Login', async () => {
|
it('Login', async () => {
|
||||||
await login(service, 'alice', 'hunter2')
|
await loginAsAlice()
|
||||||
await element(by.id('homeScreenFeedTabs-Following')).tap()
|
await element(by.id('homeScreenFeedTabs-Following')).tap()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
/* eslint-env detox/detox */
|
/* eslint-env detox/detox */
|
||||||
|
|
||||||
import {openApp, login, createServer} from '../util'
|
import {openApp, loginAsAlice, createServer} from '../util'
|
||||||
|
|
||||||
describe('Shell', () => {
|
describe('Shell', () => {
|
||||||
let service: string
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
service = await createServer('?users')
|
await createServer('?users')
|
||||||
await openApp({permissions: {notifications: 'YES'}})
|
await openApp({permissions: {notifications: 'YES'}})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Login', async () => {
|
it('Login', async () => {
|
||||||
await login(service, 'alice', 'hunter2')
|
await loginAsAlice()
|
||||||
await element(by.id('homeScreenFeedTabs-Following')).tap()
|
await element(by.id('homeScreenFeedTabs-Following')).tap()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,42 +1,34 @@
|
||||||
/* eslint-env detox/detox */
|
/* eslint-env detox/detox */
|
||||||
|
|
||||||
import {openApp, login, createServer} from '../util'
|
import {openApp, loginAsAlice, loginAsBob, createServer} from '../util'
|
||||||
|
|
||||||
describe('Thread muting', () => {
|
describe('Thread muting', () => {
|
||||||
let service: string
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
service = await createServer('?users&follows')
|
await createServer('?users&follows')
|
||||||
await openApp({permissions: {notifications: 'YES'}})
|
await openApp({permissions: {notifications: 'YES'}})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Login, create a thread, and log out', async () => {
|
it('Login, create a thread, and log out', async () => {
|
||||||
await login(service, 'alice', 'hunter2')
|
await loginAsAlice()
|
||||||
await element(by.id('homeScreenFeedTabs-Following')).tap()
|
await element(by.id('homeScreenFeedTabs-Following')).tap()
|
||||||
await element(by.id('composeFAB')).tap()
|
await element(by.id('composeFAB')).tap()
|
||||||
await element(by.id('composerTextInput')).typeText('Test thread')
|
await element(by.id('composerTextInput')).typeText('Test thread')
|
||||||
await element(by.id('composerPublishBtn')).tap()
|
await element(by.id('composerPublishBtn')).tap()
|
||||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
await expect(element(by.id('composeFAB'))).toBeVisible()
|
||||||
await element(by.id('viewHeaderDrawerBtn')).tap()
|
|
||||||
await element(by.id('menuItemButton-Settings')).tap()
|
|
||||||
await element(by.id('signOutBtn')).tap()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Login, reply to the thread, and log out', async () => {
|
it('Login, reply to the thread, and log out', async () => {
|
||||||
await login(service, 'bob', 'hunter2')
|
await loginAsBob()
|
||||||
await element(by.id('homeScreenFeedTabs-Following')).tap()
|
await element(by.id('homeScreenFeedTabs-Following')).tap()
|
||||||
const alicePosts = by.id('feedItem-by-alice.test')
|
const alicePosts = by.id('feedItem-by-alice.test')
|
||||||
await element(by.id('replyBtn').withAncestor(alicePosts)).atIndex(0).tap()
|
await element(by.id('replyBtn').withAncestor(alicePosts)).atIndex(0).tap()
|
||||||
await element(by.id('composerTextInput')).typeText('Reply 1')
|
await element(by.id('composerTextInput')).typeText('Reply 1')
|
||||||
await element(by.id('composerPublishBtn')).tap()
|
await element(by.id('composerPublishBtn')).tap()
|
||||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
await expect(element(by.id('composeFAB'))).toBeVisible()
|
||||||
await element(by.id('viewHeaderDrawerBtn')).tap()
|
|
||||||
await element(by.id('menuItemButton-Settings')).tap()
|
|
||||||
await element(by.id('signOutBtn')).tap()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Login, confirm notification exists, mute thread, and log out', async () => {
|
it('Login, confirm notification exists, mute thread, and log out', async () => {
|
||||||
await login(service, 'alice', 'hunter2')
|
await loginAsAlice()
|
||||||
|
|
||||||
await element(by.id('bottomBarNotificationsBtn')).tap()
|
await element(by.id('bottomBarNotificationsBtn')).tap()
|
||||||
const bobNotifs = by.id('feedItem-by-bob.test')
|
const bobNotifs = by.id('feedItem-by-bob.test')
|
||||||
await expect(
|
await expect(
|
||||||
|
@ -50,14 +42,10 @@ describe('Thread muting', () => {
|
||||||
await waitFor(element(by.id('viewHeaderDrawerBtn')))
|
await waitFor(element(by.id('viewHeaderDrawerBtn')))
|
||||||
.toBeVisible()
|
.toBeVisible()
|
||||||
.withTimeout(5000)
|
.withTimeout(5000)
|
||||||
|
|
||||||
await element(by.id('viewHeaderDrawerBtn')).tap()
|
|
||||||
await element(by.id('menuItemButton-Settings')).tap()
|
|
||||||
await element(by.id('signOutBtn')).tap()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Login, reply to the thread twice, and log out', async () => {
|
it('Login, reply to the thread twice, and log out', async () => {
|
||||||
await login(service, 'bob', 'hunter2')
|
await loginAsBob()
|
||||||
|
|
||||||
await element(by.id('bottomBarProfileBtn')).tap()
|
await element(by.id('bottomBarProfileBtn')).tap()
|
||||||
await element(by.id('selector-1')).tap()
|
await element(by.id('selector-1')).tap()
|
||||||
|
@ -74,13 +62,10 @@ describe('Thread muting', () => {
|
||||||
await expect(element(by.id('composeFAB'))).toBeVisible()
|
await expect(element(by.id('composeFAB'))).toBeVisible()
|
||||||
|
|
||||||
await element(by.id('bottomBarHomeBtn')).tap()
|
await element(by.id('bottomBarHomeBtn')).tap()
|
||||||
await element(by.id('viewHeaderDrawerBtn')).tap()
|
|
||||||
await element(by.id('menuItemButton-Settings')).tap()
|
|
||||||
await element(by.id('signOutBtn')).tap()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Login, confirm notifications dont exist, unmute the thread, confirm notifications exist', async () => {
|
it('Login, confirm notifications dont exist, unmute the thread, confirm notifications exist', async () => {
|
||||||
await login(service, 'alice', 'hunter2')
|
await loginAsAlice()
|
||||||
|
|
||||||
await element(by.id('bottomBarNotificationsBtn')).tap()
|
await element(by.id('bottomBarNotificationsBtn')).tap()
|
||||||
const bobNotifs = by.id('feedItem-by-bob.test')
|
const bobNotifs = by.id('feedItem-by-bob.test')
|
||||||
|
@ -93,7 +78,7 @@ describe('Thread muting', () => {
|
||||||
await element(by.id('postDropdownBtn').withAncestor(alicePosts))
|
await element(by.id('postDropdownBtn').withAncestor(alicePosts))
|
||||||
.atIndex(0)
|
.atIndex(0)
|
||||||
.tap()
|
.tap()
|
||||||
await element(by.text('Mute thread')).tap()
|
await element(by.text('Unmute thread')).tap()
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
// the swipe down to trigger PTR isnt working and I dont want to block on this
|
// the swipe down to trigger PTR isnt working and I dont want to block on this
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
/* eslint-env detox/detox */
|
/* eslint-env detox/detox */
|
||||||
|
|
||||||
import {openApp, login, createServer} from '../util'
|
import {openApp, loginAsAlice, createServer} from '../util'
|
||||||
|
|
||||||
describe('Thread screen', () => {
|
describe('Thread screen', () => {
|
||||||
let service: string
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
service = await createServer('?users&follows&thread')
|
await createServer('?users&follows&thread')
|
||||||
await openApp({permissions: {notifications: 'YES'}})
|
await openApp({permissions: {notifications: 'YES'}})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Login & navigate to thread', async () => {
|
it('Login & navigate to thread', async () => {
|
||||||
await login(service, 'alice', 'hunter2')
|
await loginAsAlice()
|
||||||
await element(by.id('homeScreenFeedTabs-Following')).tap()
|
await element(by.id('homeScreenFeedTabs-Following')).tap()
|
||||||
await element(by.id('feedItem-by-bob.test')).atIndex(0).tap()
|
await element(by.id('feedItem-by-bob.test')).atIndex(0).tap()
|
||||||
await expect(
|
await expect(
|
||||||
|
|
|
@ -69,6 +69,14 @@ export async function login(
|
||||||
await element(by.id('loginNextButton')).tap()
|
await element(by.id('loginNextButton')).tap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function loginAsAlice() {
|
||||||
|
await element(by.id('e2eSignInAlice')).tap()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loginAsBob() {
|
||||||
|
await element(by.id('e2eSignInBob')).tap()
|
||||||
|
}
|
||||||
|
|
||||||
async function openAppForDebugBuild(platform: string, opts: any) {
|
async function openAppForDebugBuild(platform: string, opts: any) {
|
||||||
const deepLinkUrl = // Local testing with packager
|
const deepLinkUrl = // Local testing with packager
|
||||||
/*process.env.EXPO_USE_UPDATES
|
/*process.env.EXPO_USE_UPDATES
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import net from 'net'
|
import net from 'net'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import {TestPds as DevEnvTestPDS, TestNetworkNoAppView} from '@atproto/dev-env'
|
import {TestNetworkNoAppView} from '@atproto/dev-env'
|
||||||
import {AtUri, BskyAgent} from '@atproto/api'
|
import {AtUri, BskyAgent} from '@atproto/api'
|
||||||
|
|
||||||
export interface TestUser {
|
export interface TestUser {
|
||||||
|
@ -24,7 +24,7 @@ export async function createServer(
|
||||||
const port = await getPort()
|
const port = await getPort()
|
||||||
const port2 = await getPort(port + 1)
|
const port2 = await getPort(port + 1)
|
||||||
const pdsUrl = `http://localhost:${port}`
|
const pdsUrl = `http://localhost:${port}`
|
||||||
const {pds, plc} = await TestNetworkNoAppView.create({
|
const testNet = await TestNetworkNoAppView.create({
|
||||||
pds: {port, publicUrl: pdsUrl, inviteRequired},
|
pds: {port, publicUrl: pdsUrl, inviteRequired},
|
||||||
plc: {port: port2},
|
plc: {port: port2},
|
||||||
})
|
})
|
||||||
|
@ -35,10 +35,10 @@ export async function createServer(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pdsUrl,
|
pdsUrl,
|
||||||
mocker: new Mocker(pds, pdsUrl, pic),
|
mocker: new Mocker(testNet, pdsUrl, pic),
|
||||||
async close() {
|
async close() {
|
||||||
await pds.server.destroy()
|
await testNet.pds.server.destroy()
|
||||||
await plc.server.destroy()
|
await testNet.plc.server.destroy()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,13 +48,21 @@ class Mocker {
|
||||||
users: Record<string, TestUser> = {}
|
users: Record<string, TestUser> = {}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public pds: DevEnvTestPDS,
|
public testNet: TestNetworkNoAppView,
|
||||||
public service: string,
|
public service: string,
|
||||||
public pic: Uint8Array,
|
public pic: Uint8Array,
|
||||||
) {
|
) {
|
||||||
this.agent = new BskyAgent({service})
|
this.agent = new BskyAgent({service})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get pds() {
|
||||||
|
return this.testNet.pds
|
||||||
|
}
|
||||||
|
|
||||||
|
get plc() {
|
||||||
|
return this.testNet.plc
|
||||||
|
}
|
||||||
|
|
||||||
// NOTE
|
// NOTE
|
||||||
// deterministic date generator
|
// deterministic date generator
|
||||||
// we use this to ensure the mock dataset is always the same
|
// we use this to ensure the mock dataset is always the same
|
||||||
|
@ -212,24 +220,34 @@ class Mocker {
|
||||||
return await agent.like(uri, cid)
|
return await agent.like(uri, cid)
|
||||||
}
|
}
|
||||||
|
|
||||||
async createFeed(user: string) {
|
async createFeed(user: string, rkey: string, posts: string[]) {
|
||||||
const agent = this.users[user]?.agent
|
const agent = this.users[user]?.agent
|
||||||
if (!agent) {
|
if (!agent) {
|
||||||
throw new Error(`Not a user: ${user}`)
|
throw new Error(`Not a user: ${user}`)
|
||||||
}
|
}
|
||||||
const fg1Uri = AtUri.make(
|
const fgUri = AtUri.make(
|
||||||
this.users[user].did,
|
this.users[user].did,
|
||||||
'app.bsky.feed.generator',
|
'app.bsky.feed.generator',
|
||||||
'alice-favs',
|
rkey,
|
||||||
)
|
)
|
||||||
|
const fg1 = await this.testNet.createFeedGen({
|
||||||
|
[fgUri.toString()]: async () => {
|
||||||
|
return {
|
||||||
|
encoding: 'application/json',
|
||||||
|
body: {
|
||||||
|
feed: posts.slice(0, 30).map(uri => ({post: uri})),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
const avatarRes = await agent.api.com.atproto.repo.uploadBlob(this.pic, {
|
const avatarRes = await agent.api.com.atproto.repo.uploadBlob(this.pic, {
|
||||||
encoding: 'image/png',
|
encoding: 'image/png',
|
||||||
})
|
})
|
||||||
return await agent.api.app.bsky.feed.generator.create(
|
return await agent.api.app.bsky.feed.generator.create(
|
||||||
{repo: this.users[user].did, rkey: fg1Uri.rkey},
|
{repo: this.users[user].did, rkey},
|
||||||
{
|
{
|
||||||
did: 'did:web:fake.com',
|
did: fg1.did,
|
||||||
displayName: 'alices feed',
|
displayName: rkey,
|
||||||
description: 'all my fav stuff',
|
description: 'all my fav stuff',
|
||||||
avatar: avatarRes.data.blob,
|
avatar: avatarRes.data.blob,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
|
|
|
@ -18,6 +18,7 @@ import * as Toast from './view/com/util/Toast'
|
||||||
import {handleLink} from './Navigation'
|
import {handleLink} from './Navigation'
|
||||||
import {QueryClientProvider} from '@tanstack/react-query'
|
import {QueryClientProvider} from '@tanstack/react-query'
|
||||||
import {queryClient} from 'lib/react-query'
|
import {queryClient} from 'lib/react-query'
|
||||||
|
import {TestCtrls} from 'view/com/testing/TestCtrls'
|
||||||
|
|
||||||
SplashScreen.preventAutoHideAsync()
|
SplashScreen.preventAutoHideAsync()
|
||||||
|
|
||||||
|
@ -59,6 +60,7 @@ const App = observer(function AppImpl() {
|
||||||
<analytics.Provider>
|
<analytics.Provider>
|
||||||
<RootStoreProvider value={rootStore}>
|
<RootStoreProvider value={rootStore}>
|
||||||
<GestureHandlerRootView style={s.h100pct}>
|
<GestureHandlerRootView style={s.h100pct}>
|
||||||
|
<TestCtrls />
|
||||||
<Shell />
|
<Shell />
|
||||||
</GestureHandlerRootView>
|
</GestureHandlerRootView>
|
||||||
</RootStoreProvider>
|
</RootStoreProvider>
|
||||||
|
|
|
@ -128,23 +128,32 @@ export class FeedTuner {
|
||||||
tune(
|
tune(
|
||||||
feed: FeedViewPost[],
|
feed: FeedViewPost[],
|
||||||
tunerFns: FeedTunerFn[] = [],
|
tunerFns: FeedTunerFn[] = [],
|
||||||
{dryRun}: {dryRun: boolean} = {dryRun: false},
|
{dryRun, maintainOrder}: {dryRun: boolean; maintainOrder: boolean} = {
|
||||||
|
dryRun: false,
|
||||||
|
maintainOrder: false,
|
||||||
|
},
|
||||||
): FeedViewPostsSlice[] {
|
): FeedViewPostsSlice[] {
|
||||||
let slices: FeedViewPostsSlice[] = []
|
let slices: FeedViewPostsSlice[] = []
|
||||||
|
|
||||||
// arrange the posts into thread slices
|
if (maintainOrder) {
|
||||||
for (let i = feed.length - 1; i >= 0; i--) {
|
slices = feed.map(item => new FeedViewPostsSlice([item]))
|
||||||
const item = feed[i]
|
} else {
|
||||||
|
// arrange the posts into thread slices
|
||||||
|
for (let i = feed.length - 1; i >= 0; i--) {
|
||||||
|
const item = feed[i]
|
||||||
|
|
||||||
const selfReplyUri = getSelfReplyUri(item)
|
const selfReplyUri = getSelfReplyUri(item)
|
||||||
if (selfReplyUri) {
|
if (selfReplyUri) {
|
||||||
const parent = slices.find(item2 => item2.isNextInThread(selfReplyUri))
|
const parent = slices.find(item2 =>
|
||||||
if (parent) {
|
item2.isNextInThread(selfReplyUri),
|
||||||
parent.insert(item)
|
)
|
||||||
continue
|
if (parent) {
|
||||||
|
parent.insert(item)
|
||||||
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
slices.unshift(new FeedViewPostsSlice([item]))
|
||||||
}
|
}
|
||||||
slices.unshift(new FeedViewPostsSlice([item]))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// run the custom tuners
|
// run the custom tuners
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {RootStoreModel} from 'state/index'
|
||||||
import {timeout} from 'lib/async/timeout'
|
import {timeout} from 'lib/async/timeout'
|
||||||
import {bundleAsync} from 'lib/async/bundle'
|
import {bundleAsync} from 'lib/async/bundle'
|
||||||
import {feedUriToHref} from 'lib/strings/url-helpers'
|
import {feedUriToHref} from 'lib/strings/url-helpers'
|
||||||
|
import {FeedTuner} from '../feed-manip'
|
||||||
import {FeedAPI, FeedAPIResponse, FeedSourceInfo} from './types'
|
import {FeedAPI, FeedAPIResponse, FeedSourceInfo} from './types'
|
||||||
|
|
||||||
const REQUEST_WAIT_MS = 500 // 500ms
|
const REQUEST_WAIT_MS = 500 // 500ms
|
||||||
|
@ -43,7 +44,7 @@ export class MergeFeedAPI implements FeedAPI {
|
||||||
|
|
||||||
// always keep following topped up
|
// always keep following topped up
|
||||||
if (this.following.numReady < limit) {
|
if (this.following.numReady < limit) {
|
||||||
promises.push(this.following.fetchNext(30))
|
promises.push(this.following.fetchNext(60))
|
||||||
}
|
}
|
||||||
|
|
||||||
// pick the next feeds to sample from
|
// pick the next feeds to sample from
|
||||||
|
@ -84,7 +85,8 @@ export class MergeFeedAPI implements FeedAPI {
|
||||||
const i = this.itemCursor++
|
const i = this.itemCursor++
|
||||||
const candidateFeeds = this.customFeeds.filter(f => f.numReady > 0)
|
const candidateFeeds = this.customFeeds.filter(f => f.numReady > 0)
|
||||||
const canSample = candidateFeeds.length > 0
|
const canSample = candidateFeeds.length > 0
|
||||||
const hasFollows = this.following.numReady > 0
|
const hasFollows = this.following.hasMore
|
||||||
|
const hasFollowsReady = this.following.numReady > 0
|
||||||
|
|
||||||
// this condition establishes the frequency that custom feeds are woven into follows
|
// this condition establishes the frequency that custom feeds are woven into follows
|
||||||
const shouldSample =
|
const shouldSample =
|
||||||
|
@ -98,7 +100,11 @@ export class MergeFeedAPI implements FeedAPI {
|
||||||
// time to sample, or the user isnt following anybody
|
// time to sample, or the user isnt following anybody
|
||||||
return candidateFeeds[this.sampleCursor++ % candidateFeeds.length].take(1)
|
return candidateFeeds[this.sampleCursor++ % candidateFeeds.length].take(1)
|
||||||
}
|
}
|
||||||
// not time to sample
|
if (!hasFollowsReady) {
|
||||||
|
// stop here so more follows can be fetched
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
// provide follow
|
||||||
return this.following.take(1)
|
return this.following.take(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,6 +180,13 @@ class MergeFeedSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
class MergeFeedSource_Following extends MergeFeedSource {
|
class MergeFeedSource_Following extends MergeFeedSource {
|
||||||
|
tuner = new FeedTuner()
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
super.reset()
|
||||||
|
this.tuner.reset()
|
||||||
|
}
|
||||||
|
|
||||||
async fetchNext(n: number) {
|
async fetchNext(n: number) {
|
||||||
return this._fetchNextInner(n)
|
return this._fetchNextInner(n)
|
||||||
}
|
}
|
||||||
|
@ -183,10 +196,16 @@ class MergeFeedSource_Following extends MergeFeedSource {
|
||||||
limit: number,
|
limit: number,
|
||||||
): Promise<AppBskyFeedGetTimeline.Response> {
|
): Promise<AppBskyFeedGetTimeline.Response> {
|
||||||
const res = await this.rootStore.agent.getTimeline({cursor, limit})
|
const res = await this.rootStore.agent.getTimeline({cursor, limit})
|
||||||
// filter out mutes pre-emptively to ensure better mixing
|
// run the tuner pre-emptively to ensure better mixing
|
||||||
res.data.feed = res.data.feed.filter(
|
const slices = this.tuner.tune(
|
||||||
post => !post.post.author.viewer?.muted,
|
res.data.feed,
|
||||||
|
this.rootStore.preferences.getFeedTuners('home'),
|
||||||
|
{
|
||||||
|
dryRun: false,
|
||||||
|
maintainOrder: true,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
res.data.feed = slices.map(slice => slice.rootItem)
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,8 +83,14 @@ export async function DEFAULT_FEEDS(
|
||||||
// local dev
|
// local dev
|
||||||
const aliceDid = await resolveHandle('alice.test')
|
const aliceDid = await resolveHandle('alice.test')
|
||||||
return {
|
return {
|
||||||
pinned: [`at://${aliceDid}/app.bsky.feed.generator/alice-favs`],
|
pinned: [
|
||||||
saved: [`at://${aliceDid}/app.bsky.feed.generator/alice-favs`],
|
`at://${aliceDid}/app.bsky.feed.generator/alice-favs`,
|
||||||
|
`at://${aliceDid}/app.bsky.feed.generator/alice-favs2`,
|
||||||
|
],
|
||||||
|
saved: [
|
||||||
|
`at://${aliceDid}/app.bsky.feed.generator/alice-favs`,
|
||||||
|
`at://${aliceDid}/app.bsky.feed.generator/alice-favs2`,
|
||||||
|
],
|
||||||
}
|
}
|
||||||
} else if (IS_STAGING(serviceUrl)) {
|
} else if (IS_STAGING(serviceUrl)) {
|
||||||
// staging
|
// staging
|
||||||
|
|
|
@ -139,53 +139,6 @@ export class PostsFeedModel {
|
||||||
this.tuner.reset()
|
this.tuner.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
get feedTuners() {
|
|
||||||
const areRepliesEnabled = this.rootStore.preferences.homeFeedRepliesEnabled
|
|
||||||
const areRepliesByFollowedOnlyEnabled =
|
|
||||||
this.rootStore.preferences.homeFeedRepliesByFollowedOnlyEnabled
|
|
||||||
const repliesThreshold = this.rootStore.preferences.homeFeedRepliesThreshold
|
|
||||||
const areRepostsEnabled = this.rootStore.preferences.homeFeedRepostsEnabled
|
|
||||||
const areQuotePostsEnabled =
|
|
||||||
this.rootStore.preferences.homeFeedQuotePostsEnabled
|
|
||||||
|
|
||||||
if (this.feedType === 'custom') {
|
|
||||||
return [
|
|
||||||
FeedTuner.dedupReposts,
|
|
||||||
FeedTuner.preferredLangOnly(
|
|
||||||
this.rootStore.preferences.contentLanguages,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
if (this.feedType === 'home' || this.feedType === 'following') {
|
|
||||||
const feedTuners = []
|
|
||||||
|
|
||||||
if (areRepostsEnabled) {
|
|
||||||
feedTuners.push(FeedTuner.dedupReposts)
|
|
||||||
} else {
|
|
||||||
feedTuners.push(FeedTuner.removeReposts)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (areRepliesEnabled) {
|
|
||||||
feedTuners.push(
|
|
||||||
FeedTuner.thresholdRepliesOnly({
|
|
||||||
userDid: this.rootStore.session.data?.did || '',
|
|
||||||
minLikes: repliesThreshold,
|
|
||||||
followedOnly: areRepliesByFollowedOnlyEnabled,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
feedTuners.push(FeedTuner.removeReplies)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!areQuotePostsEnabled) {
|
|
||||||
feedTuners.push(FeedTuner.removeQuotePosts)
|
|
||||||
}
|
|
||||||
|
|
||||||
return feedTuners
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load for first render
|
* Load for first render
|
||||||
*/
|
*/
|
||||||
|
@ -275,9 +228,14 @@ export class PostsFeedModel {
|
||||||
}
|
}
|
||||||
const post = await this.api.peekLatest()
|
const post = await this.api.peekLatest()
|
||||||
if (post) {
|
if (post) {
|
||||||
const slices = this.tuner.tune([post], this.feedTuners, {
|
const slices = this.tuner.tune(
|
||||||
dryRun: true,
|
[post],
|
||||||
})
|
this.rootStore.preferences.getFeedTuners(this.feedType),
|
||||||
|
{
|
||||||
|
dryRun: true,
|
||||||
|
maintainOrder: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
if (slices[0]) {
|
if (slices[0]) {
|
||||||
const sliceModel = new PostsFeedSliceModel(this.rootStore, slices[0])
|
const sliceModel = new PostsFeedSliceModel(this.rootStore, slices[0])
|
||||||
if (sliceModel.moderation.content.filter) {
|
if (sliceModel.moderation.content.filter) {
|
||||||
|
@ -363,7 +321,10 @@ export class PostsFeedModel {
|
||||||
|
|
||||||
const slices = this.options.isSimpleFeed
|
const slices = this.options.isSimpleFeed
|
||||||
? res.feed.map(item => new FeedViewPostsSlice([item]))
|
? res.feed.map(item => new FeedViewPostsSlice([item]))
|
||||||
: this.tuner.tune(res.feed, this.feedTuners)
|
: this.tuner.tune(
|
||||||
|
res.feed,
|
||||||
|
this.rootStore.preferences.getFeedTuners(this.feedType),
|
||||||
|
)
|
||||||
|
|
||||||
const toAppend: PostsFeedSliceModel[] = []
|
const toAppend: PostsFeedSliceModel[] = []
|
||||||
for (const slice of slices) {
|
for (const slice of slices) {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {ModerationOpts} from '@atproto/api'
|
||||||
import {DEFAULT_FEEDS} from 'lib/constants'
|
import {DEFAULT_FEEDS} from 'lib/constants'
|
||||||
import {deviceLocales} from 'platform/detection'
|
import {deviceLocales} from 'platform/detection'
|
||||||
import {getAge} from 'lib/strings/time'
|
import {getAge} from 'lib/strings/time'
|
||||||
|
import {FeedTuner} from 'lib/api/feed-manip'
|
||||||
import {LANGUAGES} from '../../../locale/languages'
|
import {LANGUAGES} from '../../../locale/languages'
|
||||||
|
|
||||||
// TEMP we need to permanently convert 'show' to 'ignore', for now we manually convert -prf
|
// TEMP we need to permanently convert 'show' to 'ignore', for now we manually convert -prf
|
||||||
|
@ -540,6 +541,52 @@ export class PreferencesModel {
|
||||||
toggleRequireAltTextEnabled() {
|
toggleRequireAltTextEnabled() {
|
||||||
this.requireAltTextEnabled = !this.requireAltTextEnabled
|
this.requireAltTextEnabled = !this.requireAltTextEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getFeedTuners(
|
||||||
|
feedType: 'home' | 'following' | 'author' | 'custom' | 'likes',
|
||||||
|
) {
|
||||||
|
const areRepliesEnabled = this.homeFeedRepliesEnabled
|
||||||
|
const areRepliesByFollowedOnlyEnabled =
|
||||||
|
this.homeFeedRepliesByFollowedOnlyEnabled
|
||||||
|
const repliesThreshold = this.homeFeedRepliesThreshold
|
||||||
|
const areRepostsEnabled = this.homeFeedRepostsEnabled
|
||||||
|
const areQuotePostsEnabled = this.homeFeedQuotePostsEnabled
|
||||||
|
|
||||||
|
if (feedType === 'custom') {
|
||||||
|
return [
|
||||||
|
FeedTuner.dedupReposts,
|
||||||
|
FeedTuner.preferredLangOnly(this.contentLanguages),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
if (feedType === 'home' || feedType === 'following') {
|
||||||
|
const feedTuners = []
|
||||||
|
|
||||||
|
if (areRepostsEnabled) {
|
||||||
|
feedTuners.push(FeedTuner.dedupReposts)
|
||||||
|
} else {
|
||||||
|
feedTuners.push(FeedTuner.removeReposts)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (areRepliesEnabled) {
|
||||||
|
feedTuners.push(
|
||||||
|
FeedTuner.thresholdRepliesOnly({
|
||||||
|
userDid: this.rootStore.session.data?.did || '',
|
||||||
|
minLikes: repliesThreshold,
|
||||||
|
followedOnly: areRepliesByFollowedOnlyEnabled,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
feedTuners.push(FeedTuner.removeReplies)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!areQuotePostsEnabled) {
|
||||||
|
feedTuners.push(FeedTuner.removeQuotePosts)
|
||||||
|
}
|
||||||
|
|
||||||
|
return feedTuners
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TEMP we need to permanently convert 'show' to 'ignore', for now we manually convert -prf
|
// TEMP we need to permanently convert 'show' to 'ignore', for now we manually convert -prf
|
||||||
|
|
|
@ -35,7 +35,7 @@ export const Component = observer(function ProfilePreviewImpl({
|
||||||
}, [model, screen])
|
}, [model, screen])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[pal.view, s.flex1]}>
|
<View testID="profilePreview" style={[pal.view, s.flex1]}>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
styles.headerWrapper,
|
styles.headerWrapper,
|
||||||
|
|
|
@ -67,6 +67,7 @@ export const FeedsTabBar = observer(function FeedsTabBarImpl(
|
||||||
</Text>
|
</Text>
|
||||||
<View style={[pal.view]}>
|
<View style={[pal.view]}>
|
||||||
<Link
|
<Link
|
||||||
|
testID="viewHeaderHomeFeedPrefsBtn"
|
||||||
href="/settings/home-feed"
|
href="/settings/home-feed"
|
||||||
hitSlop={HITSLOP_10}
|
hitSlop={HITSLOP_10}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
|
|
|
@ -299,6 +299,7 @@ export const FeedItem = observer(function FeedItemImpl({
|
||||||
{item.richText?.text ? (
|
{item.richText?.text ? (
|
||||||
<View style={styles.postTextContainer}>
|
<View style={styles.postTextContainer}>
|
||||||
<RichText
|
<RichText
|
||||||
|
testID="postText"
|
||||||
type="post-text"
|
type="post-text"
|
||||||
richText={item.richText}
|
richText={item.richText}
|
||||||
lineHeight={1.3}
|
lineHeight={1.3}
|
||||||
|
|
|
@ -556,6 +556,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
|
||||||
|
|
||||||
{!isDesktop && !hideBackButton && (
|
{!isDesktop && !hideBackButton && (
|
||||||
<TouchableWithoutFeedback
|
<TouchableWithoutFeedback
|
||||||
|
testID="profileHeaderBackBtn"
|
||||||
onPress={onPressBack}
|
onPress={onPressBack}
|
||||||
hitSlop={BACK_HITSLOP}
|
hitSlop={BACK_HITSLOP}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
|
|
|
@ -102,6 +102,7 @@ export function HeaderWithInput({
|
||||||
/>
|
/>
|
||||||
{query ? (
|
{query ? (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
testID="searchTextInputClearBtn"
|
||||||
onPress={onPressClearQuery}
|
onPress={onPressClearQuery}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="Clear search query"
|
accessibilityLabel="Clear search query"
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {Pressable, View} from 'react-native'
|
||||||
|
import {useStores} from 'state/index'
|
||||||
|
import {navigate} from '../../../Navigation'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This utility component is only included in the test simulator
|
||||||
|
* build. It gives some quick triggers which help improve the pace
|
||||||
|
* of the tests dramatically.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const BTN = {height: 1, width: 1, backgroundColor: 'red'}
|
||||||
|
|
||||||
|
export function TestCtrls() {
|
||||||
|
const store = useStores()
|
||||||
|
const onPressSignInAlice = async () => {
|
||||||
|
await store.session.login({
|
||||||
|
service: 'http://localhost:3000',
|
||||||
|
identifier: 'alice.test',
|
||||||
|
password: 'hunter2',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const onPressSignInBob = async () => {
|
||||||
|
await store.session.login({
|
||||||
|
service: 'http://localhost:3000',
|
||||||
|
identifier: 'bob.test',
|
||||||
|
password: 'hunter2',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<View style={{position: 'absolute', top: 100, right: 0, zIndex: 100}}>
|
||||||
|
<Pressable
|
||||||
|
testID="e2eSignInAlice"
|
||||||
|
onPress={onPressSignInAlice}
|
||||||
|
accessibilityRole="button"
|
||||||
|
style={BTN}
|
||||||
|
/>
|
||||||
|
<Pressable
|
||||||
|
testID="e2eSignInBob"
|
||||||
|
onPress={onPressSignInBob}
|
||||||
|
accessibilityRole="button"
|
||||||
|
style={BTN}
|
||||||
|
/>
|
||||||
|
<Pressable
|
||||||
|
testID="e2eGotoHome"
|
||||||
|
onPress={() => navigate('Home')}
|
||||||
|
accessibilityRole="button"
|
||||||
|
style={BTN}
|
||||||
|
/>
|
||||||
|
<Pressable
|
||||||
|
testID="e2eGotoSettings"
|
||||||
|
onPress={() => navigate('Settings')}
|
||||||
|
accessibilityRole="button"
|
||||||
|
style={BTN}
|
||||||
|
/>
|
||||||
|
<Pressable
|
||||||
|
testID="e2eGotoModeration"
|
||||||
|
onPress={() => navigate('Moderation')}
|
||||||
|
accessibilityRole="button"
|
||||||
|
style={BTN}
|
||||||
|
/>
|
||||||
|
<Pressable
|
||||||
|
testID="e2eToggleMergefeed"
|
||||||
|
onPress={() => store.preferences.toggleHomeFeedMergeFeedEnabled()}
|
||||||
|
accessibilityRole="button"
|
||||||
|
style={BTN}
|
||||||
|
/>
|
||||||
|
<Pressable
|
||||||
|
testID="e2eRefreshHome"
|
||||||
|
onPress={() => store.me.mainFeed.refresh()}
|
||||||
|
accessibilityRole="button"
|
||||||
|
style={BTN}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export function TestCtrls() {
|
||||||
|
return null
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ import {colors} from 'lib/styles'
|
||||||
import {TypographyVariant} from 'lib/ThemeContext'
|
import {TypographyVariant} from 'lib/ThemeContext'
|
||||||
|
|
||||||
export function ToggleButton({
|
export function ToggleButton({
|
||||||
|
testID,
|
||||||
type = 'default-light',
|
type = 'default-light',
|
||||||
label,
|
label,
|
||||||
isSelected,
|
isSelected,
|
||||||
|
@ -15,6 +16,7 @@ export function ToggleButton({
|
||||||
labelType,
|
labelType,
|
||||||
onPress,
|
onPress,
|
||||||
}: {
|
}: {
|
||||||
|
testID?: string
|
||||||
type?: ButtonType
|
type?: ButtonType
|
||||||
label: string
|
label: string
|
||||||
isSelected: boolean
|
isSelected: boolean
|
||||||
|
@ -134,7 +136,7 @@ export function ToggleButton({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
<Button type={type} onPress={onPress} style={style}>
|
<Button testID={testID} type={type} onPress={onPress} style={style}>
|
||||||
<View style={styles.outer}>
|
<View style={styles.outer}>
|
||||||
<View style={[circleStyle, styles.circle]}>
|
<View style={[circleStyle, styles.circle]}>
|
||||||
<View
|
<View
|
||||||
|
|
|
@ -86,6 +86,7 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
|
||||||
Set this setting to "No" to hide all replies from your feed.
|
Set this setting to "No" to hide all replies from your feed.
|
||||||
</Text>
|
</Text>
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
|
testID="toggleRepliesBtn"
|
||||||
type="default-light"
|
type="default-light"
|
||||||
label={store.preferences.homeFeedRepliesEnabled ? 'Yes' : 'No'}
|
label={store.preferences.homeFeedRepliesEnabled ? 'Yes' : 'No'}
|
||||||
isSelected={store.preferences.homeFeedRepliesEnabled}
|
isSelected={store.preferences.homeFeedRepliesEnabled}
|
||||||
|
|
Loading…
Reference in New Issue