Improve typeahead search with inclusion of followed users (temporary solution) (#1612)
* Update follows cache to maintain some user info * Prioritize follows in composer autocomplete * Clean up logic and add new autocomplete to search * Update follow hook
This commit is contained in:
parent
19f8389fc7
commit
bd7db8af26
20 changed files with 197 additions and 142 deletions
|
@ -1,11 +1,11 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import {AppBskyActorDefs} from '@atproto/api'
|
||||||
import {useStores} from 'state/index'
|
import {useStores} from 'state/index'
|
||||||
import {FollowState} from 'state/models/cache/my-follows'
|
import {FollowState} from 'state/models/cache/my-follows'
|
||||||
|
|
||||||
export function useFollowDid({did}: {did: string}) {
|
export function useFollowProfile(profile: AppBskyActorDefs.ProfileViewBasic) {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const state = store.me.follows.getFollowState(did)
|
const state = store.me.follows.getFollowState(profile.did)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
state,
|
state,
|
||||||
|
@ -13,8 +13,10 @@ export function useFollowDid({did}: {did: string}) {
|
||||||
toggle: React.useCallback(async () => {
|
toggle: React.useCallback(async () => {
|
||||||
if (state === FollowState.Following) {
|
if (state === FollowState.Following) {
|
||||||
try {
|
try {
|
||||||
await store.agent.deleteFollow(store.me.follows.getFollowUri(did))
|
await store.agent.deleteFollow(
|
||||||
store.me.follows.removeFollow(did)
|
store.me.follows.getFollowUri(profile.did),
|
||||||
|
)
|
||||||
|
store.me.follows.removeFollow(profile.did)
|
||||||
return {
|
return {
|
||||||
state: FollowState.NotFollowing,
|
state: FollowState.NotFollowing,
|
||||||
following: false,
|
following: false,
|
||||||
|
@ -25,8 +27,14 @@ export function useFollowDid({did}: {did: string}) {
|
||||||
}
|
}
|
||||||
} else if (state === FollowState.NotFollowing) {
|
} else if (state === FollowState.NotFollowing) {
|
||||||
try {
|
try {
|
||||||
const res = await store.agent.follow(did)
|
const res = await store.agent.follow(profile.did)
|
||||||
store.me.follows.addFollow(did, res.uri)
|
store.me.follows.addFollow(profile.did, {
|
||||||
|
followRecordUri: res.uri,
|
||||||
|
did: profile.did,
|
||||||
|
handle: profile.handle,
|
||||||
|
displayName: profile.displayName,
|
||||||
|
avatar: profile.avatar,
|
||||||
|
})
|
||||||
return {
|
return {
|
||||||
state: FollowState.Following,
|
state: FollowState.Following,
|
||||||
following: true,
|
following: true,
|
||||||
|
@ -41,6 +49,6 @@ export function useFollowDid({did}: {did: string}) {
|
||||||
state: FollowState.Unknown,
|
state: FollowState.Unknown,
|
||||||
following: false,
|
following: false,
|
||||||
}
|
}
|
||||||
}, [store, did, state]),
|
}, [store, profile, state]),
|
||||||
}
|
}
|
||||||
}
|
}
|
103
src/state/models/cache/my-follows.ts
vendored
103
src/state/models/cache/my-follows.ts
vendored
|
@ -1,7 +1,14 @@
|
||||||
import {makeAutoObservable} from 'mobx'
|
import {makeAutoObservable} from 'mobx'
|
||||||
import {AppBskyActorDefs} from '@atproto/api'
|
import {
|
||||||
|
AppBskyActorDefs,
|
||||||
|
AppBskyGraphGetFollows as GetFollows,
|
||||||
|
moderateProfile,
|
||||||
|
} from '@atproto/api'
|
||||||
import {RootStoreModel} from '../root-store'
|
import {RootStoreModel} from '../root-store'
|
||||||
|
|
||||||
|
const MAX_SYNC_PAGES = 10
|
||||||
|
const SYNC_TTL = 60e3 * 10 // 10 minutes
|
||||||
|
|
||||||
type Profile = AppBskyActorDefs.ProfileViewBasic | AppBskyActorDefs.ProfileView
|
type Profile = AppBskyActorDefs.ProfileViewBasic | AppBskyActorDefs.ProfileView
|
||||||
|
|
||||||
export enum FollowState {
|
export enum FollowState {
|
||||||
|
@ -10,6 +17,14 @@ export enum FollowState {
|
||||||
Unknown,
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FollowInfo {
|
||||||
|
did: string
|
||||||
|
followRecordUri: string | undefined
|
||||||
|
handle: string
|
||||||
|
displayName: string | undefined
|
||||||
|
avatar: string | undefined
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
* follows. It should be periodically refreshed and updated any time
|
* follows. It should be periodically refreshed and updated any time
|
||||||
|
@ -17,9 +32,8 @@ export enum FollowState {
|
||||||
*/
|
*/
|
||||||
export class MyFollowsCache {
|
export class MyFollowsCache {
|
||||||
// data
|
// data
|
||||||
followDidToRecordMap: Record<string, string | boolean> = {}
|
byDid: Record<string, FollowInfo> = {}
|
||||||
lastSync = 0
|
lastSync = 0
|
||||||
myDid?: string
|
|
||||||
|
|
||||||
constructor(public rootStore: RootStoreModel) {
|
constructor(public rootStore: RootStoreModel) {
|
||||||
makeAutoObservable(
|
makeAutoObservable(
|
||||||
|
@ -35,16 +49,45 @@ export class MyFollowsCache {
|
||||||
// =
|
// =
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this.followDidToRecordMap = {}
|
this.byDid = {}
|
||||||
this.lastSync = 0
|
}
|
||||||
this.myDid = undefined
|
|
||||||
|
/**
|
||||||
|
* Syncs a subset of the user's follows
|
||||||
|
* for performance reasons, caps out at 1000 follows
|
||||||
|
*/
|
||||||
|
async syncIfNeeded() {
|
||||||
|
if (this.lastSync > Date.now() - SYNC_TTL) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let cursor
|
||||||
|
for (let i = 0; i < MAX_SYNC_PAGES; i++) {
|
||||||
|
const res: GetFollows.Response = await this.rootStore.agent.getFollows({
|
||||||
|
actor: this.rootStore.me.did,
|
||||||
|
cursor,
|
||||||
|
limit: 100,
|
||||||
|
})
|
||||||
|
res.data.follows = res.data.follows.filter(
|
||||||
|
profile =>
|
||||||
|
!moderateProfile(profile, this.rootStore.preferences.moderationOpts)
|
||||||
|
.account.filter,
|
||||||
|
)
|
||||||
|
this.hydrateMany(res.data.follows)
|
||||||
|
if (!res.data.cursor) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
cursor = res.data.cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastSync = Date.now()
|
||||||
}
|
}
|
||||||
|
|
||||||
getFollowState(did: string): FollowState {
|
getFollowState(did: string): FollowState {
|
||||||
if (typeof this.followDidToRecordMap[did] === 'undefined') {
|
if (typeof this.byDid[did] === 'undefined') {
|
||||||
return FollowState.Unknown
|
return FollowState.Unknown
|
||||||
}
|
}
|
||||||
if (typeof this.followDidToRecordMap[did] === 'string') {
|
if (typeof this.byDid[did].followRecordUri === 'string') {
|
||||||
return FollowState.Following
|
return FollowState.Following
|
||||||
}
|
}
|
||||||
return FollowState.NotFollowing
|
return FollowState.NotFollowing
|
||||||
|
@ -53,49 +96,41 @@ export class MyFollowsCache {
|
||||||
async fetchFollowState(did: string): Promise<FollowState> {
|
async fetchFollowState(did: string): Promise<FollowState> {
|
||||||
// TODO: can we get a more efficient method for this? getProfile fetches more data than we need -prf
|
// TODO: can we get a more efficient method for this? getProfile fetches more data than we need -prf
|
||||||
const res = await this.rootStore.agent.getProfile({actor: did})
|
const res = await this.rootStore.agent.getProfile({actor: did})
|
||||||
if (res.data.viewer?.following) {
|
this.hydrate(did, res.data)
|
||||||
this.addFollow(did, res.data.viewer.following)
|
|
||||||
} else {
|
|
||||||
this.removeFollow(did)
|
|
||||||
}
|
|
||||||
return this.getFollowState(did)
|
return this.getFollowState(did)
|
||||||
}
|
}
|
||||||
|
|
||||||
getFollowUri(did: string): string {
|
getFollowUri(did: string): string {
|
||||||
const v = this.followDidToRecordMap[did]
|
const v = this.byDid[did]
|
||||||
if (typeof v === 'string') {
|
if (typeof v === 'string') {
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
throw new Error('Not a followed user')
|
throw new Error('Not a followed user')
|
||||||
}
|
}
|
||||||
|
|
||||||
addFollow(did: string, recordUri: string) {
|
addFollow(did: string, info: FollowInfo) {
|
||||||
this.followDidToRecordMap[did] = recordUri
|
this.byDid[did] = info
|
||||||
}
|
}
|
||||||
|
|
||||||
removeFollow(did: string) {
|
removeFollow(did: string) {
|
||||||
this.followDidToRecordMap[did] = false
|
if (this.byDid[did]) {
|
||||||
}
|
this.byDid[did].followRecordUri = undefined
|
||||||
|
|
||||||
/**
|
|
||||||
* Use this to incrementally update the cache as views provide information
|
|
||||||
*/
|
|
||||||
hydrate(did: string, recordUri: string | undefined) {
|
|
||||||
if (recordUri) {
|
|
||||||
this.followDidToRecordMap[did] = recordUri
|
|
||||||
} else {
|
|
||||||
this.followDidToRecordMap[did] = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
hydrate(did: string, profile: Profile) {
|
||||||
* Use this to incrementally update the cache as views provide information
|
this.byDid[did] = {
|
||||||
*/
|
did,
|
||||||
hydrateProfiles(profiles: Profile[]) {
|
followRecordUri: profile.viewer?.following,
|
||||||
|
handle: profile.handle,
|
||||||
|
displayName: profile.displayName,
|
||||||
|
avatar: profile.avatar,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hydrateMany(profiles: Profile[]) {
|
||||||
for (const profile of profiles) {
|
for (const profile of profiles) {
|
||||||
if (profile.viewer) {
|
this.hydrate(profile.did, profile)
|
||||||
this.hydrate(profile.did, profile.viewer.following)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -137,7 +137,7 @@ export class ProfileModel {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.followersCount++
|
this.followersCount++
|
||||||
this.viewer.following = res.uri
|
this.viewer.following = res.uri
|
||||||
this.rootStore.me.follows.addFollow(this.did, res.uri)
|
this.rootStore.me.follows.hydrate(this.did, this)
|
||||||
})
|
})
|
||||||
track('Profile:Follow', {
|
track('Profile:Follow', {
|
||||||
username: this.handle,
|
username: this.handle,
|
||||||
|
@ -290,8 +290,8 @@ export class ProfileModel {
|
||||||
this.labels = res.data.labels
|
this.labels = res.data.labels
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
async _createRichText() {
|
async _createRichText() {
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
import {
|
import {AppBskyActorDefs} from '@atproto/api'
|
||||||
AppBskyActorDefs,
|
|
||||||
AppBskyGraphGetFollows as GetFollows,
|
|
||||||
moderateProfile,
|
|
||||||
} 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'
|
||||||
|
@ -43,35 +39,13 @@ export class FoafsModel {
|
||||||
try {
|
try {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
|
|
||||||
// fetch & hydrate up to 1000 follows
|
// fetch some of the user's follows
|
||||||
{
|
await this.rootStore.me.follows.syncIfNeeded()
|
||||||
let cursor
|
|
||||||
for (let i = 0; i < 10; i++) {
|
|
||||||
const res: GetFollows.Response =
|
|
||||||
await this.rootStore.agent.getFollows({
|
|
||||||
actor: this.rootStore.me.did,
|
|
||||||
cursor,
|
|
||||||
limit: 100,
|
|
||||||
})
|
|
||||||
res.data.follows = res.data.follows.filter(
|
|
||||||
profile =>
|
|
||||||
!moderateProfile(
|
|
||||||
profile,
|
|
||||||
this.rootStore.preferences.moderationOpts,
|
|
||||||
).account.filter,
|
|
||||||
)
|
|
||||||
this.rootStore.me.follows.hydrateProfiles(res.data.follows)
|
|
||||||
if (!res.data.cursor) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
cursor = res.data.cursor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// grab 10 of the users followed by the user
|
// grab 10 of the users followed by the user
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.sources = sampleSize(
|
this.sources = sampleSize(
|
||||||
Object.keys(this.rootStore.me.follows.followDidToRecordMap),
|
Object.keys(this.rootStore.me.follows.byDid),
|
||||||
10,
|
10,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -100,7 +74,7 @@ export class FoafsModel {
|
||||||
for (let i = 0; i < results.length; i++) {
|
for (let i = 0; i < results.length; i++) {
|
||||||
const res = results[i]
|
const res = results[i]
|
||||||
if (res.status === 'fulfilled') {
|
if (res.status === 'fulfilled') {
|
||||||
this.rootStore.me.follows.hydrateProfiles(res.value.data.follows)
|
this.rootStore.me.follows.hydrateMany(res.value.data.follows)
|
||||||
}
|
}
|
||||||
const profile = profiles.data.profiles[i]
|
const profile = profiles.data.profiles[i]
|
||||||
const source = this.sources[i]
|
const source = this.sources[i]
|
||||||
|
|
|
@ -76,7 +76,7 @@ export class SuggestedActorsModel {
|
||||||
!moderateProfile(actor, this.rootStore.preferences.moderationOpts)
|
!moderateProfile(actor, this.rootStore.preferences.moderationOpts)
|
||||||
.account.filter,
|
.account.filter,
|
||||||
)
|
)
|
||||||
this.rootStore.me.follows.hydrateProfiles(actors)
|
this.rootStore.me.follows.hydrateMany(actors)
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
if (replace) {
|
if (replace) {
|
||||||
|
@ -118,7 +118,7 @@ export class SuggestedActorsModel {
|
||||||
actor: actor,
|
actor: actor,
|
||||||
})
|
})
|
||||||
const {suggestions: moreSuggestions} = res.data
|
const {suggestions: moreSuggestions} = res.data
|
||||||
this.rootStore.me.follows.hydrateProfiles(moreSuggestions)
|
this.rootStore.me.follows.hydrateMany(moreSuggestions)
|
||||||
// dedupe
|
// dedupe
|
||||||
const toInsert = moreSuggestions.filter(
|
const toInsert = moreSuggestions.filter(
|
||||||
s => !this.suggestions.find(s2 => s2.did === s.did),
|
s => !this.suggestions.find(s2 => s2.did === s.did),
|
||||||
|
|
|
@ -4,6 +4,8 @@ import AwaitLock from 'await-lock'
|
||||||
import {RootStoreModel} from '../root-store'
|
import {RootStoreModel} from '../root-store'
|
||||||
import {isInvalidHandle} from 'lib/strings/handles'
|
import {isInvalidHandle} from 'lib/strings/handles'
|
||||||
|
|
||||||
|
type ProfileViewBasic = AppBskyActorDefs.ProfileViewBasic
|
||||||
|
|
||||||
export class UserAutocompleteModel {
|
export class UserAutocompleteModel {
|
||||||
// state
|
// state
|
||||||
isLoading = false
|
isLoading = false
|
||||||
|
@ -12,9 +14,8 @@ export class UserAutocompleteModel {
|
||||||
lock = new AwaitLock()
|
lock = new AwaitLock()
|
||||||
|
|
||||||
// data
|
// data
|
||||||
follows: AppBskyActorDefs.ProfileViewBasic[] = []
|
|
||||||
searchRes: AppBskyActorDefs.ProfileViewBasic[] = []
|
|
||||||
knownHandles: Set<string> = new Set()
|
knownHandles: Set<string> = new Set()
|
||||||
|
_suggestions: ProfileViewBasic[] = []
|
||||||
|
|
||||||
constructor(public rootStore: RootStoreModel) {
|
constructor(public rootStore: RootStoreModel) {
|
||||||
makeAutoObservable(
|
makeAutoObservable(
|
||||||
|
@ -27,29 +28,35 @@ export class UserAutocompleteModel {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
get suggestions() {
|
get follows(): ProfileViewBasic[] {
|
||||||
|
return Object.values(this.rootStore.me.follows.byDid).map(item => ({
|
||||||
|
did: item.did,
|
||||||
|
handle: item.handle,
|
||||||
|
displayName: item.displayName,
|
||||||
|
avatar: item.avatar,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
get suggestions(): ProfileViewBasic[] {
|
||||||
if (!this.isActive) {
|
if (!this.isActive) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
if (this.prefix) {
|
return this._suggestions
|
||||||
return this.searchRes.map(user => ({
|
|
||||||
handle: user.handle,
|
|
||||||
displayName: user.displayName,
|
|
||||||
avatar: user.avatar,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
return this.follows.map(follow => ({
|
|
||||||
handle: follow.handle,
|
|
||||||
displayName: follow.displayName,
|
|
||||||
avatar: follow.avatar,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// public api
|
// public api
|
||||||
// =
|
// =
|
||||||
|
|
||||||
async setup() {
|
async setup() {
|
||||||
await this._getFollows()
|
await this.rootStore.me.follows.syncIfNeeded()
|
||||||
|
runInAction(() => {
|
||||||
|
for (const did in this.rootStore.me.follows.byDid) {
|
||||||
|
const info = this.rootStore.me.follows.byDid[did]
|
||||||
|
if (!isInvalidHandle(info.handle)) {
|
||||||
|
this.knownHandles.add(info.handle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
setActive(v: boolean) {
|
setActive(v: boolean) {
|
||||||
|
@ -57,7 +64,7 @@ export class UserAutocompleteModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
async setPrefix(prefix: string) {
|
async setPrefix(prefix: string) {
|
||||||
const origPrefix = prefix.trim()
|
const origPrefix = prefix.trim().toLocaleLowerCase()
|
||||||
this.prefix = origPrefix
|
this.prefix = origPrefix
|
||||||
await this.lock.acquireAsync()
|
await this.lock.acquireAsync()
|
||||||
try {
|
try {
|
||||||
|
@ -65,9 +72,27 @@ export class UserAutocompleteModel {
|
||||||
if (this.prefix !== origPrefix) {
|
if (this.prefix !== origPrefix) {
|
||||||
return // another prefix was set before we got our chance
|
return // another prefix was set before we got our chance
|
||||||
}
|
}
|
||||||
await this._search()
|
|
||||||
|
// reset to follow results
|
||||||
|
this._computeSuggestions([])
|
||||||
|
|
||||||
|
// ask backend
|
||||||
|
const res = await this.rootStore.agent.searchActorsTypeahead({
|
||||||
|
term: this.prefix,
|
||||||
|
limit: 8,
|
||||||
|
})
|
||||||
|
this._computeSuggestions(res.data.actors)
|
||||||
|
|
||||||
|
// update known handles
|
||||||
|
runInAction(() => {
|
||||||
|
for (const u of res.data.actors) {
|
||||||
|
this.knownHandles.add(u.handle)
|
||||||
|
}
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
this.searchRes = []
|
runInAction(() => {
|
||||||
|
this._computeSuggestions([])
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.lock.release()
|
this.lock.release()
|
||||||
|
@ -77,28 +102,40 @@ export class UserAutocompleteModel {
|
||||||
// internal
|
// internal
|
||||||
// =
|
// =
|
||||||
|
|
||||||
async _getFollows() {
|
_computeSuggestions(searchRes: AppBskyActorDefs.ProfileViewBasic[] = []) {
|
||||||
const res = await this.rootStore.agent.getFollows({
|
if (this.prefix) {
|
||||||
actor: this.rootStore.me.did || '',
|
const items: ProfileViewBasic[] = []
|
||||||
})
|
for (const item of this.follows) {
|
||||||
runInAction(() => {
|
if (prefixMatch(this.prefix, item)) {
|
||||||
this.follows = res.data.follows.filter(f => !isInvalidHandle(f.handle))
|
items.push(item)
|
||||||
for (const f of this.follows) {
|
}
|
||||||
this.knownHandles.add(f.handle)
|
if (items.length >= 8) {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
for (const item of searchRes) {
|
||||||
}
|
if (!items.find(item2 => item2.handle === item.handle)) {
|
||||||
|
items.push({
|
||||||
async _search() {
|
did: item.did,
|
||||||
const res = await this.rootStore.agent.searchActorsTypeahead({
|
handle: item.handle,
|
||||||
term: this.prefix,
|
displayName: item.displayName,
|
||||||
limit: 8,
|
avatar: item.avatar,
|
||||||
})
|
})
|
||||||
runInAction(() => {
|
}
|
||||||
this.searchRes = res.data.actors
|
|
||||||
for (const u of this.searchRes) {
|
|
||||||
this.knownHandles.add(u.handle)
|
|
||||||
}
|
}
|
||||||
})
|
this._suggestions = items
|
||||||
|
} else {
|
||||||
|
this._suggestions = this.follows
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function prefixMatch(prefix: string, info: ProfileViewBasic): boolean {
|
||||||
|
if (info.handle.includes(prefix)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (info.displayName?.toLocaleLowerCase().includes(prefix)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
@ -316,7 +316,7 @@ export class PostsFeedModel {
|
||||||
this.emptyFetches = 0
|
this.emptyFetches = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
this.rootStore.me.follows.hydrateProfiles(
|
this.rootStore.me.follows.hydrateMany(
|
||||||
res.feed.map(item => item.post.author),
|
res.feed.map(item => item.post.author),
|
||||||
)
|
)
|
||||||
for (const item of res.feed) {
|
for (const item of res.feed) {
|
||||||
|
|
|
@ -61,7 +61,7 @@ export class InvitedUsers {
|
||||||
profile => !profile.viewer?.following,
|
profile => !profile.viewer?.following,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
this.rootStore.me.follows.hydrateProfiles(this.profiles)
|
this.rootStore.me.follows.hydrateMany(this.profiles)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.rootStore.log.error(
|
this.rootStore.log.error(
|
||||||
'Failed to fetch profiles for invited users',
|
'Failed to fetch profiles for invited users',
|
||||||
|
|
|
@ -126,7 +126,7 @@ export class LikesModel {
|
||||||
_appendAll(res: GetLikes.Response) {
|
_appendAll(res: GetLikes.Response) {
|
||||||
this.loadMoreCursor = res.data.cursor
|
this.loadMoreCursor = res.data.cursor
|
||||||
this.hasMore = !!this.loadMoreCursor
|
this.hasMore = !!this.loadMoreCursor
|
||||||
this.rootStore.me.follows.hydrateProfiles(
|
this.rootStore.me.follows.hydrateMany(
|
||||||
res.data.likes.map(like => like.actor),
|
res.data.likes.map(like => like.actor),
|
||||||
)
|
)
|
||||||
this.likes = this.likes.concat(res.data.likes)
|
this.likes = this.likes.concat(res.data.likes)
|
||||||
|
|
|
@ -130,6 +130,6 @@ export class RepostedByModel {
|
||||||
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)
|
||||||
this.rootStore.me.follows.hydrateProfiles(res.data.repostedBy)
|
this.rootStore.me.follows.hydrateMany(res.data.repostedBy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -115,6 +115,6 @@ export class UserFollowersModel {
|
||||||
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)
|
||||||
this.rootStore.me.follows.hydrateProfiles(res.data.followers)
|
this.rootStore.me.follows.hydrateMany(res.data.followers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -115,6 +115,6 @@ export class UserFollowsModel {
|
||||||
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)
|
||||||
this.rootStore.me.follows.hydrateProfiles(res.data.follows)
|
this.rootStore.me.follows.hydrateMany(res.data.follows)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,7 +59,7 @@ export class SearchUIModel {
|
||||||
} while (profilesSearch.length)
|
} while (profilesSearch.length)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.rootStore.me.follows.hydrateProfiles(profiles)
|
this.rootStore.me.follows.hydrateMany(profiles)
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.profiles = profiles
|
this.profiles = profiles
|
||||||
|
|
|
@ -89,7 +89,7 @@ export const ProfileCard = observer(function ProfileCardImpl({
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<FollowButton
|
<FollowButton
|
||||||
did={profile.did}
|
profile={profile}
|
||||||
labelStyle={styles.followButton}
|
labelStyle={styles.followButton}
|
||||||
onToggleFollow={async isFollow => {
|
onToggleFollow={async isFollow => {
|
||||||
if (isFollow) {
|
if (isFollow) {
|
||||||
|
|
|
@ -75,7 +75,7 @@ function InvitedUser({
|
||||||
<FollowButton
|
<FollowButton
|
||||||
unfollowedType="primary"
|
unfollowedType="primary"
|
||||||
followedType="primary-light"
|
followedType="primary-light"
|
||||||
did={profile.did}
|
profile={profile}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
testID="dismissBtn"
|
testID="dismissBtn"
|
||||||
|
|
|
@ -1,25 +1,26 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {StyleProp, TextStyle, View} from 'react-native'
|
import {StyleProp, TextStyle, View} from 'react-native'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
|
import {AppBskyActorDefs} from '@atproto/api'
|
||||||
import {Button, ButtonType} from '../util/forms/Button'
|
import {Button, ButtonType} from '../util/forms/Button'
|
||||||
import * as Toast from '../util/Toast'
|
import * as Toast from '../util/Toast'
|
||||||
import {FollowState} from 'state/models/cache/my-follows'
|
import {FollowState} from 'state/models/cache/my-follows'
|
||||||
import {useFollowDid} from 'lib/hooks/useFollowDid'
|
import {useFollowProfile} from 'lib/hooks/useFollowProfile'
|
||||||
|
|
||||||
export const FollowButton = observer(function FollowButtonImpl({
|
export const FollowButton = observer(function FollowButtonImpl({
|
||||||
unfollowedType = 'inverted',
|
unfollowedType = 'inverted',
|
||||||
followedType = 'default',
|
followedType = 'default',
|
||||||
did,
|
profile,
|
||||||
onToggleFollow,
|
onToggleFollow,
|
||||||
labelStyle,
|
labelStyle,
|
||||||
}: {
|
}: {
|
||||||
unfollowedType?: ButtonType
|
unfollowedType?: ButtonType
|
||||||
followedType?: ButtonType
|
followedType?: ButtonType
|
||||||
did: string
|
profile: AppBskyActorDefs.ProfileViewBasic
|
||||||
onToggleFollow?: (v: boolean) => void
|
onToggleFollow?: (v: boolean) => void
|
||||||
labelStyle?: StyleProp<TextStyle>
|
labelStyle?: StyleProp<TextStyle>
|
||||||
}) {
|
}) {
|
||||||
const {state, following, toggle} = useFollowDid({did})
|
const {state, following, toggle} = useFollowProfile(profile)
|
||||||
|
|
||||||
const onPress = React.useCallback(async () => {
|
const onPress = React.useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -200,7 +200,7 @@ export const ProfileCardWithFollowBtn = observer(
|
||||||
noBorder={noBorder}
|
noBorder={noBorder}
|
||||||
followers={followers}
|
followers={followers}
|
||||||
renderButton={
|
renderButton={
|
||||||
isMe ? undefined : () => <FollowButton did={profile.did} />
|
isMe ? undefined : () => <FollowButton profile={profile} />
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
@ -19,7 +19,7 @@ import {useStores} from 'state/index'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {Text} from 'view/com/util/text/Text'
|
import {Text} from 'view/com/util/text/Text'
|
||||||
import {UserAvatar} from 'view/com/util/UserAvatar'
|
import {UserAvatar} from 'view/com/util/UserAvatar'
|
||||||
import {useFollowDid} from 'lib/hooks/useFollowDid'
|
import {useFollowProfile} from 'lib/hooks/useFollowProfile'
|
||||||
import {Button} from 'view/com/util/forms/Button'
|
import {Button} from 'view/com/util/forms/Button'
|
||||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||||
import {sanitizeHandle} from 'lib/strings/handles'
|
import {sanitizeHandle} from 'lib/strings/handles'
|
||||||
|
@ -83,7 +83,7 @@ export function ProfileHeaderSuggestedFollows({
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
store.me.follows.hydrateProfiles(suggestions)
|
store.me.follows.hydrateMany(suggestions)
|
||||||
|
|
||||||
return suggestions
|
return suggestions
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -218,7 +218,7 @@ const SuggestedFollow = observer(function SuggestedFollowImpl({
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const {following, toggle} = useFollowDid({did: profile.did})
|
const {following, toggle} = useFollowProfile(profile)
|
||||||
const moderation = moderateProfile(profile, store.preferences.moderationOpts)
|
const moderation = moderateProfile(profile, store.preferences.moderationOpts)
|
||||||
|
|
||||||
const onPress = React.useCallback(async () => {
|
const onPress = React.useCallback(async () => {
|
||||||
|
|
|
@ -148,18 +148,18 @@ export const SearchScreen = withAuthRequired(
|
||||||
style={pal.view}
|
style={pal.view}
|
||||||
onScroll={onMainScroll}
|
onScroll={onMainScroll}
|
||||||
scrollEventThrottle={100}>
|
scrollEventThrottle={100}>
|
||||||
{query && autocompleteView.searchRes.length ? (
|
{query && autocompleteView.suggestions.length ? (
|
||||||
<>
|
<>
|
||||||
{autocompleteView.searchRes.map((profile, index) => (
|
{autocompleteView.suggestions.map((suggestion, index) => (
|
||||||
<ProfileCard
|
<ProfileCard
|
||||||
key={profile.did}
|
key={suggestion.did}
|
||||||
testID={`searchAutoCompleteResult-${profile.handle}`}
|
testID={`searchAutoCompleteResult-${suggestion.handle}`}
|
||||||
profile={profile}
|
profile={suggestion}
|
||||||
noBorder={index === 0}
|
noBorder={index === 0}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
) : query && !autocompleteView.searchRes.length ? (
|
) : query && !autocompleteView.suggestions.length ? (
|
||||||
<View>
|
<View>
|
||||||
<Text style={[pal.textLight, styles.searchPrompt]}>
|
<Text style={[pal.textLight, styles.searchPrompt]}>
|
||||||
No results found for {autocompleteView.prefix}
|
No results found for {autocompleteView.prefix}
|
||||||
|
|
|
@ -90,9 +90,9 @@ export const DesktopSearch = observer(function DesktopSearch() {
|
||||||
|
|
||||||
{query !== '' && (
|
{query !== '' && (
|
||||||
<View style={[pal.view, pal.borderDark, styles.resultsContainer]}>
|
<View style={[pal.view, pal.borderDark, styles.resultsContainer]}>
|
||||||
{autocompleteView.searchRes.length ? (
|
{autocompleteView.suggestions.length ? (
|
||||||
<>
|
<>
|
||||||
{autocompleteView.searchRes.map((item, i) => (
|
{autocompleteView.suggestions.map((item, i) => (
|
||||||
<ProfileCard key={item.did} profile={item} noBorder={i === 0} />
|
<ProfileCard key={item.did} profile={item} noBorder={i === 0} />
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue