Skip to content

Commit

Permalink
Feed UI update working branch [WIP] (#1420)
Browse files Browse the repository at this point in the history
* Feeds navigation on right side of desktop (#1403)

* Remove home feed header on desktop

* Add feeds to right sidebar

* Add simple non-moving header to desktop

* Improve loading state of custom feed header

* Remove log

Co-authored-by: Eric Bailey <[email protected]>

* Remove dead comment

---------

Co-authored-by: Eric Bailey <[email protected]>

* Redesign feeds tab (#1439)

* consolidate saved feeds and discover into one screen

* Add hoverStyle behavior to <Link>

* More UI work on SavedFeeds

* Replace satellite icon with a hashtag

* Tune My Feeds mobile ui

* Handle no results in my feeds

* Remove old DiscoverFeeds screen

* Remove multifeed

* Remove DiscoverFeeds from router

* Improve loading placeholders

* Small fixes

* Fix types

* Fix overflow issue on firefox

* Add icons prompting to open feeds

---------

Co-authored-by: Paul Frazee <[email protected]>

* Merge feed prototype [WIP] (#1398)

* POC WIP for the mergefeed

* Add feed API wrapper and move mergefeed into it

* Show feed source in mergefeed

* Add lodash.random dep

* Improve mergefeed sampling and reliability

* Tune source ui element

* Improve mergefeed edge condition handling

* Remove in-place update of feeds for performance

* Fix link on native

* Fix bad ref

* Improve variety in mergefeed sampling

* Fix types

* Fix rebase error

* Add missing source field (got dropped in merge)

* Update find more link

* Simplify the right hand feeds nav

* Bring back load latest button on desktop & unify impl

* Add 'From' to source

* Add simple headers to desktop home & notifications

* Fix thread view jumping around horizontally

* Add unread indicators to desktop headers

* Add home feed preference for enabling the mergefeed

* Add a preference for showing replies among followed users only (#1448)

* Add a preference for showing replies among followed users only

* Simplify the reply filter UI

* Fix typo

* Simplified custom feed header

* Add soft reset to custom feed screen

* Drop all the in-post translate links except when expanded (#1455)

* Update mobile feed settings links to match desktop

* Fixes to feeds screen loading states

* Bolder active state of feeds tab on mobile web

* Fix dark mode issue

---------

Co-authored-by: Eric Bailey <[email protected]>
Co-authored-by: Ansh <[email protected]>
  • Loading branch information
3 people authored Sep 18, 2023
1 parent 3118e3e commit ea88533
Show file tree
Hide file tree
Showing 57 changed files with 1,884 additions and 1,497 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
"lodash.isequal": "^4.5.0",
"lodash.omit": "^4.5.0",
"lodash.once": "^4.1.1",
"lodash.random": "^3.2.0",
"lodash.samplesize": "^4.2.0",
"lodash.set": "^4.3.2",
"lodash.shuffle": "^4.2.0",
Expand Down Expand Up @@ -168,6 +169,7 @@
"@types/lodash.isequal": "^4.5.6",
"@types/lodash.omit": "^4.5.7",
"@types/lodash.once": "^4.1.7",
"@types/lodash.random": "^3.2.7",
"@types/lodash.samplesize": "^4.2.7",
"@types/lodash.set": "^4.3.7",
"@types/lodash.shuffle": "^4.2.7",
Expand Down
6 changes: 0 additions & 6 deletions src/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ import {FeedsScreen} from './view/screens/Feeds'
import {NotificationsScreen} from './view/screens/Notifications'
import {ModerationScreen} from './view/screens/Moderation'
import {ModerationMuteListsScreen} from './view/screens/ModerationMuteLists'
import {DiscoverFeedsScreen} from 'view/screens/DiscoverFeeds'
import {NotFoundScreen} from './view/screens/NotFound'
import {SettingsScreen} from './view/screens/Settings'
import {ProfileScreen} from './view/screens/Profile'
Expand Down Expand Up @@ -113,11 +112,6 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
component={ModerationBlockedAccounts}
options={{title: title('Blocked Accounts')}}
/>
<Stack.Screen
name="DiscoverFeeds"
component={DiscoverFeedsScreen}
options={{title: title('Discover Feeds')}}
/>
<Stack.Screen
name="Settings"
component={SettingsScreen}
Expand Down
47 changes: 42 additions & 5 deletions src/lib/api/feed-manip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
AppBskyEmbedRecordWithMedia,
AppBskyEmbedRecord,
} from '@atproto/api'
import {FeedSourceInfo} from './feed/types'
import {isPostInLanguage} from '../../locale/helpers'
type FeedViewPost = AppBskyFeedDefs.FeedViewPost

Expand Down Expand Up @@ -64,6 +65,11 @@ export class FeedViewPostsSlice {
)
}

get source(): FeedSourceInfo | undefined {
return this.items.find(item => '__source' in item && !!item.__source)
?.__source as FeedSourceInfo
}

containsUri(uri: string) {
return !!this.items.find(item => item.post.uri === uri)
}
Expand Down Expand Up @@ -91,6 +97,23 @@ export class FeedViewPostsSlice {
}
}
}

isFollowingAllAuthors(userDid: string) {
const item = this.rootItem
if (item.post.author.did === userDid) {
return true
}
if (AppBskyFeedDefs.isPostView(item.reply?.parent)) {
const parent = item.reply?.parent
if (parent?.author.did === userDid) {
return true
}
return (
parent?.author.viewer?.following && item.post.author.viewer?.following
)
}
return false
}
}

export class FeedTuner {
Expand Down Expand Up @@ -222,20 +245,34 @@ export class FeedTuner {
return slices
}

static likedRepliesOnly({repliesThreshold}: {repliesThreshold: number}) {
static thresholdRepliesOnly({
userDid,
minLikes,
followedOnly,
}: {
userDid: string
minLikes: number
followedOnly: boolean
}) {
return (
tuner: FeedTuner,
slices: FeedViewPostsSlice[],
): FeedViewPostsSlice[] => {
// remove any replies without at least repliesThreshold likes
// remove any replies without at least minLikes likes
for (let i = slices.length - 1; i >= 0; i--) {
if (slices[i].isFullThread || !slices[i].isReply) {
const slice = slices[i]
if (slice.isFullThread || !slice.isReply) {
continue
}

const item = slices[i].rootItem
const item = slice.rootItem
const isRepost = Boolean(item.reason)
if (!isRepost && (item.post.likeCount || 0) < repliesThreshold) {
if (isRepost) {
continue
}
if ((item.post.likeCount || 0) < minLikes) {
slices.splice(i, 1)
} else if (followedOnly && !slice.isFollowingAllAuthors(userDid)) {
slices.splice(i, 1)
}
}
Expand Down
45 changes: 45 additions & 0 deletions src/lib/api/feed/author.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {
AppBskyFeedDefs,
AppBskyFeedGetAuthorFeed as GetAuthorFeed,
} from '@atproto/api'
import {RootStoreModel} from 'state/index'
import {FeedAPI, FeedAPIResponse} from './types'

export class AuthorFeedAPI implements FeedAPI {
cursor: string | undefined

constructor(
public rootStore: RootStoreModel,
public params: GetAuthorFeed.QueryParams,
) {}

reset() {
this.cursor = undefined
}

async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await this.rootStore.agent.getAuthorFeed({
...this.params,
limit: 1,
})
return res.data.feed[0]
}

async fetchNext({limit}: {limit: number}): Promise<FeedAPIResponse> {
const res = await this.rootStore.agent.getAuthorFeed({
...this.params,
cursor: this.cursor,
limit,
})
if (res.success) {
this.cursor = res.data.cursor
return {
cursor: res.data.cursor,
feed: res.data.feed,
}
}
return {
feed: [],
}
}
}
52 changes: 52 additions & 0 deletions src/lib/api/feed/custom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {
AppBskyFeedDefs,
AppBskyFeedGetFeed as GetCustomFeed,
} from '@atproto/api'
import {RootStoreModel} from 'state/index'
import {FeedAPI, FeedAPIResponse} from './types'

export class CustomFeedAPI implements FeedAPI {
cursor: string | undefined

constructor(
public rootStore: RootStoreModel,
public params: GetCustomFeed.QueryParams,
) {}

reset() {
this.cursor = undefined
}

async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await this.rootStore.agent.app.bsky.feed.getFeed({
...this.params,
limit: 1,
})
return res.data.feed[0]
}

async fetchNext({limit}: {limit: number}): Promise<FeedAPIResponse> {
const res = await this.rootStore.agent.app.bsky.feed.getFeed({
...this.params,
cursor: this.cursor,
limit,
})
if (res.success) {
this.cursor = res.data.cursor
// NOTE
// some custom feeds fail to enforce the pagination limit
// so we manually truncate here
// -prf
if (res.data.feed.length > limit) {
res.data.feed = res.data.feed.slice(0, limit)
}
return {
cursor: res.data.cursor,
feed: res.data.feed,
}
}
return {
feed: [],
}
}
}
37 changes: 37 additions & 0 deletions src/lib/api/feed/following.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {AppBskyFeedDefs} from '@atproto/api'
import {RootStoreModel} from 'state/index'
import {FeedAPI, FeedAPIResponse} from './types'

export class FollowingFeedAPI implements FeedAPI {
cursor: string | undefined

constructor(public rootStore: RootStoreModel) {}

reset() {
this.cursor = undefined
}

async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await this.rootStore.agent.getTimeline({
limit: 1,
})
return res.data.feed[0]
}

async fetchNext({limit}: {limit: number}): Promise<FeedAPIResponse> {
const res = await this.rootStore.agent.getTimeline({
cursor: this.cursor,
limit,
})
if (res.success) {
this.cursor = res.data.cursor
return {
cursor: res.data.cursor,
feed: res.data.feed,
}
}
return {
feed: [],
}
}
}
45 changes: 45 additions & 0 deletions src/lib/api/feed/likes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {
AppBskyFeedDefs,
AppBskyFeedGetActorLikes as GetActorLikes,
} from '@atproto/api'
import {RootStoreModel} from 'state/index'
import {FeedAPI, FeedAPIResponse} from './types'

export class LikesFeedAPI implements FeedAPI {
cursor: string | undefined

constructor(
public rootStore: RootStoreModel,
public params: GetActorLikes.QueryParams,
) {}

reset() {
this.cursor = undefined
}

async peekLatest(): Promise<AppBskyFeedDefs.FeedViewPost> {
const res = await this.rootStore.agent.getActorLikes({
...this.params,
limit: 1,
})
return res.data.feed[0]
}

async fetchNext({limit}: {limit: number}): Promise<FeedAPIResponse> {
const res = await this.rootStore.agent.getActorLikes({
...this.params,
cursor: this.cursor,
limit,
})
if (res.success) {
this.cursor = res.data.cursor
return {
cursor: res.data.cursor,
feed: res.data.feed,
}
}
return {
feed: [],
}
}
}
Loading

0 comments on commit ea88533

Please sign in to comment.