Merge remote-tracking branch 'origin/main' into eric/app-864-integrate-post-tags-into-app

* origin/main: (40 commits)
  1.52
  README: tweaks to high-level context (#1625)
  Fix stuck lightbox header after double tap (#1627)
  Fix: add padding to the spinner bottom while loading threads (#1626)
  Rewrite Android lightbox (#1624)
  Dont trim before posting (close #1621) (#1622)
  Only listen to back button on android (#1623)
  Improve typeahead search with inclusion of followed users (temporary solution) (#1612)
  Slightly smaller highlighted post text (#1608)
  Pull upstream bugfixes to bottom-sheet (#1606)
  Fix animations and gestures getting reset on state updates in the lightbox (#1618)
  Remove unused lightbox options (#1616)
  Profile UI tweaks (#1607)
  Fix invite codes flash on desktop, use loading placeholder (#1591)
  Update to react-native@0.72.5 (#1599)
  Fixed a typo on the onboarding recommended screen (#1604)
  Onboarding & feed fixes (#1602)
  Improve time to content in the search page (#1603)
  Fix a potential reference error in bottombarweb (#1600)
  Fix: only use scroll-positioning control on thread when looking at replies (#1587)
  ...
This commit is contained in:
Eric Bailey
2023-10-09 11:02:43 -05:00
108 changed files with 3705 additions and 2167 deletions
+69 -34
View File
@@ -1,7 +1,14 @@
import {makeAutoObservable} from 'mobx'
import {AppBskyActorDefs} from '@atproto/api'
import {
AppBskyActorDefs,
AppBskyGraphGetFollows as GetFollows,
moderateProfile,
} from '@atproto/api'
import {RootStoreModel} from '../root-store'
const MAX_SYNC_PAGES = 10
const SYNC_TTL = 60e3 * 10 // 10 minutes
type Profile = AppBskyActorDefs.ProfileViewBasic | AppBskyActorDefs.ProfileView
export enum FollowState {
@@ -10,6 +17,14 @@ export enum FollowState {
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
* follows. It should be periodically refreshed and updated any time
@@ -17,9 +32,8 @@ export enum FollowState {
*/
export class MyFollowsCache {
// data
followDidToRecordMap: Record<string, string | boolean> = {}
byDid: Record<string, FollowInfo> = {}
lastSync = 0
myDid?: string
constructor(public rootStore: RootStoreModel) {
makeAutoObservable(
@@ -35,16 +49,45 @@ export class MyFollowsCache {
// =
clear() {
this.followDidToRecordMap = {}
this.lastSync = 0
this.myDid = undefined
this.byDid = {}
}
/**
* 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 {
if (typeof this.followDidToRecordMap[did] === 'undefined') {
if (typeof this.byDid[did] === 'undefined') {
return FollowState.Unknown
}
if (typeof this.followDidToRecordMap[did] === 'string') {
if (typeof this.byDid[did].followRecordUri === 'string') {
return FollowState.Following
}
return FollowState.NotFollowing
@@ -53,49 +96,41 @@ export class MyFollowsCache {
async fetchFollowState(did: string): Promise<FollowState> {
// 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})
if (res.data.viewer?.following) {
this.addFollow(did, res.data.viewer.following)
} else {
this.removeFollow(did)
}
this.hydrate(did, res.data)
return this.getFollowState(did)
}
getFollowUri(did: string): string {
const v = this.followDidToRecordMap[did]
const v = this.byDid[did]
if (typeof v === 'string') {
return v
}
throw new Error('Not a followed user')
}
addFollow(did: string, recordUri: string) {
this.followDidToRecordMap[did] = recordUri
addFollow(did: string, info: FollowInfo) {
this.byDid[did] = info
}
removeFollow(did: string) {
this.followDidToRecordMap[did] = false
}
/**
* 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
if (this.byDid[did]) {
this.byDid[did].followRecordUri = undefined
}
}
/**
* Use this to incrementally update the cache as views provide information
*/
hydrateProfiles(profiles: Profile[]) {
hydrate(did: string, profile: Profile) {
this.byDid[did] = {
did,
followRecordUri: profile.viewer?.following,
handle: profile.handle,
displayName: profile.displayName,
avatar: profile.avatar,
}
}
hydrateMany(profiles: Profile[]) {
for (const profile of profiles) {
if (profile.viewer) {
this.hydrate(profile.did, profile.viewer.following)
}
this.hydrate(profile.did, profile)
}
}
}
+8
View File
@@ -2,6 +2,7 @@ import {makeAutoObservable, runInAction} from 'mobx'
import {
AppBskyFeedGetPostThread as GetPostThread,
AppBskyFeedDefs,
AppBskyFeedPost,
PostModeration,
} from '@atproto/api'
import {AtUri} from '@atproto/api'
@@ -76,6 +77,13 @@ export class PostThreadModel {
return this.rootStore.mutedThreads.uris.has(this.rootUri)
}
get isCachedPostAReply() {
if (AppBskyFeedPost.isRecord(this.thread?.post.record)) {
return !!this.thread?.post.record.reply
}
return false
}
// public api
// =
+2 -2
View File
@@ -137,7 +137,7 @@ export class ProfileModel {
runInAction(() => {
this.followersCount++
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', {
username: this.handle,
@@ -290,8 +290,8 @@ export class ProfileModel {
this.labels = res.data.labels
if (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() {
+5 -31
View File
@@ -1,8 +1,4 @@
import {
AppBskyActorDefs,
AppBskyGraphGetFollows as GetFollows,
moderateProfile,
} from '@atproto/api'
import {AppBskyActorDefs} from '@atproto/api'
import {makeAutoObservable, runInAction} from 'mobx'
import sampleSize from 'lodash.samplesize'
import {bundleAsync} from 'lib/async/bundle'
@@ -43,35 +39,13 @@ export class FoafsModel {
try {
this.isLoading = true
// fetch & hydrate up to 1000 follows
{
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
}
}
// fetch some of the user's follows
await this.rootStore.me.follows.syncIfNeeded()
// grab 10 of the users followed by the user
runInAction(() => {
this.sources = sampleSize(
Object.keys(this.rootStore.me.follows.followDidToRecordMap),
Object.keys(this.rootStore.me.follows.byDid),
10,
)
})
@@ -100,7 +74,7 @@ export class FoafsModel {
for (let i = 0; i < results.length; i++) {
const res = results[i]
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 source = this.sources[i]
+1
View File
@@ -81,6 +81,7 @@ export class OnboardingModel {
}
finish() {
this.rootStore.me.mainFeed.refresh() // load the selected content
this.step = 'Home'
track('Onboarding:Complete')
}
@@ -76,7 +76,7 @@ export class SuggestedActorsModel {
!moderateProfile(actor, this.rootStore.preferences.moderationOpts)
.account.filter,
)
this.rootStore.me.follows.hydrateProfiles(actors)
this.rootStore.me.follows.hydrateMany(actors)
runInAction(() => {
if (replace) {
@@ -118,7 +118,7 @@ export class SuggestedActorsModel {
actor: actor,
})
const {suggestions: moreSuggestions} = res.data
this.rootStore.me.follows.hydrateProfiles(moreSuggestions)
this.rootStore.me.follows.hydrateMany(moreSuggestions)
// dedupe
const toInsert = moreSuggestions.filter(
s => !this.suggestions.find(s2 => s2.did === s.did),
+77 -40
View File
@@ -4,6 +4,8 @@ import AwaitLock from 'await-lock'
import {RootStoreModel} from '../root-store'
import {isInvalidHandle} from 'lib/strings/handles'
type ProfileViewBasic = AppBskyActorDefs.ProfileViewBasic
export class UserAutocompleteModel {
// state
isLoading = false
@@ -12,9 +14,8 @@ export class UserAutocompleteModel {
lock = new AwaitLock()
// data
follows: AppBskyActorDefs.ProfileViewBasic[] = []
searchRes: AppBskyActorDefs.ProfileViewBasic[] = []
knownHandles: Set<string> = new Set()
_suggestions: ProfileViewBasic[] = []
constructor(public rootStore: RootStoreModel) {
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) {
return []
}
if (this.prefix) {
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,
}))
return this._suggestions
}
// public api
// =
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) {
@@ -57,7 +64,7 @@ export class UserAutocompleteModel {
}
async setPrefix(prefix: string) {
const origPrefix = prefix.trim()
const origPrefix = prefix.trim().toLocaleLowerCase()
this.prefix = origPrefix
await this.lock.acquireAsync()
try {
@@ -65,9 +72,27 @@ export class UserAutocompleteModel {
if (this.prefix !== origPrefix) {
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 {
this.searchRes = []
runInAction(() => {
this._computeSuggestions([])
})
}
} finally {
this.lock.release()
@@ -77,28 +102,40 @@ export class UserAutocompleteModel {
// internal
// =
async _getFollows() {
const res = await this.rootStore.agent.getFollows({
actor: this.rootStore.me.did || '',
})
runInAction(() => {
this.follows = res.data.follows.filter(f => !isInvalidHandle(f.handle))
for (const f of this.follows) {
this.knownHandles.add(f.handle)
_computeSuggestions(searchRes: AppBskyActorDefs.ProfileViewBasic[] = []) {
if (this.prefix) {
const items: ProfileViewBasic[] = []
for (const item of this.follows) {
if (prefixMatch(this.prefix, item)) {
items.push(item)
}
if (items.length >= 8) {
break
}
}
})
}
async _search() {
const res = await this.rootStore.agent.searchActorsTypeahead({
term: this.prefix,
limit: 8,
})
runInAction(() => {
this.searchRes = res.data.actors
for (const u of this.searchRes) {
this.knownHandles.add(u.handle)
for (const item of searchRes) {
if (!items.find(item2 => item2.handle === item.handle)) {
items.push({
did: item.did,
handle: item.handle,
displayName: item.displayName,
avatar: item.avatar,
})
}
}
})
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
}
+6 -2
View File
@@ -116,6 +116,10 @@ export class PostsFeedModel {
return this.hasLoaded && !this.hasContent
}
get isLoadingMore() {
return this.isLoading && !this.isRefreshing
}
setHasNewLatest(v: boolean) {
this.hasNewLatest = v
}
@@ -307,12 +311,12 @@ export class PostsFeedModel {
}
async _appendAll(res: FeedAPIResponse, replace = false) {
this.hasMore = !!res.cursor
this.hasMore = !!res.cursor && res.feed.length > 0
if (replace) {
this.emptyFetches = 0
}
this.rootStore.me.follows.hydrateProfiles(
this.rootStore.me.follows.hydrateMany(
res.feed.map(item => item.post.author),
)
for (const item of res.feed) {
+1 -1
View File
@@ -61,7 +61,7 @@ export class InvitedUsers {
profile => !profile.viewer?.following,
)
})
this.rootStore.me.follows.hydrateProfiles(this.profiles)
this.rootStore.me.follows.hydrateMany(this.profiles)
} catch (e) {
this.rootStore.log.error(
'Failed to fetch profiles for invited users',
+1 -1
View File
@@ -126,7 +126,7 @@ export class LikesModel {
_appendAll(res: GetLikes.Response) {
this.loadMoreCursor = res.data.cursor
this.hasMore = !!this.loadMoreCursor
this.rootStore.me.follows.hydrateProfiles(
this.rootStore.me.follows.hydrateMany(
res.data.likes.map(like => like.actor),
)
this.likes = this.likes.concat(res.data.likes)
+1 -1
View File
@@ -130,6 +130,6 @@ export class RepostedByModel {
this.loadMoreCursor = res.data.cursor
this.hasMore = !!this.loadMoreCursor
this.repostedBy = this.repostedBy.concat(res.data.repostedBy)
this.rootStore.me.follows.hydrateProfiles(res.data.repostedBy)
this.rootStore.me.follows.hydrateMany(res.data.repostedBy)
}
}
+1 -1
View File
@@ -115,6 +115,6 @@ export class UserFollowersModel {
this.loadMoreCursor = res.data.cursor
this.hasMore = !!this.loadMoreCursor
this.followers = this.followers.concat(res.data.followers)
this.rootStore.me.follows.hydrateProfiles(res.data.followers)
this.rootStore.me.follows.hydrateMany(res.data.followers)
}
}
+1 -1
View File
@@ -115,6 +115,6 @@ export class UserFollowsModel {
this.loadMoreCursor = res.data.cursor
this.hasMore = !!this.loadMoreCursor
this.follows = this.follows.concat(res.data.follows)
this.rootStore.me.follows.hydrateProfiles(res.data.follows)
this.rootStore.me.follows.hydrateMany(res.data.follows)
}
}
+5 -3
View File
@@ -25,13 +25,13 @@ export class MeModel {
savedFeeds: SavedFeedsModel
notifications: NotificationsFeedModel
follows: MyFollowsCache
invites: ComAtprotoServerDefs.InviteCode[] = []
invites: ComAtprotoServerDefs.InviteCode[] | null = []
appPasswords: ComAtprotoServerListAppPasswords.AppPassword[] = []
lastProfileStateUpdate = Date.now()
lastNotifsUpdate = Date.now()
get invitesAvailable() {
return this.invites.filter(isInviteAvailable).length
return this.invites?.filter(isInviteAvailable).length || null
}
constructor(public rootStore: RootStoreModel) {
@@ -180,7 +180,9 @@ export class MeModel {
} catch (e) {
this.rootStore.log.error('Failed to fetch user invite codes', e)
}
await this.rootStore.invitedUsers.fetch(this.invites)
if (this.invites) {
await this.rootStore.invitedUsers.fetch(this.invites)
}
}
}
+6
View File
@@ -21,6 +21,7 @@ import {PreferencesModel} from './ui/preferences'
import {resetToTab} from '../../Navigation'
import {ImageSizesCache} from './cache/image-sizes'
import {MutedThreads} from './muted-threads'
import {Reminders} from './ui/reminders'
import {reset as resetNavigation} from '../../Navigation'
import {RecentTagsModel} from './ui/tags-autocomplete'
@@ -54,6 +55,7 @@ export class RootStoreModel {
linkMetas = new LinkMetasCache(this)
imageSizes = new ImageSizesCache()
mutedThreads = new MutedThreads()
reminders = new Reminders(this)
recentTags = new RecentTagsModel()
constructor(agent: BskyAgent) {
@@ -79,6 +81,7 @@ export class RootStoreModel {
preferences: this.preferences.serialize(),
invitedUsers: this.invitedUsers.serialize(),
mutedThreads: this.mutedThreads.serialize(),
reminders: this.reminders.serialize(),
recentTags: this.recentTags.serialize(),
}
}
@@ -115,6 +118,9 @@ export class RootStoreModel {
if (hasProp(v, 'recentTags')) {
this.recentTags.hydrate(v.recentTags)
}
if (hasProp(v, 'reminders')) {
this.reminders.hydrate(v.reminders)
}
}
}
+16
View File
@@ -30,6 +30,7 @@ export const accountData = z.object({
email: z.string().optional(),
displayName: z.string().optional(),
aviUrl: z.string().optional(),
emailConfirmed: z.boolean().optional(),
})
export type AccountData = z.infer<typeof accountData>
@@ -106,6 +107,10 @@ export class SessionModel {
return this.accounts.filter(acct => acct.did !== this.data?.did)
}
get emailNeedsConfirmation() {
return !this.currentSession?.emailConfirmed
}
get isSandbox() {
if (!this.data) {
return false
@@ -217,6 +222,7 @@ export class SessionModel {
? addedInfo.displayName
: existingAccount?.displayName || '',
aviUrl: addedInfo ? addedInfo.aviUrl : existingAccount?.aviUrl || '',
emailConfirmed: session?.emailConfirmed,
}
if (!existingAccount) {
this.accounts.push(newAccount)
@@ -246,6 +252,8 @@ export class SessionModel {
did: acct.did,
displayName: acct.displayName,
aviUrl: acct.aviUrl,
email: acct.email,
emailConfirmed: acct.emailConfirmed,
}))
}
@@ -297,6 +305,8 @@ export class SessionModel {
refreshJwt: account.refreshJwt || '',
did: account.did,
handle: account.handle,
email: account.email,
emailConfirmed: account.emailConfirmed,
}),
)
const addedInfo = await this.loadAccountInfo(agent, account.did)
@@ -452,4 +462,10 @@ export class SessionModel {
await this.rootStore.me.load()
}
}
updateLocalAccountData(changes: Partial<AccountData>) {
this.accounts = this.accounts.map(acct =>
acct.did === this.data?.did ? {...acct, ...changes} : acct,
)
}
}
+82 -29
View File
@@ -418,6 +418,7 @@ export class PreferencesModel {
const oldPinned = this.pinnedFeeds
this.savedFeeds = saved
this.pinnedFeeds = pinned
await this.lock.acquireAsync()
try {
const res = await cb()
runInAction(() => {
@@ -430,6 +431,8 @@ export class PreferencesModel {
this.pinnedFeeds = oldPinned
})
throw e
} finally {
this.lock.release()
}
}
@@ -441,7 +444,7 @@ export class PreferencesModel {
async addSavedFeed(v: string) {
return this._optimisticUpdateSavedFeeds(
[...this.savedFeeds, v],
[...this.savedFeeds.filter(uri => uri !== v), v],
this.pinnedFeeds,
() => this.rootStore.agent.addSavedFeed(v),
)
@@ -457,8 +460,8 @@ export class PreferencesModel {
async addPinnedFeed(v: string) {
return this._optimisticUpdateSavedFeeds(
this.savedFeeds,
[...this.pinnedFeeds, v],
[...this.savedFeeds.filter(uri => uri !== v), v],
[...this.pinnedFeeds.filter(uri => uri !== v), v],
() => this.rootStore.agent.addPinnedFeed(v),
)
}
@@ -473,71 +476,121 @@ export class PreferencesModel {
async setBirthDate(birthDate: Date) {
this.birthDate = birthDate
await this.rootStore.agent.setPersonalDetails({birthDate})
await this.lock.acquireAsync()
try {
await this.rootStore.agent.setPersonalDetails({birthDate})
} finally {
this.lock.release()
}
}
async toggleHomeFeedHideReplies() {
this.homeFeed.hideReplies = !this.homeFeed.hideReplies
await this.rootStore.agent.setFeedViewPrefs('home', {
hideReplies: this.homeFeed.hideReplies,
})
await this.lock.acquireAsync()
try {
await this.rootStore.agent.setFeedViewPrefs('home', {
hideReplies: this.homeFeed.hideReplies,
})
} finally {
this.lock.release()
}
}
async toggleHomeFeedHideRepliesByUnfollowed() {
this.homeFeed.hideRepliesByUnfollowed =
!this.homeFeed.hideRepliesByUnfollowed
await this.rootStore.agent.setFeedViewPrefs('home', {
hideRepliesByUnfollowed: this.homeFeed.hideRepliesByUnfollowed,
})
await this.lock.acquireAsync()
try {
await this.rootStore.agent.setFeedViewPrefs('home', {
hideRepliesByUnfollowed: this.homeFeed.hideRepliesByUnfollowed,
})
} finally {
this.lock.release()
}
}
async setHomeFeedHideRepliesByLikeCount(threshold: number) {
this.homeFeed.hideRepliesByLikeCount = threshold
await this.rootStore.agent.setFeedViewPrefs('home', {
hideRepliesByLikeCount: this.homeFeed.hideRepliesByLikeCount,
})
await this.lock.acquireAsync()
try {
await this.rootStore.agent.setFeedViewPrefs('home', {
hideRepliesByLikeCount: this.homeFeed.hideRepliesByLikeCount,
})
} finally {
this.lock.release()
}
}
async toggleHomeFeedHideReposts() {
this.homeFeed.hideReposts = !this.homeFeed.hideReposts
await this.rootStore.agent.setFeedViewPrefs('home', {
hideReposts: this.homeFeed.hideReposts,
})
await this.lock.acquireAsync()
try {
await this.rootStore.agent.setFeedViewPrefs('home', {
hideReposts: this.homeFeed.hideReposts,
})
} finally {
this.lock.release()
}
}
async toggleHomeFeedHideQuotePosts() {
this.homeFeed.hideQuotePosts = !this.homeFeed.hideQuotePosts
await this.rootStore.agent.setFeedViewPrefs('home', {
hideQuotePosts: this.homeFeed.hideQuotePosts,
})
await this.lock.acquireAsync()
try {
await this.rootStore.agent.setFeedViewPrefs('home', {
hideQuotePosts: this.homeFeed.hideQuotePosts,
})
} finally {
this.lock.release()
}
}
async toggleHomeFeedMergeFeedEnabled() {
this.homeFeed.lab_mergeFeedEnabled = !this.homeFeed.lab_mergeFeedEnabled
await this.rootStore.agent.setFeedViewPrefs('home', {
lab_mergeFeedEnabled: this.homeFeed.lab_mergeFeedEnabled,
})
await this.lock.acquireAsync()
try {
await this.rootStore.agent.setFeedViewPrefs('home', {
lab_mergeFeedEnabled: this.homeFeed.lab_mergeFeedEnabled,
})
} finally {
this.lock.release()
}
}
async setThreadSort(v: string) {
if (THREAD_SORT_VALUES.includes(v)) {
this.thread.sort = v
await this.rootStore.agent.setThreadViewPrefs({sort: v})
await this.lock.acquireAsync()
try {
await this.rootStore.agent.setThreadViewPrefs({sort: v})
} finally {
this.lock.release()
}
}
}
async togglePrioritizedFollowedUsers() {
this.thread.prioritizeFollowedUsers = !this.thread.prioritizeFollowedUsers
await this.rootStore.agent.setThreadViewPrefs({
prioritizeFollowedUsers: this.thread.prioritizeFollowedUsers,
})
await this.lock.acquireAsync()
try {
await this.rootStore.agent.setThreadViewPrefs({
prioritizeFollowedUsers: this.thread.prioritizeFollowedUsers,
})
} finally {
this.lock.release()
}
}
async toggleThreadTreeViewEnabled() {
this.thread.lab_treeViewEnabled = !this.thread.lab_treeViewEnabled
await this.rootStore.agent.setThreadViewPrefs({
lab_treeViewEnabled: this.thread.lab_treeViewEnabled,
})
await this.lock.acquireAsync()
try {
await this.rootStore.agent.setThreadViewPrefs({
lab_treeViewEnabled: this.thread.lab_treeViewEnabled,
})
} finally {
this.lock.release()
}
}
toggleRequireAltTextEnabled() {
+64
View File
@@ -0,0 +1,64 @@
import {makeAutoObservable} from 'mobx'
import {isObj, hasProp} from 'lib/type-guards'
import {RootStoreModel} from '../root-store'
import {toHashCode} from 'lib/strings/helpers'
const DAY = 60e3 * 24 * 1 // 1 day (ms)
export class Reminders {
lastEmailConfirm: Date = new Date()
constructor(public rootStore: RootStoreModel) {
makeAutoObservable(
this,
{serialize: false, hydrate: false},
{autoBind: true},
)
}
serialize() {
return {
lastEmailConfirm: this.lastEmailConfirm
? this.lastEmailConfirm.toISOString()
: undefined,
}
}
hydrate(v: unknown) {
if (
isObj(v) &&
hasProp(v, 'lastEmailConfirm') &&
typeof v.lastEmailConfirm === 'string'
) {
this.lastEmailConfirm = new Date(v.lastEmailConfirm)
}
}
get shouldRequestEmailConfirmation() {
const sess = this.rootStore.session.currentSession
if (!sess) {
return false
}
if (sess.emailConfirmed) {
return false
}
if (this.rootStore.onboarding.isActive) {
return false
}
const today = new Date()
// shard the users into 2 day of the week buckets
// (this is to avoid a sudden influx of email updates when
// this feature rolls out)
const code = toHashCode(sess.did) % 7
if (code !== today.getDay() && code !== (today.getDay() + 1) % 7) {
return false
}
// only ask once a day at most, but because of the bucketing
// this will be more like weekly
return Number(today) - Number(this.lastEmailConfirm) > DAY
}
setEmailConfirmationRequested() {
this.lastEmailConfirm = new Date()
}
}
+1 -1
View File
@@ -59,7 +59,7 @@ export class SearchUIModel {
} while (profilesSearch.length)
}
this.rootStore.me.follows.hydrateProfiles(profiles)
this.rootStore.me.follows.hydrateMany(profiles)
runInAction(() => {
this.profiles = profiles
+34
View File
@@ -24,6 +24,7 @@ export interface ConfirmModal {
onPressCancel?: () => void | Promise<void>
confirmBtnText?: string
confirmBtnStyle?: StyleProp<ViewStyle>
cancelBtnText?: string
}
export interface EditProfileModal {
@@ -140,6 +141,25 @@ export interface BirthDateSettingsModal {
name: 'birth-date-settings'
}
export interface VerifyEmailModal {
name: 'verify-email'
showReminder?: boolean
}
export interface ChangeEmailModal {
name: 'change-email'
}
export interface SwitchAccountModal {
name: 'switch-account'
}
export interface LinkWarningModal {
name: 'link-warning'
text: string
href: string
}
export type Modal =
// Account
| AddAppPasswordModal
@@ -148,6 +168,9 @@ export type Modal =
| EditProfileModal
| ProfilePreviewModal
| BirthDateSettingsModal
| VerifyEmailModal
| ChangeEmailModal
| SwitchAccountModal
// Curation
| ContentFilteringSettingsModal
@@ -174,6 +197,7 @@ export type Modal =
// Generic
| ConfirmModal
| LinkWarningModal
interface LightboxModel {}
@@ -250,6 +274,7 @@ export class ShellUiModel {
})
this.setupClock()
this.setupLoginModals()
}
serialize(): unknown {
@@ -375,4 +400,13 @@ export class ShellUiModel {
})
}, 60_000)
}
setupLoginModals() {
this.rootStore.onSessionReady(() => {
if (this.rootStore.reminders.shouldRequestEmailConfirmation) {
this.openModal({name: 'verify-email', showReminder: true})
this.rootStore.reminders.setEmailConfirmationRequested()
}
})
}
}