diff options
Diffstat (limited to 'src/state')
-rw-r--r-- | src/state/models/media/image.ts | 13 | ||||
-rw-r--r-- | src/state/models/root-store.ts | 6 | ||||
-rw-r--r-- | src/state/models/session.ts | 16 | ||||
-rw-r--r-- | src/state/models/ui/reminders.ts | 65 | ||||
-rw-r--r-- | src/state/models/ui/shell.ts | 22 |
5 files changed, 118 insertions, 4 deletions
diff --git a/src/state/models/media/image.ts b/src/state/models/media/image.ts index 844ecb778..10aef0ff4 100644 --- a/src/state/models/media/image.ts +++ b/src/state/models/media/image.ts @@ -8,6 +8,7 @@ import {openCropper} from 'lib/media/picker' import {ActionCrop, FlipType, SaveFormat} from 'expo-image-manipulator' import {Position} from 'react-avatar-editor' import {Dimensions} from 'lib/media/types' +import {isIOS} from 'platform/detection' export interface ImageManipulationAttributes { aspectRatio?: '4:3' | '1:1' | '3:4' | 'None' @@ -164,8 +165,13 @@ export class ImageModel implements Omit<RNImage, 'size'> { // Mobile async crop() { try { - // openCropper requires an output width and height hence - // getting upload dimensions before cropping is necessary. + // NOTE + // on ios, react-native-image-cropper gives really bad quality + // without specifying width and height. on android, however, the + // crop stretches incorrectly if you do specify it. these are + // both separate bugs in the library. we deal with that by + // providing width & height for ios only + // -prf const {width, height} = this.getUploadDimensions({ width: this.width, height: this.height, @@ -175,8 +181,7 @@ export class ImageModel implements Omit<RNImage, 'size'> { mediaType: 'photo', path: this.path, freeStyleCropEnabled: true, - width, - height, + ...(isIOS ? {width, height} : {}), }) runInAction(() => { diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts index 1a81072a2..363a81c0f 100644 --- a/src/state/models/root-store.ts +++ b/src/state/models/root-store.ts @@ -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' // TEMPORARY (APP-700) @@ -53,6 +54,7 @@ export class RootStoreModel { linkMetas = new LinkMetasCache(this) imageSizes = new ImageSizesCache() mutedThreads = new MutedThreads() + reminders = new Reminders(this) constructor(agent: BskyAgent) { this.agent = agent @@ -77,6 +79,7 @@ export class RootStoreModel { preferences: this.preferences.serialize(), invitedUsers: this.invitedUsers.serialize(), mutedThreads: this.mutedThreads.serialize(), + reminders: this.reminders.serialize(), } } @@ -109,6 +112,9 @@ export class RootStoreModel { if (hasProp(v, 'mutedThreads')) { this.mutedThreads.hydrate(v.mutedThreads) } + if (hasProp(v, 'reminders')) { + this.reminders.hydrate(v.reminders) + } } } diff --git a/src/state/models/session.ts b/src/state/models/session.ts index 1bc722c8c..7cd3c1222 100644 --- a/src/state/models/session.ts +++ b/src/state/models/session.ts @@ -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, + ) + } } diff --git a/src/state/models/ui/reminders.ts b/src/state/models/ui/reminders.ts new file mode 100644 index 000000000..f8becdec3 --- /dev/null +++ b/src/state/models/ui/reminders.ts @@ -0,0 +1,65 @@ +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 { + // NOTE + // by defaulting to the current date, we ensure that the user won't be nagged + // on first run (aka right after creating an account) + // -prf + 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 + } + 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() + } +} diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts index 647513563..15d92f927 100644 --- a/src/state/models/ui/shell.ts +++ b/src/state/models/ui/shell.ts @@ -24,6 +24,7 @@ export interface ConfirmModal { onPressCancel?: () => void | Promise<void> confirmBtnText?: string confirmBtnStyle?: StyleProp<ViewStyle> + cancelBtnText?: string } export interface EditProfileModal { @@ -140,6 +141,15 @@ export interface BirthDateSettingsModal { name: 'birth-date-settings' } +export interface VerifyEmailModal { + name: 'verify-email' + showReminder?: boolean +} + +export interface ChangeEmailModal { + name: 'change-email' +} + export type Modal = // Account | AddAppPasswordModal @@ -148,6 +158,8 @@ export type Modal = | EditProfileModal | ProfilePreviewModal | BirthDateSettingsModal + | VerifyEmailModal + | ChangeEmailModal // Curation | ContentFilteringSettingsModal @@ -250,6 +262,7 @@ export class ShellUiModel { }) this.setupClock() + this.setupLoginModals() } serialize(): unknown { @@ -375,4 +388,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() + } + }) + } } |