Merge branch 'main' into internationalization

This commit is contained in:
Ansh Nanda
2023-11-08 14:12:36 -08:00
108 changed files with 1448 additions and 1337 deletions
+13 -1
View File
@@ -22,6 +22,10 @@ import * as Toast from 'view/com/util/Toast'
import {queryClient} from 'lib/react-query' import {queryClient} from 'lib/react-query'
import {TestCtrls} from 'view/com/testing/TestCtrls' import {TestCtrls} from 'view/com/testing/TestCtrls'
import {Provider as ShellStateProvider} from 'state/shell' import {Provider as ShellStateProvider} from 'state/shell'
import {Provider as ModalStateProvider} from 'state/modals'
import {Provider as MutedThreadsProvider} from 'state/muted-threads'
import {Provider as InvitesStateProvider} from 'state/invites'
import {Provider as PrefsStateProvider} from 'state/preferences'
SplashScreen.preventAutoHideAsync() SplashScreen.preventAutoHideAsync()
@@ -78,7 +82,15 @@ function App() {
return ( return (
<ShellStateProvider> <ShellStateProvider>
<InnerApp /> <PrefsStateProvider>
<MutedThreadsProvider>
<InvitesStateProvider>
<ModalStateProvider>
<InnerApp />
</ModalStateProvider>
</InvitesStateProvider>
</MutedThreadsProvider>
</PrefsStateProvider>
</ShellStateProvider> </ShellStateProvider>
) )
} }
+13 -1
View File
@@ -20,6 +20,10 @@ import {i18n} from '@lingui/core'
import {I18nProvider} from '@lingui/react' import {I18nProvider} from '@lingui/react'
import {defaultLocale, dynamicActivate} from './locale/i18n' import {defaultLocale, dynamicActivate} from './locale/i18n'
import {Provider as ShellStateProvider} from 'state/shell' import {Provider as ShellStateProvider} from 'state/shell'
import {Provider as ModalStateProvider} from 'state/modals'
import {Provider as MutedThreadsProvider} from 'state/muted-threads'
import {Provider as InvitesStateProvider} from 'state/invites'
import {Provider as PrefsStateProvider} from 'state/preferences'
const InnerApp = observer(function AppImpl() { const InnerApp = observer(function AppImpl() {
const colorMode = useColorMode() const colorMode = useColorMode()
@@ -74,7 +78,15 @@ function App() {
return ( return (
<ShellStateProvider> <ShellStateProvider>
<InnerApp /> <PrefsStateProvider>
<MutedThreadsProvider>
<InvitesStateProvider>
<ModalStateProvider>
<InnerApp />
</ModalStateProvider>
</InvitesStateProvider>
</MutedThreadsProvider>
</PrefsStateProvider>
</ShellStateProvider> </ShellStateProvider>
) )
} }
+4 -1
View File
@@ -7,6 +7,7 @@ import {AccountData} from 'state/models/session'
import {reset as resetNavigation} from '../../Navigation' import {reset as resetNavigation} from '../../Navigation'
import * as Toast from 'view/com/util/Toast' import * as Toast from 'view/com/util/Toast'
import {useSetDrawerOpen} from '#/state/shell/drawer-open' import {useSetDrawerOpen} from '#/state/shell/drawer-open'
import {useModalControls} from '#/state/modals'
export function useAccountSwitcher(): [ export function useAccountSwitcher(): [
boolean, boolean,
@@ -16,6 +17,7 @@ export function useAccountSwitcher(): [
const {track} = useAnalytics() const {track} = useAnalytics()
const store = useStores() const store = useStores()
const setDrawerOpen = useSetDrawerOpen() const setDrawerOpen = useSetDrawerOpen()
const {closeModal} = useModalControls()
const [isSwitching, setIsSwitching] = useState(false) const [isSwitching, setIsSwitching] = useState(false)
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
@@ -25,6 +27,7 @@ export function useAccountSwitcher(): [
setIsSwitching(true) setIsSwitching(true)
const success = await store.session.resumeSession(acct) const success = await store.session.resumeSession(acct)
setDrawerOpen(false) setDrawerOpen(false)
closeModal()
store.shell.closeAllActiveElements() store.shell.closeAllActiveElements()
if (success) { if (success) {
resetNavigation() resetNavigation()
@@ -36,7 +39,7 @@ export function useAccountSwitcher(): [
store.session.clear() store.session.clear()
} }
}, },
[track, setIsSwitching, navigation, store, setDrawerOpen], [track, setIsSwitching, navigation, store, setDrawerOpen, closeModal],
) )
return [isSwitching, setIsSwitching, onPressSwitchAccount] return [isSwitching, setIsSwitching, onPressSwitchAccount]
+4 -4
View File
@@ -1,15 +1,15 @@
import * as Updates from 'expo-updates' import * as Updates from 'expo-updates'
import {useCallback, useEffect} from 'react' import {useCallback, useEffect} from 'react'
import {AppState} from 'react-native' import {AppState} from 'react-native'
import {useStores} from 'state/index'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useModalControls} from '#/state/modals'
export function useOTAUpdate() { export function useOTAUpdate() {
const store = useStores() const {openModal} = useModalControls()
// HELPER FUNCTIONS // HELPER FUNCTIONS
const showUpdatePopup = useCallback(() => { const showUpdatePopup = useCallback(() => {
store.shell.openModal({ openModal({
name: 'confirm', name: 'confirm',
title: 'Update Available', title: 'Update Available',
message: message:
@@ -20,7 +20,7 @@ export function useOTAUpdate() {
}) })
}, },
}) })
}, [store.shell]) }, [openModal])
const checkForUpdate = useCallback(async () => { const checkForUpdate = useCallback(async () => {
logger.debug('useOTAUpdate: Checking for update...') logger.debug('useOTAUpdate: Checking for update...')
try { try {
-12
View File
@@ -1,12 +0,0 @@
import {RootStoreModel} from 'state/index'
import {ImageModel} from 'state/models/media/image'
export async function openAltTextModal(
store: RootStoreModel,
image: ImageModel,
) {
store.shell.openModal({
name: 'alt-text-image',
image,
})
}
+3 -2
View File
@@ -4,6 +4,7 @@ import {CameraOpts, CropperOptions} from './types'
import {RootStoreModel} from 'state/index' import {RootStoreModel} from 'state/index'
import {Image as RNImage} from 'react-native-image-crop-picker' import {Image as RNImage} from 'react-native-image-crop-picker'
export {openPicker} from './picker.shared' export {openPicker} from './picker.shared'
import {unstable__openModal} from '#/state/modals'
export async function openCamera( export async function openCamera(
_store: RootStoreModel, _store: RootStoreModel,
@@ -14,12 +15,12 @@ export async function openCamera(
} }
export async function openCropper( export async function openCropper(
store: RootStoreModel, _store: RootStoreModel,
opts: CropperOptions, opts: CropperOptions,
): Promise<RNImage> { ): Promise<RNImage> {
// TODO handle more opts // TODO handle more opts
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
store.shell.openModal({ unstable__openModal({
name: 'crop-image', name: 'crop-image',
uri: opts.path, uri: opts.path,
onSelect: (img?: RNImage) => { onSelect: (img?: RNImage) => {
+56
View File
@@ -0,0 +1,56 @@
import React from 'react'
import * as persisted from '#/state/persisted'
type StateContext = persisted.Schema['invites']
type ApiContext = {
setInviteCopied: (code: string) => void
}
const stateContext = React.createContext<StateContext>(
persisted.defaults.invites,
)
const apiContext = React.createContext<ApiContext>({
setInviteCopied(_: string) {},
})
export function Provider({children}: React.PropsWithChildren<{}>) {
const [state, setState] = React.useState(persisted.get('invites'))
const api = React.useMemo(
() => ({
setInviteCopied(code: string) {
setState(state => {
state = {
...state,
copiedInvites: state.copiedInvites.includes(code)
? state.copiedInvites
: state.copiedInvites.concat([code]),
}
persisted.write('invites', state)
return state
})
},
}),
[setState],
)
React.useEffect(() => {
return persisted.onUpdate(() => {
setState(persisted.get('invites'))
})
}, [setState])
return (
<stateContext.Provider value={state}>
<apiContext.Provider value={api}>{children}</apiContext.Provider>
</stateContext.Provider>
)
}
export function useInvitesState() {
return React.useContext(stateContext)
}
export function useInvitesAPI() {
return React.useContext(apiContext)
}
+284
View File
@@ -0,0 +1,284 @@
import React from 'react'
import {AppBskyActorDefs, ModerationUI} from '@atproto/api'
import {StyleProp, ViewStyle, DeviceEventEmitter} from 'react-native'
import {Image as RNImage} from 'react-native-image-crop-picker'
import {ProfileModel} from '#/state/models/content/profile'
import {ImageModel} from '#/state/models/media/image'
import {ListModel} from '#/state/models/content/list'
import {GalleryModel} from '#/state/models/media/gallery'
export interface ConfirmModal {
name: 'confirm'
title: string
message: string | (() => JSX.Element)
onPressConfirm: () => void | Promise<void>
onPressCancel?: () => void | Promise<void>
confirmBtnText?: string
confirmBtnStyle?: StyleProp<ViewStyle>
cancelBtnText?: string
}
export interface EditProfileModal {
name: 'edit-profile'
profileView: ProfileModel
onUpdate?: () => void
}
export interface ProfilePreviewModal {
name: 'profile-preview'
did: string
}
export interface ServerInputModal {
name: 'server-input'
initialService: string
onSelect: (url: string) => void
}
export interface ModerationDetailsModal {
name: 'moderation-details'
context: 'account' | 'content'
moderation: ModerationUI
}
export type ReportModal = {
name: 'report'
} & (
| {
uri: string
cid: string
}
| {did: string}
)
export interface CreateOrEditListModal {
name: 'create-or-edit-list'
purpose?: string
list?: ListModel
onSave?: (uri: string) => void
}
export interface UserAddRemoveListsModal {
name: 'user-add-remove-lists'
subject: string
displayName: string
onAdd?: (listUri: string) => void
onRemove?: (listUri: string) => void
}
export interface ListAddUserModal {
name: 'list-add-user'
list: ListModel
onAdd?: (profile: AppBskyActorDefs.ProfileViewBasic) => void
}
export interface EditImageModal {
name: 'edit-image'
image: ImageModel
gallery: GalleryModel
}
export interface CropImageModal {
name: 'crop-image'
uri: string
onSelect: (img?: RNImage) => void
}
export interface AltTextImageModal {
name: 'alt-text-image'
image: ImageModel
}
export interface DeleteAccountModal {
name: 'delete-account'
}
export interface RepostModal {
name: 'repost'
onRepost: () => void
onQuote: () => void
isReposted: boolean
}
export interface SelfLabelModal {
name: 'self-label'
labels: string[]
hasMedia: boolean
onChange: (labels: string[]) => void
}
export interface ChangeHandleModal {
name: 'change-handle'
onChanged: () => void
}
export interface WaitlistModal {
name: 'waitlist'
}
export interface InviteCodesModal {
name: 'invite-codes'
}
export interface AddAppPasswordModal {
name: 'add-app-password'
}
export interface ContentFilteringSettingsModal {
name: 'content-filtering-settings'
}
export interface ContentLanguagesSettingsModal {
name: 'content-languages-settings'
}
export interface PostLanguagesSettingsModal {
name: 'post-languages-settings'
}
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
| ChangeHandleModal
| DeleteAccountModal
| EditProfileModal
| ProfilePreviewModal
| BirthDateSettingsModal
| VerifyEmailModal
| ChangeEmailModal
| SwitchAccountModal
// Curation
| ContentFilteringSettingsModal
| ContentLanguagesSettingsModal
| PostLanguagesSettingsModal
// Moderation
| ModerationDetailsModal
| ReportModal
// Lists
| CreateOrEditListModal
| UserAddRemoveListsModal
| ListAddUserModal
// Posts
| AltTextImageModal
| CropImageModal
| EditImageModal
| ServerInputModal
| RepostModal
| SelfLabelModal
// Bluesky access
| WaitlistModal
| InviteCodesModal
// Generic
| ConfirmModal
| LinkWarningModal
const ModalContext = React.createContext<{
isModalActive: boolean
activeModals: Modal[]
}>({
isModalActive: false,
activeModals: [],
})
const ModalControlContext = React.createContext<{
openModal: (modal: Modal) => void
closeModal: () => void
}>({
openModal: () => {},
closeModal: () => {},
})
/**
* @deprecated DO NOT USE THIS unless you have no other choice.
*/
export let unstable__openModal: (modal: Modal) => void = () => {
throw new Error(`ModalContext is not initialized`)
}
export function Provider({children}: React.PropsWithChildren<{}>) {
const [isModalActive, setIsModalActive] = React.useState(false)
const [activeModals, setActiveModals] = React.useState<Modal[]>([])
const openModal = React.useCallback(
(modal: Modal) => {
DeviceEventEmitter.emit('navigation')
setActiveModals(activeModals => [...activeModals, modal])
setIsModalActive(true)
},
[setIsModalActive, setActiveModals],
)
unstable__openModal = openModal
const closeModal = React.useCallback(() => {
let totalActiveModals = 0
setActiveModals(activeModals => {
activeModals.pop()
totalActiveModals = activeModals.length
return activeModals
})
setIsModalActive(totalActiveModals > 0)
}, [setIsModalActive, setActiveModals])
const state = React.useMemo(
() => ({
isModalActive,
activeModals,
}),
[isModalActive, activeModals],
)
const methods = React.useMemo(
() => ({
openModal,
closeModal,
}),
[openModal, closeModal],
)
return (
<ModalContext.Provider value={state}>
<ModalControlContext.Provider value={methods}>
{children}
</ModalControlContext.Provider>
</ModalContext.Provider>
)
}
export function useModals() {
return React.useContext(ModalContext)
}
export function useModalControls() {
return React.useContext(ModalControlContext)
}
@@ -63,10 +63,6 @@ export class PostThreadItemModel {
return this.post.uri return this.post.uri
} }
get isThreadMuted() {
return this.data.isThreadMuted
}
get moderation(): PostModeration { get moderation(): PostModeration {
return this.data.moderation return this.data.moderation
} }
@@ -129,10 +125,6 @@ export class PostThreadItemModel {
this.data.toggleRepost() this.data.toggleRepost()
} }
async toggleThreadMute() {
this.data.toggleThreadMute()
}
async delete() { async delete() {
this.data.delete() this.data.delete()
} }
-12
View File
@@ -74,10 +74,6 @@ export class PostThreadModel {
return this.resolvedUri return this.resolvedUri
} }
get isThreadMuted() {
return this.rootStore.mutedThreads.uris.has(this.rootUri)
}
get isCachedPostAReply() { get isCachedPostAReply() {
if (AppBskyFeedPost.isRecord(this.thread?.post.record)) { if (AppBskyFeedPost.isRecord(this.thread?.post.record)) {
return !!this.thread?.post.record.reply return !!this.thread?.post.record.reply
@@ -140,14 +136,6 @@ export class PostThreadModel {
this.refresh() this.refresh()
} }
async toggleThreadMute() {
if (this.isThreadMuted) {
this.rootStore.mutedThreads.uris.delete(this.rootUri)
} else {
this.rootStore.mutedThreads.uris.add(this.rootUri)
}
}
// state transitions // state transitions
// = // =
-106
View File
@@ -1,106 +0,0 @@
import {makeAutoObservable} from 'mobx'
import {RootStoreModel} from '../root-store'
import {hasProp} from 'lib/type-guards'
import {track} from 'lib/analytics/analytics'
import {SuggestedActorsModel} from './suggested-actors'
export const OnboardingScreenSteps = {
Welcome: 'Welcome',
RecommendedFeeds: 'RecommendedFeeds',
RecommendedFollows: 'RecommendedFollows',
Home: 'Home',
} as const
type OnboardingStep =
(typeof OnboardingScreenSteps)[keyof typeof OnboardingScreenSteps]
const OnboardingStepsArray = Object.values(OnboardingScreenSteps)
export class OnboardingModel {
// state
step: OnboardingStep = 'Home' // default state to skip onboarding, only enabled for new users by calling start()
// data
suggestedActors: SuggestedActorsModel
constructor(public rootStore: RootStoreModel) {
this.suggestedActors = new SuggestedActorsModel(this.rootStore)
makeAutoObservable(this, {
rootStore: false,
hydrate: false,
serialize: false,
})
}
serialize(): unknown {
return {
step: this.step,
}
}
hydrate(v: unknown) {
if (typeof v === 'object' && v !== null) {
if (
hasProp(v, 'step') &&
typeof v.step === 'string' &&
OnboardingStepsArray.includes(v.step as OnboardingStep)
) {
this.step = v.step as OnboardingStep
}
} else {
// if there is no valid state, we'll just reset
this.reset()
}
}
/**
* Returns the name of the next screen in the onboarding process based on the current step or screen name provided.
* @param {OnboardingStep} [currentScreenName]
* @returns name of next screen in the onboarding process
*/
next(currentScreenName?: OnboardingStep) {
currentScreenName = currentScreenName || this.step
if (currentScreenName === 'Welcome') {
this.step = 'RecommendedFeeds'
return this.step
} else if (this.step === 'RecommendedFeeds') {
this.step = 'RecommendedFollows'
// prefetch recommended follows
this.suggestedActors.loadMore(true)
return this.step
} else if (this.step === 'RecommendedFollows') {
this.finish()
return this.step
} else {
// if we get here, we're in an invalid state, let's just go Home
return 'Home'
}
}
start() {
this.step = 'Welcome'
track('Onboarding:Begin')
}
finish() {
this.rootStore.me.mainFeed.refresh() // load the selected content
this.step = 'Home'
track('Onboarding:Complete')
}
reset() {
this.step = 'Welcome'
track('Onboarding:Reset')
}
skip() {
this.step = 'Home'
track('Onboarding:Skipped')
}
get isComplete() {
return this.step === 'Home'
}
get isActive() {
return !this.isComplete
}
}
+3 -3
View File
@@ -18,6 +18,7 @@ import {RootStoreModel} from '../root-store'
import {PostThreadModel} from '../content/post-thread' import {PostThreadModel} from '../content/post-thread'
import {cleanError} from 'lib/strings/errors' import {cleanError} from 'lib/strings/errors'
import {logger} from '#/logger' import {logger} from '#/logger'
import {isThreadMuted} from '#/state/muted-threads'
const GROUPABLE_REASONS = ['like', 'repost', 'follow'] const GROUPABLE_REASONS = ['like', 'repost', 'follow']
const PAGE_SIZE = 30 const PAGE_SIZE = 30
@@ -303,7 +304,7 @@ export class NotificationsFeedModel {
} }
get unreadCountLabel(): string { get unreadCountLabel(): string {
const count = this.unreadCount + this.rootStore.invitedUsers.numNotifs const count = this.unreadCount
if (count >= MAX_VISIBLE_NOTIFS) { if (count >= MAX_VISIBLE_NOTIFS) {
return `${MAX_VISIBLE_NOTIFS}+` return `${MAX_VISIBLE_NOTIFS}+`
} }
@@ -550,8 +551,7 @@ export class NotificationsFeedModel {
.filter(item => { .filter(item => {
const hideByLabel = item.shouldFilter const hideByLabel = item.shouldFilter
let mutedThread = !!( let mutedThread = !!(
item.reasonSubjectRootUri && item.reasonSubjectRootUri && isThreadMuted(item.reasonSubjectRootUri)
this.rootStore.mutedThreads.uris.has(item.reasonSubjectRootUri)
) )
return !hideByLabel && !mutedThread return !hideByLabel && !mutedThread
}) })
-18
View File
@@ -75,10 +75,6 @@ export class PostsFeedItemModel {
return this.post.uri return this.post.uri
} }
get isThreadMuted() {
return this.rootStore.mutedThreads.uris.has(this.rootUri)
}
get moderation(): PostModeration { get moderation(): PostModeration {
return moderatePost(this.post, this.rootStore.preferences.moderationOpts) return moderatePost(this.post, this.rootStore.preferences.moderationOpts)
} }
@@ -172,20 +168,6 @@ export class PostsFeedItemModel {
} }
} }
async toggleThreadMute() {
try {
if (this.isThreadMuted) {
this.rootStore.mutedThreads.uris.delete(this.rootUri)
track('Post:ThreadUnmute')
} else {
this.rootStore.mutedThreads.uris.add(this.rootUri)
track('Post:ThreadMute')
}
} catch (error) {
logger.error('Failed to toggle thread mute', {error})
}
}
async delete() { async delete() {
try { try {
await this.rootStore.agent.deletePost(this.post.uri) await this.rootStore.agent.deletePost(this.post.uri)
-88
View File
@@ -1,88 +0,0 @@
import {makeAutoObservable, runInAction} from 'mobx'
import {ComAtprotoServerDefs, AppBskyActorDefs} from '@atproto/api'
import {RootStoreModel} from './root-store'
import {isObj, hasProp, isStrArray} from 'lib/type-guards'
import {logger} from '#/logger'
export class InvitedUsers {
copiedInvites: string[] = []
seenDids: string[] = []
profiles: AppBskyActorDefs.ProfileViewDetailed[] = []
get numNotifs() {
return this.profiles.length
}
constructor(public rootStore: RootStoreModel) {
makeAutoObservable(
this,
{rootStore: false, serialize: false, hydrate: false},
{autoBind: true},
)
}
serialize() {
return {seenDids: this.seenDids, copiedInvites: this.copiedInvites}
}
hydrate(v: unknown) {
if (isObj(v) && hasProp(v, 'seenDids') && isStrArray(v.seenDids)) {
this.seenDids = v.seenDids
}
if (
isObj(v) &&
hasProp(v, 'copiedInvites') &&
isStrArray(v.copiedInvites)
) {
this.copiedInvites = v.copiedInvites
}
}
async fetch(invites: ComAtprotoServerDefs.InviteCode[]) {
// pull the dids of invited users not marked seen
const dids = []
for (const invite of invites) {
for (const use of invite.uses) {
if (!this.seenDids.includes(use.usedBy)) {
dids.push(use.usedBy)
}
}
}
// fetch their profiles
this.profiles = []
if (dids.length) {
try {
const res = await this.rootStore.agent.app.bsky.actor.getProfiles({
actors: dids,
})
runInAction(() => {
// save the ones following -- these are the ones we want to notify the user about
this.profiles = res.data.profiles.filter(
profile => !profile.viewer?.following,
)
})
this.rootStore.me.follows.hydrateMany(this.profiles)
} catch (e) {
logger.error('Failed to fetch profiles for invited users', {
error: e,
})
}
}
}
isInviteCopied(invite: string) {
return this.copiedInvites.includes(invite)
}
setInviteCopied(invite: string) {
if (!this.isInviteCopied(invite)) {
this.copiedInvites.push(invite)
}
}
markSeen(did: string) {
this.seenDids.push(did)
this.profiles = this.profiles.filter(profile => profile.did !== did)
}
}
-1
View File
@@ -193,7 +193,6 @@ export class MeModel {
error: e, error: e,
}) })
} }
await this.rootStore.invitedUsers.fetch(this.invites)
} }
} }
-13
View File
@@ -4,7 +4,6 @@ import {ImageModel} from './image'
import {Image as RNImage} from 'react-native-image-crop-picker' import {Image as RNImage} from 'react-native-image-crop-picker'
import {openPicker} from 'lib/media/picker' import {openPicker} from 'lib/media/picker'
import {getImageDim} from 'lib/media/manip' import {getImageDim} from 'lib/media/manip'
import {isNative} from 'platform/detection'
export class GalleryModel { export class GalleryModel {
images: ImageModel[] = [] images: ImageModel[] = []
@@ -42,18 +41,6 @@ export class GalleryModel {
} }
} }
async edit(image: ImageModel) {
if (isNative) {
this.crop(image)
} else {
this.rootStore.shell.openModal({
name: 'edit-image',
image,
gallery: this,
})
}
}
async paste(uri: string) { async paste(uri: string) {
if (this.size >= 4) { if (this.size >= 4) {
return return
-29
View File
@@ -1,29 +0,0 @@
/**
* This is a temporary client-side system for storing muted threads
* When the system lands on prod we should switch to that
*/
import {makeAutoObservable} from 'mobx'
import {isObj, hasProp, isStrArray} from 'lib/type-guards'
export class MutedThreads {
uris: Set<string> = new Set()
constructor() {
makeAutoObservable(
this,
{serialize: false, hydrate: false},
{autoBind: true},
)
}
serialize() {
return {uris: Array.from(this.uris)}
}
hydrate(v: unknown) {
if (isObj(v) && hasProp(v, 'uris') && isStrArray(v.uris)) {
this.uris = new Set(v.uris)
}
}
}
-24
View File
@@ -15,12 +15,9 @@ import {ProfilesCache} from './cache/profiles-view'
import {PostsCache} from './cache/posts' import {PostsCache} from './cache/posts'
import {LinkMetasCache} from './cache/link-metas' import {LinkMetasCache} from './cache/link-metas'
import {MeModel} from './me' import {MeModel} from './me'
import {InvitedUsers} from './invited-users'
import {PreferencesModel} from './ui/preferences' import {PreferencesModel} from './ui/preferences'
import {resetToTab} from '../../Navigation' import {resetToTab} from '../../Navigation'
import {ImageSizesCache} from './cache/image-sizes' import {ImageSizesCache} from './cache/image-sizes'
import {MutedThreads} from './muted-threads'
import {Reminders} from './ui/reminders'
import {reset as resetNavigation} from '../../Navigation' import {reset as resetNavigation} from '../../Navigation'
import {logger} from '#/logger' import {logger} from '#/logger'
@@ -28,7 +25,6 @@ import {logger} from '#/logger'
// remove after backend testing finishes // remove after backend testing finishes
// -prf // -prf
import {applyDebugHeader} from 'lib/api/debug-appview-proxy-header' import {applyDebugHeader} from 'lib/api/debug-appview-proxy-header'
import {OnboardingModel} from './discovery/onboarding'
export const appInfo = z.object({ export const appInfo = z.object({
build: z.string(), build: z.string(),
@@ -45,15 +41,11 @@ export class RootStoreModel {
shell = new ShellUiModel(this) shell = new ShellUiModel(this)
preferences = new PreferencesModel(this) preferences = new PreferencesModel(this)
me = new MeModel(this) me = new MeModel(this)
onboarding = new OnboardingModel(this)
invitedUsers = new InvitedUsers(this)
handleResolutions = new HandleResolutionsCache() handleResolutions = new HandleResolutionsCache()
profiles = new ProfilesCache(this) profiles = new ProfilesCache(this)
posts = new PostsCache(this) posts = new PostsCache(this)
linkMetas = new LinkMetasCache(this) linkMetas = new LinkMetasCache(this)
imageSizes = new ImageSizesCache() imageSizes = new ImageSizesCache()
mutedThreads = new MutedThreads()
reminders = new Reminders(this)
constructor(agent: BskyAgent) { constructor(agent: BskyAgent) {
this.agent = agent this.agent = agent
@@ -73,11 +65,7 @@ export class RootStoreModel {
appInfo: this.appInfo, appInfo: this.appInfo,
session: this.session.serialize(), session: this.session.serialize(),
me: this.me.serialize(), me: this.me.serialize(),
onboarding: this.onboarding.serialize(),
preferences: this.preferences.serialize(), preferences: this.preferences.serialize(),
invitedUsers: this.invitedUsers.serialize(),
mutedThreads: this.mutedThreads.serialize(),
reminders: this.reminders.serialize(),
} }
} }
@@ -92,24 +80,12 @@ export class RootStoreModel {
if (hasProp(v, 'me')) { if (hasProp(v, 'me')) {
this.me.hydrate(v.me) this.me.hydrate(v.me)
} }
if (hasProp(v, 'onboarding')) {
this.onboarding.hydrate(v.onboarding)
}
if (hasProp(v, 'session')) { if (hasProp(v, 'session')) {
this.session.hydrate(v.session) this.session.hydrate(v.session)
} }
if (hasProp(v, 'preferences')) { if (hasProp(v, 'preferences')) {
this.preferences.hydrate(v.preferences) this.preferences.hydrate(v.preferences)
} }
if (hasProp(v, 'invitedUsers')) {
this.invitedUsers.hydrate(v.invitedUsers)
}
if (hasProp(v, 'mutedThreads')) {
this.mutedThreads.hydrate(v.mutedThreads)
}
if (hasProp(v, 'reminders')) {
this.reminders.hydrate(v.reminders)
}
} }
} }
+4 -3
View File
@@ -9,6 +9,7 @@ import {cleanError} from 'lib/strings/errors'
import {getAge} from 'lib/strings/time' import {getAge} from 'lib/strings/time'
import {track} from 'lib/analytics/analytics' import {track} from 'lib/analytics/analytics'
import {logger} from '#/logger' import {logger} from '#/logger'
import {DispatchContext as OnboardingDispatchContext} from '#/state/shell/onboarding'
const DEFAULT_DATE = new Date(Date.now() - 60e3 * 60 * 24 * 365 * 20) // default to 20 years ago const DEFAULT_DATE = new Date(Date.now() - 60e3 * 60 * 24 * 365 * 20) // default to 20 years ago
@@ -90,7 +91,7 @@ export class CreateAccountModel {
} }
} }
async submit() { async submit(onboardingDispatch: OnboardingDispatchContext) {
if (!this.email) { if (!this.email) {
this.setStep(2) this.setStep(2)
return this.setError('Please enter your email.') return this.setError('Please enter your email.')
@@ -111,7 +112,7 @@ export class CreateAccountModel {
this.setIsProcessing(true) this.setIsProcessing(true)
try { try {
this.rootStore.onboarding.start() // start now to avoid flashing the wrong view onboardingDispatch({type: 'start'}) // start now to avoid flashing the wrong view
await this.rootStore.session.createAccount({ await this.rootStore.session.createAccount({
service: this.serviceUrl, service: this.serviceUrl,
email: this.email, email: this.email,
@@ -122,7 +123,7 @@ export class CreateAccountModel {
/* dont await */ this.rootStore.preferences.setBirthDate(this.birthDate) /* dont await */ this.rootStore.preferences.setBirthDate(this.birthDate)
track('Create Account') track('Create Account')
} catch (e: any) { } catch (e: any) {
this.rootStore.onboarding.skip() // undo starting the onboard onboardingDispatch({type: 'skip'}) // undo starting the onboard
let errMsg = e.toString() let errMsg = e.toString()
if (e instanceof ComAtprotoServerCreateAccount.InvalidInviteCodeError) { if (e instanceof ComAtprotoServerCreateAccount.InvalidInviteCodeError) {
errMsg = errMsg =
+2 -147
View File
@@ -10,11 +10,10 @@ import {isObj, hasProp} from 'lib/type-guards'
import {RootStoreModel} from '../root-store' import {RootStoreModel} from '../root-store'
import {ModerationOpts} from '@atproto/api' import {ModerationOpts} from '@atproto/api'
import {DEFAULT_FEEDS} from 'lib/constants' import {DEFAULT_FEEDS} from 'lib/constants'
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 {FeedTuner} from 'lib/api/feed-manip'
import {LANGUAGES} from '../../../locale/languages'
import {logger} from '#/logger' import {logger} from '#/logger'
import {getContentLanguages} from '#/state/preferences/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
export type LabelPreference = APILabelPreference | 'show' export type LabelPreference = APILabelPreference | 'show'
@@ -34,9 +33,6 @@ const LABEL_GROUPS = [
'impersonation', 'impersonation',
] ]
const VISIBILITY_VALUES = ['ignore', 'warn', 'hide'] const VISIBILITY_VALUES = ['ignore', 'warn', 'hide']
const DEFAULT_LANG_CODES = (deviceLocales || [])
.concat(['en', 'ja', 'pt', 'de'])
.slice(0, 6)
const THREAD_SORT_VALUES = ['oldest', 'newest', 'most-likes', 'random'] const THREAD_SORT_VALUES = ['oldest', 'newest', 'most-likes', 'random']
interface LegacyPreferences { interface LegacyPreferences {
@@ -62,10 +58,6 @@ export class LabelPreferencesModel {
export class PreferencesModel { export class PreferencesModel {
adultContentEnabled = false adultContentEnabled = false
primaryLanguage: string = deviceLocales[0] || 'en'
contentLanguages: string[] = deviceLocales || []
postLanguage: string = deviceLocales[0] || 'en'
postLanguageHistory: string[] = DEFAULT_LANG_CODES
contentLabels = new LabelPreferencesModel() contentLabels = new LabelPreferencesModel()
savedFeeds: string[] = [] savedFeeds: string[] = []
pinnedFeeds: string[] = [] pinnedFeeds: string[] = []
@@ -83,7 +75,6 @@ export class PreferencesModel {
prioritizeFollowedUsers: true, prioritizeFollowedUsers: true,
lab_treeViewEnabled: false, // experimental lab_treeViewEnabled: false, // experimental
} }
requireAltTextEnabled: boolean = false
// used to help with transitions from device-stored to server-stored preferences // used to help with transitions from device-stored to server-stored preferences
legacyPreferences: LegacyPreferences | undefined legacyPreferences: LegacyPreferences | undefined
@@ -104,14 +95,9 @@ export class PreferencesModel {
serialize() { serialize() {
return { return {
primaryLanguage: this.primaryLanguage,
contentLanguages: this.contentLanguages,
postLanguage: this.postLanguage,
postLanguageHistory: this.postLanguageHistory,
contentLabels: this.contentLabels, contentLabels: this.contentLabels,
savedFeeds: this.savedFeeds, savedFeeds: this.savedFeeds,
pinnedFeeds: this.pinnedFeeds, pinnedFeeds: this.pinnedFeeds,
requireAltTextEnabled: this.requireAltTextEnabled,
} }
} }
@@ -122,44 +108,6 @@ export class PreferencesModel {
*/ */
hydrate(v: unknown) { hydrate(v: unknown) {
if (isObj(v)) { if (isObj(v)) {
if (
hasProp(v, 'primaryLanguage') &&
typeof v.primaryLanguage === 'string'
) {
this.primaryLanguage = v.primaryLanguage
} else {
// default to the device languages
this.primaryLanguage = deviceLocales[0] || 'en'
}
// check if content languages in preferences exist, otherwise default to device languages
if (
hasProp(v, 'contentLanguages') &&
Array.isArray(v.contentLanguages) &&
typeof v.contentLanguages.every(item => typeof item === 'string')
) {
this.contentLanguages = v.contentLanguages
} else {
// default to the device languages
this.contentLanguages = deviceLocales
}
if (hasProp(v, 'postLanguage') && typeof v.postLanguage === 'string') {
this.postLanguage = v.postLanguage
} else {
// default to the device languages
this.postLanguage = deviceLocales[0] || 'en'
}
if (
hasProp(v, 'postLanguageHistory') &&
Array.isArray(v.postLanguageHistory) &&
typeof v.postLanguageHistory.every(item => typeof item === 'string')
) {
this.postLanguageHistory = v.postLanguageHistory
.concat(DEFAULT_LANG_CODES)
.slice(0, 6)
} else {
// default to a starter set
this.postLanguageHistory = DEFAULT_LANG_CODES
}
// check if content labels in preferences exist, then hydrate // check if content labels in preferences exist, then hydrate
if (hasProp(v, 'contentLabels') && typeof v.contentLabels === 'object') { if (hasProp(v, 'contentLabels') && typeof v.contentLabels === 'object') {
Object.assign(this.contentLabels, v.contentLabels) Object.assign(this.contentLabels, v.contentLabels)
@@ -180,13 +128,6 @@ export class PreferencesModel {
) { ) {
this.pinnedFeeds = v.pinnedFeeds this.pinnedFeeds = v.pinnedFeeds
} }
// check if requiring alt text is enabled in preferences, then hydrate
if (
hasProp(v, 'requireAltTextEnabled') &&
typeof v.requireAltTextEnabled === 'boolean'
) {
this.requireAltTextEnabled = v.requireAltTextEnabled
}
// grab legacy values // grab legacy values
this.legacyPreferences = getLegacyPreferences(v) this.legacyPreferences = getLegacyPreferences(v)
} }
@@ -271,9 +212,6 @@ export class PreferencesModel {
try { try {
runInAction(() => { runInAction(() => {
this.contentLabels = new LabelPreferencesModel() this.contentLabels = new LabelPreferencesModel()
this.contentLanguages = deviceLocales
this.postLanguage = deviceLocales ? deviceLocales.join(',') : 'en'
this.postLanguageHistory = DEFAULT_LANG_CODES
this.savedFeeds = [] this.savedFeeds = []
this.pinnedFeeds = [] this.pinnedFeeds = []
}) })
@@ -285,81 +223,6 @@ export class PreferencesModel {
} }
} }
// languages
// =
hasContentLanguage(code2: string) {
return this.contentLanguages.includes(code2)
}
toggleContentLanguage(code2: string) {
if (this.hasContentLanguage(code2)) {
this.contentLanguages = this.contentLanguages.filter(
lang => lang !== code2,
)
} else {
this.contentLanguages = this.contentLanguages.concat([code2])
}
}
/**
* A getter that splits `this.postLanguage` into an array of strings.
*
* This was previously the main field on this model, but now we're
* concatenating lang codes to make multi-selection a little better.
*/
get postLanguages() {
// filter out empty strings if exist
return this.postLanguage.split(',').filter(Boolean)
}
hasPostLanguage(code2: string) {
return this.postLanguages.includes(code2)
}
togglePostLanguage(code2: string) {
if (this.hasPostLanguage(code2)) {
this.postLanguage = this.postLanguages
.filter(lang => lang !== code2)
.join(',')
} else {
// sort alphabetically for deterministic comparison in context menu
this.postLanguage = this.postLanguages
.concat([code2])
.sort((a, b) => a.localeCompare(b))
.join(',')
}
}
setPostLanguage(commaSeparatedLangCodes: string) {
this.postLanguage = commaSeparatedLangCodes
}
/**
* Saves whatever language codes are currently selected into a history array,
* which is then used to populate the language selector menu.
*/
savePostLanguageToHistory() {
// filter out duplicate `this.postLanguage` if exists, and prepend
// value to start of array
this.postLanguageHistory = [this.postLanguage]
.concat(
this.postLanguageHistory.filter(
commaSeparatedLangCodes =>
commaSeparatedLangCodes !== this.postLanguage,
),
)
.slice(0, 6)
}
getReadablePostLanguages() {
const all = this.postLanguages.map(code2 => {
const lang = LANGUAGES.find(l => l.code2 === code2)
return lang ? lang.name : code2
})
return all.join(', ')
}
// moderation // moderation
// = // =
@@ -608,21 +471,13 @@ export class PreferencesModel {
} }
} }
toggleRequireAltTextEnabled() {
this.requireAltTextEnabled = !this.requireAltTextEnabled
}
setPrimaryLanguage(lang: string) {
this.primaryLanguage = lang
}
getFeedTuners( getFeedTuners(
feedType: 'home' | 'following' | 'author' | 'custom' | 'list' | 'likes', feedType: 'home' | 'following' | 'author' | 'custom' | 'list' | 'likes',
) { ) {
if (feedType === 'custom') { if (feedType === 'custom') {
return [ return [
FeedTuner.dedupReposts, FeedTuner.dedupReposts,
FeedTuner.preferredLangOnly(this.contentLanguages), FeedTuner.preferredLangOnly(getContentLanguages()),
] ]
} }
if (feedType === 'list') { if (feedType === 'list') {
-24
View File
@@ -1,24 +0,0 @@
import {makeAutoObservable} from 'mobx'
import {RootStoreModel} from '../root-store'
export class Reminders {
constructor(public rootStore: RootStoreModel) {
makeAutoObservable(
this,
{serialize: false, hydrate: false},
{autoBind: true},
)
}
serialize() {
return {}
}
hydrate(_v: unknown) {}
get shouldRequestEmailConfirmation() {
return false
}
setEmailConfirmationRequested() {}
}
-64
View File
@@ -1,64 +0,0 @@
import {makeAutoObservable} from 'mobx'
import {isObj, hasProp} from 'lib/type-guards'
import {RootStoreModel} from '../root-store'
import {toHashCode} from 'lib/strings/helpers'
export class Reminders {
lastEmailConfirm: Date | null = null
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
}
// only prompt once
if (this.lastEmailConfirm) {
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
}
return true
}
setEmailConfirmationRequested() {
this.lastEmailConfirm = new Date()
}
}
+9 -223
View File
@@ -1,12 +1,12 @@
import {AppBskyEmbedRecord, AppBskyActorDefs, ModerationUI} from '@atproto/api' import {AppBskyEmbedRecord} from '@atproto/api'
import {RootStoreModel} from '../root-store' import {RootStoreModel} from '../root-store'
import {makeAutoObservable, runInAction} from 'mobx' import {makeAutoObservable, runInAction} from 'mobx'
import {ProfileModel} from '../content/profile' import {ProfileModel} from '../content/profile'
import {Image as RNImage} from 'react-native-image-crop-picker' import {
import {ImageModel} from '../media/image' shouldRequestEmailConfirmation,
import {ListModel} from '../content/list' setEmailConfirmationRequested,
import {GalleryModel} from '../media/gallery' } from '#/state/shell/reminders'
import {StyleProp, ViewStyle} from 'react-native' import {unstable__openModal} from '#/state/modals'
export type ColorMode = 'system' | 'light' | 'dark' export type ColorMode = 'system' | 'light' | 'dark'
@@ -14,200 +14,6 @@ export function isColorMode(v: unknown): v is ColorMode {
return v === 'system' || v === 'light' || v === 'dark' return v === 'system' || v === 'light' || v === 'dark'
} }
export interface ConfirmModal {
name: 'confirm'
title: string
message: string | (() => JSX.Element)
onPressConfirm: () => void | Promise<void>
onPressCancel?: () => void | Promise<void>
confirmBtnText?: string
confirmBtnStyle?: StyleProp<ViewStyle>
cancelBtnText?: string
}
export interface EditProfileModal {
name: 'edit-profile'
profileView: ProfileModel
onUpdate?: () => void
}
export interface ProfilePreviewModal {
name: 'profile-preview'
did: string
}
export interface ServerInputModal {
name: 'server-input'
initialService: string
onSelect: (url: string) => void
}
export interface ModerationDetailsModal {
name: 'moderation-details'
context: 'account' | 'content'
moderation: ModerationUI
}
export type ReportModal = {
name: 'report'
} & (
| {
uri: string
cid: string
}
| {did: string}
)
export interface CreateOrEditListModal {
name: 'create-or-edit-list'
purpose?: string
list?: ListModel
onSave?: (uri: string) => void
}
export interface UserAddRemoveListsModal {
name: 'user-add-remove-lists'
subject: string
displayName: string
onAdd?: (listUri: string) => void
onRemove?: (listUri: string) => void
}
export interface ListAddUserModal {
name: 'list-add-user'
list: ListModel
onAdd?: (profile: AppBskyActorDefs.ProfileViewBasic) => void
}
export interface EditImageModal {
name: 'edit-image'
image: ImageModel
gallery: GalleryModel
}
export interface CropImageModal {
name: 'crop-image'
uri: string
onSelect: (img?: RNImage) => void
}
export interface AltTextImageModal {
name: 'alt-text-image'
image: ImageModel
}
export interface DeleteAccountModal {
name: 'delete-account'
}
export interface RepostModal {
name: 'repost'
onRepost: () => void
onQuote: () => void
isReposted: boolean
}
export interface SelfLabelModal {
name: 'self-label'
labels: string[]
hasMedia: boolean
onChange: (labels: string[]) => void
}
export interface ChangeHandleModal {
name: 'change-handle'
onChanged: () => void
}
export interface WaitlistModal {
name: 'waitlist'
}
export interface InviteCodesModal {
name: 'invite-codes'
}
export interface AddAppPasswordModal {
name: 'add-app-password'
}
export interface ContentFilteringSettingsModal {
name: 'content-filtering-settings'
}
export interface ContentLanguagesSettingsModal {
name: 'content-languages-settings'
}
export interface PostLanguagesSettingsModal {
name: 'post-languages-settings'
}
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
| ChangeHandleModal
| DeleteAccountModal
| EditProfileModal
| ProfilePreviewModal
| BirthDateSettingsModal
| VerifyEmailModal
| ChangeEmailModal
| SwitchAccountModal
// Curation
| ContentFilteringSettingsModal
| ContentLanguagesSettingsModal
| PostLanguagesSettingsModal
// Moderation
| ModerationDetailsModal
| ReportModal
// Lists
| CreateOrEditListModal
| UserAddRemoveListsModal
| ListAddUserModal
// Posts
| AltTextImageModal
| CropImageModal
| EditImageModal
| ServerInputModal
| RepostModal
| SelfLabelModal
// Bluesky access
| WaitlistModal
| InviteCodesModal
// Generic
| ConfirmModal
| LinkWarningModal
interface LightboxModel {} interface LightboxModel {}
export class ProfileImageLightbox implements LightboxModel { export class ProfileImageLightbox implements LightboxModel {
@@ -263,8 +69,6 @@ export interface ComposerOpts {
} }
export class ShellUiModel { export class ShellUiModel {
isModalActive = false
activeModals: Modal[] = []
isLightboxActive = false isLightboxActive = false
activeLightbox: ProfileImageLightbox | ImagesLightbox | null = null activeLightbox: ProfileImageLightbox | ImagesLightbox | null = null
isComposerActive = false isComposerActive = false
@@ -289,10 +93,6 @@ export class ShellUiModel {
this.closeLightbox() this.closeLightbox()
return true return true
} }
if (this.isModalActive) {
this.closeModal()
return true
}
if (this.isComposerActive) { if (this.isComposerActive) {
this.closeComposer() this.closeComposer()
return true return true
@@ -307,25 +107,11 @@ export class ShellUiModel {
if (this.isLightboxActive) { if (this.isLightboxActive) {
this.closeLightbox() this.closeLightbox()
} }
while (this.isModalActive) {
this.closeModal()
}
if (this.isComposerActive) { if (this.isComposerActive) {
this.closeComposer() this.closeComposer()
} }
} }
openModal(modal: Modal) {
this.rootStore.emitNavigation()
this.isModalActive = true
this.activeModals.push(modal)
}
closeModal() {
this.activeModals.pop()
this.isModalActive = this.activeModals.length > 0
}
openLightbox(lightbox: ProfileImageLightbox | ImagesLightbox) { openLightbox(lightbox: ProfileImageLightbox | ImagesLightbox) {
this.rootStore.emitNavigation() this.rootStore.emitNavigation()
this.isLightboxActive = true this.isLightboxActive = true
@@ -358,9 +144,9 @@ export class ShellUiModel {
setupLoginModals() { setupLoginModals() {
this.rootStore.onSessionReady(() => { this.rootStore.onSessionReady(() => {
if (this.rootStore.reminders.shouldRequestEmailConfirmation) { if (shouldRequestEmailConfirmation(this.rootStore.session)) {
this.openModal({name: 'verify-email', showReminder: true}) unstable__openModal({name: 'verify-email', showReminder: true})
this.rootStore.reminders.setEmailConfirmationRequested() setEmailConfirmationRequested()
} }
}) })
} }
+59
View File
@@ -0,0 +1,59 @@
import React from 'react'
import * as persisted from '#/state/persisted'
type StateContext = persisted.Schema['mutedThreads']
type ToggleContext = (uri: string) => boolean
const stateContext = React.createContext<StateContext>(
persisted.defaults.mutedThreads,
)
const toggleContext = React.createContext<ToggleContext>((_: string) => false)
export function Provider({children}: React.PropsWithChildren<{}>) {
const [state, setState] = React.useState(persisted.get('mutedThreads'))
const toggleThreadMute = React.useCallback(
(uri: string) => {
let muted = false
setState((arr: string[]) => {
if (arr.includes(uri)) {
arr = arr.filter(v => v !== uri)
muted = false
} else {
arr = arr.concat([uri])
muted = true
}
persisted.write('mutedThreads', arr)
return arr
})
return muted
},
[setState],
)
React.useEffect(() => {
return persisted.onUpdate(() => {
setState(persisted.get('mutedThreads'))
})
}, [setState])
return (
<stateContext.Provider value={state}>
<toggleContext.Provider value={toggleThreadMute}>
{children}
</toggleContext.Provider>
</stateContext.Provider>
)
}
export function useMutedThreads() {
return React.useContext(stateContext)
}
export function useToggleThreadMute() {
return React.useContext(toggleContext)
}
export function isThreadMuted(uri: string) {
return persisted.get('mutedThreads').includes(uri)
}
+1 -1
View File
@@ -6,7 +6,7 @@ import * as store from '#/state/persisted/store'
import BroadcastChannel from '#/state/persisted/broadcast' import BroadcastChannel from '#/state/persisted/broadcast'
export type {Schema} from '#/state/persisted/schema' export type {Schema} from '#/state/persisted/schema'
export {defaults as schema} from '#/state/persisted/schema' export {defaults} from '#/state/persisted/schema'
const broadcast = new BroadcastChannel('BSKY_BROADCAST_CHANNEL') const broadcast = new BroadcastChannel('BSKY_BROADCAST_CHANNEL')
const UPDATE_EVENT = 'BSKY_UPDATE' const UPDATE_EVENT = 'BSKY_UPDATE'
+4 -6
View File
@@ -76,9 +76,9 @@ export function transform(legacy: LegacySchema): Schema {
defaults.session.currentAccount, defaults.session.currentAccount,
}, },
reminders: { reminders: {
lastEmailConfirmReminder: lastEmailConfirm:
legacy.reminders.lastEmailConfirm || legacy.reminders.lastEmailConfirm ||
defaults.reminders.lastEmailConfirmReminder, defaults.reminders.lastEmailConfirm,
}, },
languagePrefs: { languagePrefs: {
primaryLanguage: primaryLanguage:
@@ -97,11 +97,9 @@ export function transform(legacy: LegacySchema): Schema {
legacy.preferences.requireAltTextEnabled || legacy.preferences.requireAltTextEnabled ||
defaults.requireAltTextEnabled, defaults.requireAltTextEnabled,
mutedThreads: legacy.mutedThreads.uris || defaults.mutedThreads, mutedThreads: legacy.mutedThreads.uris || defaults.mutedThreads,
invitedUsers: { invites: {
seenDids: legacy.invitedUsers.seenDids || defaults.invitedUsers.seenDids,
copiedInvites: copiedInvites:
legacy.invitedUsers.copiedInvites || legacy.invitedUsers.copiedInvites || defaults.invites.copiedInvites,
defaults.invitedUsers.copiedInvites,
}, },
onboarding: { onboarding: {
step: legacy.onboarding.step || defaults.onboarding.step, step: legacy.onboarding.step || defaults.onboarding.step,
+7 -9
View File
@@ -7,9 +7,9 @@ const accountSchema = z.object({
did: z.string(), did: z.string(),
refreshJwt: z.string().optional(), refreshJwt: z.string().optional(),
accessJwt: z.string().optional(), accessJwt: z.string().optional(),
handle: z.string(), handle: z.string().optional(),
displayName: z.string(), displayName: z.string().optional(),
aviUrl: z.string(), aviUrl: z.string().optional(),
}) })
export const schema = z.object({ export const schema = z.object({
@@ -19,7 +19,7 @@ export const schema = z.object({
currentAccount: accountSchema.optional(), currentAccount: accountSchema.optional(),
}), }),
reminders: z.object({ reminders: z.object({
lastEmailConfirmReminder: z.string().optional(), lastEmailConfirm: z.string().optional(),
}), }),
languagePrefs: z.object({ languagePrefs: z.object({
primaryLanguage: z.string(), // should move to server primaryLanguage: z.string(), // should move to server
@@ -29,8 +29,7 @@ export const schema = z.object({
}), }),
requireAltTextEnabled: z.boolean(), // should move to server requireAltTextEnabled: z.boolean(), // should move to server
mutedThreads: z.array(z.string()), // should move to server mutedThreads: z.array(z.string()), // should move to server
invitedUsers: z.object({ invites: z.object({
seenDids: z.array(z.string()),
copiedInvites: z.array(z.string()), copiedInvites: z.array(z.string()),
}), }),
onboarding: z.object({ onboarding: z.object({
@@ -46,7 +45,7 @@ export const defaults: Schema = {
currentAccount: undefined, currentAccount: undefined,
}, },
reminders: { reminders: {
lastEmailConfirmReminder: undefined, lastEmailConfirm: undefined,
}, },
languagePrefs: { languagePrefs: {
primaryLanguage: deviceLocales[0] || 'en', primaryLanguage: deviceLocales[0] || 'en',
@@ -58,8 +57,7 @@ export const defaults: Schema = {
}, },
requireAltTextEnabled: false, requireAltTextEnabled: false,
mutedThreads: [], mutedThreads: [],
invitedUsers: { invites: {
seenDids: [],
copiedInvites: [], copiedInvites: [],
}, },
onboarding: { onboarding: {
@@ -0,0 +1,48 @@
import React from 'react'
import * as persisted from '#/state/persisted'
type StateContext = persisted.Schema['requireAltTextEnabled']
type SetContext = (v: persisted.Schema['requireAltTextEnabled']) => void
const stateContext = React.createContext<StateContext>(
persisted.defaults.requireAltTextEnabled,
)
const setContext = React.createContext<SetContext>(
(_: persisted.Schema['requireAltTextEnabled']) => {},
)
export function Provider({children}: React.PropsWithChildren<{}>) {
const [state, setState] = React.useState(
persisted.get('requireAltTextEnabled'),
)
const setStateWrapped = React.useCallback(
(requireAltTextEnabled: persisted.Schema['requireAltTextEnabled']) => {
setState(requireAltTextEnabled)
persisted.write('requireAltTextEnabled', requireAltTextEnabled)
},
[setState],
)
React.useEffect(() => {
return persisted.onUpdate(() => {
setState(persisted.get('requireAltTextEnabled'))
})
}, [setStateWrapped])
return (
<stateContext.Provider value={state}>
<setContext.Provider value={setStateWrapped}>
{children}
</setContext.Provider>
</stateContext.Provider>
)
}
export function useRequireAltTextEnabled() {
return React.useContext(stateContext)
}
export function useSetRequireAltTextEnabled() {
return React.useContext(setContext)
}
+17
View File
@@ -0,0 +1,17 @@
import React from 'react'
import {Provider as LanguagesProvider} from './languages'
import {Provider as AltTextRequiredProvider} from '../preferences/alt-text-required'
export {useLanguagePrefs, useLanguagePrefsApi} from './languages'
export {
useRequireAltTextEnabled,
useSetRequireAltTextEnabled,
} from './alt-text-required'
export function Provider({children}: React.PropsWithChildren<{}>) {
return (
<LanguagesProvider>
<AltTextRequiredProvider>{children}</AltTextRequiredProvider>
</LanguagesProvider>
)
}
+137
View File
@@ -0,0 +1,137 @@
import React from 'react'
import * as persisted from '#/state/persisted'
type SetStateCb = (
s: persisted.Schema['languagePrefs'],
) => persisted.Schema['languagePrefs']
type StateContext = persisted.Schema['languagePrefs']
type ApiContext = {
setPrimaryLanguage: (code2: string) => void
setPostLanguage: (commaSeparatedLangCodes: string) => void
toggleContentLanguage: (code2: string) => void
togglePostLanguage: (code2: string) => void
savePostLanguageToHistory: () => void
}
const stateContext = React.createContext<StateContext>(
persisted.defaults.languagePrefs,
)
const apiContext = React.createContext<ApiContext>({
setPrimaryLanguage: (_: string) => {},
setPostLanguage: (_: string) => {},
toggleContentLanguage: (_: string) => {},
togglePostLanguage: (_: string) => {},
savePostLanguageToHistory: () => {},
})
export function Provider({children}: React.PropsWithChildren<{}>) {
const [state, setState] = React.useState(persisted.get('languagePrefs'))
const setStateWrapped = React.useCallback(
(fn: SetStateCb) => {
const s = fn(persisted.get('languagePrefs'))
setState(s)
persisted.write('languagePrefs', s)
},
[setState],
)
React.useEffect(() => {
return persisted.onUpdate(() => {
setState(persisted.get('languagePrefs'))
})
}, [setStateWrapped])
const api = React.useMemo(
() => ({
setPrimaryLanguage(code2: string) {
setStateWrapped(s => ({...s, primaryLanguage: code2}))
},
setPostLanguage(commaSeparatedLangCodes: string) {
setStateWrapped(s => ({...s, postLanguage: commaSeparatedLangCodes}))
},
toggleContentLanguage(code2: string) {
setStateWrapped(s => {
const exists = s.contentLanguages.includes(code2)
const next = exists
? s.contentLanguages.filter(lang => lang !== code2)
: s.contentLanguages.concat(code2)
return {
...s,
contentLanguages: next,
}
})
},
togglePostLanguage(code2: string) {
setStateWrapped(s => {
const exists = hasPostLanguage(state.postLanguage, code2)
let next = s.postLanguage
if (exists) {
next = toPostLanguages(s.postLanguage)
.filter(lang => lang !== code2)
.join(',')
} else {
// sort alphabetically for deterministic comparison in context menu
next = toPostLanguages(s.postLanguage)
.concat([code2])
.sort((a, b) => a.localeCompare(b))
.join(',')
}
return {
...s,
postLanguage: next,
}
})
},
/**
* Saves whatever language codes are currently selected into a history array,
* which is then used to populate the language selector menu.
*/
savePostLanguageToHistory() {
// filter out duplicate `this.postLanguage` if exists, and prepend
// value to start of array
setStateWrapped(s => ({
...s,
postLanguageHistory: [s.postLanguage]
.concat(
s.postLanguageHistory.filter(
commaSeparatedLangCodes =>
commaSeparatedLangCodes !== s.postLanguage,
),
)
.slice(0, 6),
}))
},
}),
[state, setStateWrapped],
)
return (
<stateContext.Provider value={state}>
<apiContext.Provider value={api}>{children}</apiContext.Provider>
</stateContext.Provider>
)
}
export function useLanguagePrefs() {
return React.useContext(stateContext)
}
export function useLanguagePrefsApi() {
return React.useContext(apiContext)
}
export function getContentLanguages() {
return persisted.get('languagePrefs').contentLanguages
}
export function toPostLanguages(postLanguage: string): string[] {
// filter out empty strings if exist
return postLanguage.split(',').filter(Boolean)
}
export function hasPostLanguage(postLanguage: string, code2: string): boolean {
return toPostLanguages(postLanguage).includes(code2)
}
+1 -1
View File
@@ -27,7 +27,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
setState(persisted.get('colorMode')) setState(persisted.get('colorMode'))
updateDocument(persisted.get('colorMode')) updateDocument(persisted.get('colorMode'))
}) })
}, [setStateWrapped]) }, [setState])
return ( return (
<stateContext.Provider value={state}> <stateContext.Provider value={state}>
+5 -1
View File
@@ -3,6 +3,7 @@ import {Provider as DrawerOpenProvider} from './drawer-open'
import {Provider as DrawerSwipableProvider} from './drawer-swipe-disabled' import {Provider as DrawerSwipableProvider} from './drawer-swipe-disabled'
import {Provider as MinimalModeProvider} from './minimal-mode' import {Provider as MinimalModeProvider} from './minimal-mode'
import {Provider as ColorModeProvider} from './color-mode' import {Provider as ColorModeProvider} from './color-mode'
import {Provider as OnboardingProvider} from './onboarding'
export {useIsDrawerOpen, useSetDrawerOpen} from './drawer-open' export {useIsDrawerOpen, useSetDrawerOpen} from './drawer-open'
export { export {
@@ -11,13 +12,16 @@ export {
} from './drawer-swipe-disabled' } from './drawer-swipe-disabled'
export {useMinimalShellMode, useSetMinimalShellMode} from './minimal-mode' export {useMinimalShellMode, useSetMinimalShellMode} from './minimal-mode'
export {useColorMode, useSetColorMode} from './color-mode' export {useColorMode, useSetColorMode} from './color-mode'
export {useOnboardingState, useOnboardingDispatch} from './onboarding'
export function Provider({children}: React.PropsWithChildren<{}>) { export function Provider({children}: React.PropsWithChildren<{}>) {
return ( return (
<DrawerOpenProvider> <DrawerOpenProvider>
<DrawerSwipableProvider> <DrawerSwipableProvider>
<MinimalModeProvider> <MinimalModeProvider>
<ColorModeProvider>{children}</ColorModeProvider> <ColorModeProvider>
<OnboardingProvider>{children}</OnboardingProvider>
</ColorModeProvider>
</MinimalModeProvider> </MinimalModeProvider>
</DrawerSwipableProvider> </DrawerSwipableProvider>
</DrawerOpenProvider> </DrawerOpenProvider>
+119
View File
@@ -0,0 +1,119 @@
import React from 'react'
import * as persisted from '#/state/persisted'
import {track} from '#/lib/analytics/analytics'
export const OnboardingScreenSteps = {
Welcome: 'Welcome',
RecommendedFeeds: 'RecommendedFeeds',
RecommendedFollows: 'RecommendedFollows',
Home: 'Home',
} as const
type OnboardingStep =
(typeof OnboardingScreenSteps)[keyof typeof OnboardingScreenSteps]
const OnboardingStepsArray = Object.values(OnboardingScreenSteps)
type Action =
| {type: 'set'; step: OnboardingStep}
| {type: 'next'; currentStep?: OnboardingStep}
| {type: 'start'}
| {type: 'finish'}
| {type: 'skip'}
export type StateContext = persisted.Schema['onboarding'] & {
isComplete: boolean
isActive: boolean
}
export type DispatchContext = (action: Action) => void
const stateContext = React.createContext<StateContext>(
compute(persisted.defaults.onboarding),
)
const dispatchContext = React.createContext<DispatchContext>((_: Action) => {})
function reducer(state: StateContext, action: Action): StateContext {
switch (action.type) {
case 'set': {
if (OnboardingStepsArray.includes(action.step)) {
persisted.write('onboarding', {step: action.step})
return compute({...state, step: action.step})
}
return state
}
case 'next': {
const currentStep = action.currentStep || state.step
let nextStep = 'Home'
if (currentStep === 'Welcome') {
nextStep = 'RecommendedFeeds'
} else if (currentStep === 'RecommendedFeeds') {
nextStep = 'RecommendedFollows'
} else if (currentStep === 'RecommendedFollows') {
nextStep = 'Home'
}
persisted.write('onboarding', {step: nextStep})
return compute({...state, step: nextStep})
}
case 'start': {
track('Onboarding:Begin')
persisted.write('onboarding', {step: 'Welcome'})
return compute({...state, step: 'Welcome'})
}
case 'finish': {
track('Onboarding:Complete')
persisted.write('onboarding', {step: 'Home'})
return compute({...state, step: 'Home'})
}
case 'skip': {
track('Onboarding:Skipped')
persisted.write('onboarding', {step: 'Home'})
return compute({...state, step: 'Home'})
}
default: {
throw new Error('Invalid action')
}
}
}
export function Provider({children}: React.PropsWithChildren<{}>) {
const [state, dispatch] = React.useReducer(
reducer,
compute(persisted.get('onboarding')),
)
React.useEffect(() => {
return persisted.onUpdate(() => {
dispatch({
type: 'set',
step: persisted.get('onboarding').step as OnboardingStep,
})
})
}, [dispatch])
return (
<stateContext.Provider value={state}>
<dispatchContext.Provider value={dispatch}>
{children}
</dispatchContext.Provider>
</stateContext.Provider>
)
}
export function useOnboardingState() {
return React.useContext(stateContext)
}
export function useOnboardingDispatch() {
return React.useContext(dispatchContext)
}
export function isOnboardingActive() {
return compute(persisted.get('onboarding')).isActive
}
function compute(state: persisted.Schema['onboarding']): StateContext {
return {
...state,
isActive: state.step !== 'Home',
isComplete: state.step === 'Home',
}
}
+7
View File
@@ -0,0 +1,7 @@
import {SessionModel} from '../models/session'
export function shouldRequestEmailConfirmation(_session: SessionModel) {
return false
}
export function setEmailConfirmationRequested() {}
+37
View File
@@ -0,0 +1,37 @@
import * as persisted from '#/state/persisted'
import {SessionModel} from '../models/session'
import {toHashCode} from 'lib/strings/helpers'
import {isOnboardingActive} from './onboarding'
export function shouldRequestEmailConfirmation(session: SessionModel) {
const sess = session.currentSession
if (!sess) {
return false
}
if (sess.emailConfirmed) {
return false
}
if (isOnboardingActive()) {
return false
}
// only prompt once
if (persisted.get('reminders').lastEmailConfirm) {
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
}
return true
}
export function setEmailConfirmationRequested() {
persisted.write('reminders', {
...persisted.get('reminders'),
lastEmailConfirm: new Date().toISOString(),
})
}
+8 -7
View File
@@ -4,34 +4,35 @@ import {observer} from 'mobx-react-lite'
import {ErrorBoundary} from 'view/com/util/ErrorBoundary' import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useStores} from 'state/index'
import {Welcome} from './onboarding/Welcome' import {Welcome} from './onboarding/Welcome'
import {RecommendedFeeds} from './onboarding/RecommendedFeeds' import {RecommendedFeeds} from './onboarding/RecommendedFeeds'
import {RecommendedFollows} from './onboarding/RecommendedFollows' import {RecommendedFollows} from './onboarding/RecommendedFollows'
import {useSetMinimalShellMode} from '#/state/shell/minimal-mode' import {useSetMinimalShellMode} from '#/state/shell/minimal-mode'
import {useOnboardingState, useOnboardingDispatch} from '#/state/shell'
export const Onboarding = observer(function OnboardingImpl() { export const Onboarding = observer(function OnboardingImpl() {
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores()
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const onboardingState = useOnboardingState()
const onboardingDispatch = useOnboardingDispatch()
React.useEffect(() => { React.useEffect(() => {
setMinimalShellMode(true) setMinimalShellMode(true)
}, [setMinimalShellMode]) }, [setMinimalShellMode])
const next = () => store.onboarding.next() const next = () => onboardingDispatch({type: 'next'})
const skip = () => store.onboarding.skip() const skip = () => onboardingDispatch({type: 'skip'})
return ( return (
<SafeAreaView testID="onboardingView" style={[s.hContentRegion, pal.view]}> <SafeAreaView testID="onboardingView" style={[s.hContentRegion, pal.view]}>
<ErrorBoundary> <ErrorBoundary>
{store.onboarding.step === 'Welcome' && ( {onboardingState.step === 'Welcome' && (
<Welcome skip={skip} next={next} /> <Welcome skip={skip} next={next} />
)} )}
{store.onboarding.step === 'RecommendedFeeds' && ( {onboardingState.step === 'RecommendedFeeds' && (
<RecommendedFeeds next={next} /> <RecommendedFeeds next={next} />
)} )}
{store.onboarding.step === 'RecommendedFollows' && ( {onboardingState.step === 'RecommendedFollows' && (
<RecommendedFollows next={next} /> <RecommendedFollows next={next} />
)} )}
</ErrorBoundary> </ErrorBoundary>
+4 -2
View File
@@ -17,6 +17,7 @@ import {CreateAccountModel} from 'state/models/ui/create-account'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useOnboardingDispatch} from '#/state/shell'
import {Step1} from './Step1' import {Step1} from './Step1'
import {Step2} from './Step2' import {Step2} from './Step2'
@@ -32,6 +33,7 @@ export const CreateAccount = observer(function CreateAccountImpl({
const store = useStores() const store = useStores()
const model = React.useMemo(() => new CreateAccountModel(store), [store]) const model = React.useMemo(() => new CreateAccountModel(store), [store])
const {_} = useLingui() const {_} = useLingui()
const onboardingDispatch = useOnboardingDispatch()
React.useEffect(() => { React.useEffect(() => {
screen('CreateAccount') screen('CreateAccount')
@@ -62,14 +64,14 @@ export const CreateAccount = observer(function CreateAccountImpl({
model.next() model.next()
} else { } else {
try { try {
await model.submit() await model.submit(onboardingDispatch)
} catch { } catch {
// dont need to handle here // dont need to handle here
} finally { } finally {
track('Try Create Account') track('Try Create Account')
} }
} }
}, [model, track]) }, [model, track, onboardingDispatch])
return ( return (
<LoggedOutLayout <LoggedOutLayout
+4 -4
View File
@@ -10,10 +10,10 @@ import {usePalette} from 'lib/hooks/usePalette'
import {TextInput} from '../util/TextInput' import {TextInput} from '../util/TextInput'
import {Policies} from './Policies' import {Policies} from './Policies'
import {ErrorMessage} from 'view/com/util/error/ErrorMessage' import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
import {useStores} from 'state/index'
import {isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
/** STEP 2: Your account /** STEP 2: Your account
* @field Invite code or waitlist * @field Invite code or waitlist
@@ -30,12 +30,12 @@ export const Step2 = observer(function Step2Impl({
model: CreateAccountModel model: CreateAccountModel
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores()
const {_} = useLingui() const {_} = useLingui()
const {openModal} = useModalControls()
const onPressWaitlist = React.useCallback(() => { const onPressWaitlist = React.useCallback(() => {
store.shell.openModal({name: 'waitlist'}) openModal({name: 'waitlist'})
}, [store]) }, [openModal])
return ( return (
<View> <View>
+13 -10
View File
@@ -22,12 +22,12 @@ import {usePalette} from 'lib/hooks/usePalette'
import {useTheme} from 'lib/ThemeContext' import {useTheme} from 'lib/ThemeContext'
import {cleanError} from 'lib/strings/errors' import {cleanError} from 'lib/strings/errors'
import {logger} from '#/logger' import {logger} from '#/logger'
import {msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {styles} from './styles' import {styles} from './styles'
import {useModalControls} from '#/state/modals'
export const ForgotPasswordForm = ({ export const ForgotPasswordForm = ({
store,
error, error,
serviceUrl, serviceUrl,
serviceDescription, serviceDescription,
@@ -51,13 +51,14 @@ export const ForgotPasswordForm = ({
const [email, setEmail] = useState<string>('') const [email, setEmail] = useState<string>('')
const {screen} = useAnalytics() const {screen} = useAnalytics()
const {_} = useLingui() const {_} = useLingui()
const {openModal} = useModalControls()
useEffect(() => { useEffect(() => {
screen('Signin:ForgotPassword') screen('Signin:ForgotPassword')
}, [screen]) }, [screen])
const onPressSelectService = () => { const onPressSelectService = () => {
store.shell.openModal({ openModal({
name: 'server-input', name: 'server-input',
initialService: serviceUrl, initialService: serviceUrl,
onSelect: setServiceUrl, onSelect: setServiceUrl,
@@ -94,11 +95,13 @@ export const ForgotPasswordForm = ({
<> <>
<View> <View>
<Text type="title-lg" style={[pal.text, styles.screenTitle]}> <Text type="title-lg" style={[pal.text, styles.screenTitle]}>
Reset password <Trans>Reset password</Trans>
</Text> </Text>
<Text type="md" style={[pal.text, styles.instructions]}> <Text type="md" style={[pal.text, styles.instructions]}>
Enter the email you used to create your account. We'll send you a <Trans>
"reset code" so you can set a new password. Enter the email you used to create your account. We'll send you a
"reset code" so you can set a new password.
</Trans>
</Text> </Text>
<View <View
testID="forgotPasswordView" testID="forgotPasswordView"
@@ -160,7 +163,7 @@ export const ForgotPasswordForm = ({
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}> <View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
<TouchableOpacity onPress={onPressBack} accessibilityRole="button"> <TouchableOpacity onPress={onPressBack} accessibilityRole="button">
<Text type="xl" style={[pal.link, s.pl5]}> <Text type="xl" style={[pal.link, s.pl5]}>
Back <Trans>Back</Trans>
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
<View style={s.flex1} /> <View style={s.flex1} />
@@ -168,7 +171,7 @@ export const ForgotPasswordForm = ({
<ActivityIndicator /> <ActivityIndicator />
) : !email ? ( ) : !email ? (
<Text type="xl-bold" style={[pal.link, s.pr5, styles.dimmed]}> <Text type="xl-bold" style={[pal.link, s.pr5, styles.dimmed]}>
Next <Trans>Next</Trans>
</Text> </Text>
) : ( ) : (
<TouchableOpacity <TouchableOpacity
@@ -178,13 +181,13 @@ export const ForgotPasswordForm = ({
accessibilityLabel={_(msg`Go to next`)} accessibilityLabel={_(msg`Go to next`)}
accessibilityHint="Navigates to the next screen"> accessibilityHint="Navigates to the next screen">
<Text type="xl-bold" style={[pal.link, s.pr5]}> <Text type="xl-bold" style={[pal.link, s.pr5]}>
Next <Trans>Next</Trans>
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
)} )}
{!serviceDescription || isProcessing ? ( {!serviceDescription || isProcessing ? (
<Text type="xl" style={[pal.textLight, s.pl10]}> <Text type="xl" style={[pal.textLight, s.pl10]}>
Processing... <Trans>Processing...</Trans>
</Text> </Text>
) : undefined} ) : undefined}
</View> </View>
+3 -1
View File
@@ -25,6 +25,7 @@ import {logger} from '#/logger'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {styles} from './styles' import {styles} from './styles'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
export const LoginForm = ({ export const LoginForm = ({
store, store,
@@ -57,9 +58,10 @@ export const LoginForm = ({
const [password, setPassword] = useState<string>('') const [password, setPassword] = useState<string>('')
const passwordInputRef = useRef<TextInput>(null) const passwordInputRef = useRef<TextInput>(null)
const {_} = useLingui() const {_} = useLingui()
const {openModal} = useModalControls()
const onPressSelectService = () => { const onPressSelectService = () => {
store.shell.openModal({ openModal({
name: 'server-input', name: 'server-input',
initialService: serviceUrl, initialService: serviceUrl,
onSelect: setServiceUrl, onSelect: setServiceUrl,
@@ -11,6 +11,7 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useStores} from 'state/index' import {useStores} from 'state/index'
import {RecommendedFollowsItem} from './RecommendedFollowsItem' import {RecommendedFollowsItem} from './RecommendedFollowsItem'
import {SuggestedActorsModel} from '#/state/models/discovery/suggested-actors'
type Props = { type Props = {
next: () => void next: () => void
@@ -21,16 +22,10 @@ export const RecommendedFollows = observer(function RecommendedFollowsImpl({
const store = useStores() const store = useStores()
const pal = usePalette('default') const pal = usePalette('default')
const {isTabletOrMobile} = useWebMediaQueries() const {isTabletOrMobile} = useWebMediaQueries()
const suggestedActors = React.useMemo(() => {
React.useEffect(() => { const model = new SuggestedActorsModel(store)
// Load suggested actors if not already loaded model.refresh()
// prefetch should happen in the onboarding model return model
if (
!store.onboarding.suggestedActors.hasLoaded ||
store.onboarding.suggestedActors.isEmpty
) {
store.onboarding.suggestedActors.loadMore(true)
}
}, [store]) }, [store])
const title = ( const title = (
@@ -98,13 +93,19 @@ export const RecommendedFollows = observer(function RecommendedFollowsImpl({
horizontal horizontal
titleStyle={isTabletOrMobile ? undefined : {minWidth: 470}} titleStyle={isTabletOrMobile ? undefined : {minWidth: 470}}
contentStyle={{paddingHorizontal: 0}}> contentStyle={{paddingHorizontal: 0}}>
{store.onboarding.suggestedActors.isLoading ? ( {suggestedActors.isLoading ? (
<ActivityIndicator size="large" /> <ActivityIndicator size="large" />
) : ( ) : (
<FlatList <FlatList
data={store.onboarding.suggestedActors.suggestions} data={suggestedActors.suggestions}
renderItem={({item, index}) => ( renderItem={({item, index}) => (
<RecommendedFollowsItem item={item} index={index} /> <RecommendedFollowsItem
item={item}
index={index}
insertSuggestionsByActor={suggestedActors.insertSuggestionsByActor.bind(
suggestedActors,
)}
/>
)} )}
keyExtractor={(item, index) => item.did + index.toString()} keyExtractor={(item, index) => item.did + index.toString()}
style={{flex: 1}} style={{flex: 1}}
@@ -126,13 +127,19 @@ export const RecommendedFollows = observer(function RecommendedFollowsImpl({
users. users.
</Text> </Text>
</View> </View>
{store.onboarding.suggestedActors.isLoading ? ( {suggestedActors.isLoading ? (
<ActivityIndicator size="large" /> <ActivityIndicator size="large" />
) : ( ) : (
<FlatList <FlatList
data={store.onboarding.suggestedActors.suggestions} data={suggestedActors.suggestions}
renderItem={({item, index}) => ( renderItem={({item, index}) => (
<RecommendedFollowsItem item={item} index={index} /> <RecommendedFollowsItem
item={item}
index={index}
insertSuggestionsByActor={suggestedActors.insertSuggestionsByActor.bind(
suggestedActors,
)}
/>
)} )}
keyExtractor={(item, index) => item.did + index.toString()} keyExtractor={(item, index) => item.did + index.toString()}
style={{flex: 1}} style={{flex: 1}}
@@ -1,4 +1,4 @@
import React, {useMemo} from 'react' import React from 'react'
import {View, StyleSheet, ActivityIndicator} from 'react-native' import {View, StyleSheet, ActivityIndicator} from 'react-native'
import {AppBskyActorDefs, moderateProfile} from '@atproto/api' import {AppBskyActorDefs, moderateProfile} from '@atproto/api'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
@@ -19,22 +19,19 @@ import {Trans} from '@lingui/macro'
type Props = { type Props = {
item: SuggestedActor item: SuggestedActor
index: number index: number
insertSuggestionsByActor: (did: string, index: number) => Promise<void>
} }
export const RecommendedFollowsItem: React.FC<Props> = ({item, index}) => { export const RecommendedFollowsItem: React.FC<Props> = ({
item,
index,
insertSuggestionsByActor,
}) => {
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores()
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const delay = useMemo(() => {
return (
50 *
(Math.abs(store.onboarding.suggestedActors.lastInsertedAtIndex - index) %
5)
)
}, [index, store.onboarding.suggestedActors.lastInsertedAtIndex])
return ( return (
<Animated.View <Animated.View
entering={FadeInRight.delay(delay).springify()} entering={FadeInRight}
style={[ style={[
styles.cardContainer, styles.cardContainer,
pal.view, pal.view,
@@ -44,7 +41,12 @@ export const RecommendedFollowsItem: React.FC<Props> = ({item, index}) => {
borderRightWidth: isMobile ? undefined : 1, borderRightWidth: isMobile ? undefined : 1,
}, },
]}> ]}>
<ProfileCard key={item.did} profile={item} index={index} /> <ProfileCard
key={item.did}
profile={item}
index={index}
insertSuggestionsByActor={insertSuggestionsByActor}
/>
</Animated.View> </Animated.View>
) )
} }
@@ -52,9 +54,11 @@ export const RecommendedFollowsItem: React.FC<Props> = ({item, index}) => {
export const ProfileCard = observer(function ProfileCardImpl({ export const ProfileCard = observer(function ProfileCardImpl({
profile, profile,
index, index,
insertSuggestionsByActor,
}: { }: {
profile: AppBskyActorDefs.ProfileViewBasic profile: AppBskyActorDefs.ProfileViewBasic
index: number index: number
insertSuggestionsByActor: (did: string, index: number) => Promise<void>
}) { }) {
const {track} = useAnalytics() const {track} = useAnalytics()
const store = useStores() const store = useStores()
@@ -95,10 +99,7 @@ export const ProfileCard = observer(function ProfileCardImpl({
onToggleFollow={async isFollow => { onToggleFollow={async isFollow => {
if (isFollow) { if (isFollow) {
setAddingMoreSuggestions(true) setAddingMoreSuggestions(true)
await store.onboarding.suggestedActors.insertSuggestionsByActor( await insertSuggestionsByActor(profile.did, index)
profile.did,
index,
)
setAddingMoreSuggestions(false) setAddingMoreSuggestions(false)
track('Onboarding:SuggestedFollowFollowed') track('Onboarding:SuggestedFollowFollowed')
} }
+3 -1
View File
@@ -13,19 +13,21 @@ import {Onboarding} from './Onboarding'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {STATUS_PAGE_URL} from 'lib/constants' import {STATUS_PAGE_URL} from 'lib/constants'
import {useOnboardingState} from '#/state/shell'
export const withAuthRequired = <P extends object>( export const withAuthRequired = <P extends object>(
Component: React.ComponentType<P>, Component: React.ComponentType<P>,
): React.FC<P> => ): React.FC<P> =>
observer(function AuthRequired(props: P) { observer(function AuthRequired(props: P) {
const store = useStores() const store = useStores()
const onboardingState = useOnboardingState()
if (store.session.isResumingSession) { if (store.session.isResumingSession) {
return <Loading /> return <Loading />
} }
if (!store.session.hasSession) { if (!store.session.hasSession) {
return <LoggedOut /> return <LoggedOut />
} }
if (store.onboarding.isActive) { if (onboardingState.isActive) {
return <Onboarding /> return <Onboarding />
} }
return <Component {...props} /> return <Component {...props} />
+23 -15
View File
@@ -51,6 +51,13 @@ import {EmojiPickerButton} from './text-input/web/EmojiPicker.web'
import {insertMentionAt} from 'lib/strings/mention-manip' import {insertMentionAt} from 'lib/strings/mention-manip'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useModals, useModalControls} from '#/state/modals'
import {useRequireAltTextEnabled} from '#/state/preferences'
import {
useLanguagePrefs,
useLanguagePrefsApi,
toPostLanguages,
} from '#/state/preferences/languages'
type Props = ComposerOpts type Props = ComposerOpts
export const ComposePost = observer(function ComposePost({ export const ComposePost = observer(function ComposePost({
@@ -59,11 +66,16 @@ export const ComposePost = observer(function ComposePost({
quote: initQuote, quote: initQuote,
mention: initMention, mention: initMention,
}: Props) { }: Props) {
const {activeModals} = useModals()
const {openModal, closeModal} = useModalControls()
const {track} = useAnalytics() const {track} = useAnalytics()
const pal = usePalette('default') const pal = usePalette('default')
const {isDesktop, isMobile} = useWebMediaQueries() const {isDesktop, isMobile} = useWebMediaQueries()
const store = useStores() const store = useStores()
const {_} = useLingui() const {_} = useLingui()
const requireAltTextEnabled = useRequireAltTextEnabled()
const langPrefs = useLanguagePrefs()
const setLangPrefs = useLanguagePrefsApi()
const textInput = useRef<TextInputRef>(null) const textInput = useRef<TextInputRef>(null)
const [isKeyboardVisible] = useIsKeyboardVisible({iosUseWillEvents: true}) const [isKeyboardVisible] = useIsKeyboardVisible({iosUseWillEvents: true})
const [isProcessing, setIsProcessing] = useState(false) const [isProcessing, setIsProcessing] = useState(false)
@@ -111,18 +123,18 @@ export const ComposePost = observer(function ComposePost({
const onPressCancel = useCallback(() => { const onPressCancel = useCallback(() => {
if (graphemeLength > 0 || !gallery.isEmpty) { if (graphemeLength > 0 || !gallery.isEmpty) {
if (store.shell.activeModals.some(modal => modal.name === 'confirm')) { if (activeModals.some(modal => modal.name === 'confirm')) {
store.shell.closeModal() closeModal()
} }
if (Keyboard) { if (Keyboard) {
Keyboard.dismiss() Keyboard.dismiss()
} }
store.shell.openModal({ openModal({
name: 'confirm', name: 'confirm',
title: 'Discard draft', title: 'Discard draft',
onPressConfirm: onClose, onPressConfirm: onClose,
onPressCancel: () => { onPressCancel: () => {
store.shell.closeModal() closeModal()
}, },
message: "Are you sure you'd like to discard this draft?", message: "Are you sure you'd like to discard this draft?",
confirmBtnText: 'Discard', confirmBtnText: 'Discard',
@@ -131,7 +143,7 @@ export const ComposePost = observer(function ComposePost({
} else { } else {
onClose() onClose()
} }
}, [store, onClose, graphemeLength, gallery]) }, [openModal, closeModal, activeModals, onClose, graphemeLength, gallery])
// android back button // android back button
useEffect(() => { useEffect(() => {
if (!isAndroid) { if (!isAndroid) {
@@ -190,7 +202,7 @@ export const ComposePost = observer(function ComposePost({
if (isProcessing || graphemeLength > MAX_GRAPHEME_LENGTH) { if (isProcessing || graphemeLength > MAX_GRAPHEME_LENGTH) {
return return
} }
if (store.preferences.requireAltTextEnabled && gallery.needsAltText) { if (requireAltTextEnabled && gallery.needsAltText) {
return return
} }
@@ -213,7 +225,7 @@ export const ComposePost = observer(function ComposePost({
labels, labels,
onStateChange: setProcessingState, onStateChange: setProcessingState,
knownHandles: autocompleteView.knownHandles, knownHandles: autocompleteView.knownHandles,
langs: store.preferences.postLanguages, langs: toPostLanguages(langPrefs.postLanguage),
}) })
} catch (e: any) { } catch (e: any) {
if (extLink) { if (extLink) {
@@ -235,7 +247,7 @@ export const ComposePost = observer(function ComposePost({
if (!replyTo) { if (!replyTo) {
store.me.mainFeed.onPostCreated() store.me.mainFeed.onPostCreated()
} }
store.preferences.savePostLanguageToHistory() setLangPrefs.savePostLanguageToHistory()
onPost?.() onPost?.()
onClose() onClose()
Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`) Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`)
@@ -244,12 +256,8 @@ export const ComposePost = observer(function ComposePost({
const canPost = useMemo( const canPost = useMemo(
() => () =>
graphemeLength <= MAX_GRAPHEME_LENGTH && graphemeLength <= MAX_GRAPHEME_LENGTH &&
(!store.preferences.requireAltTextEnabled || !gallery.needsAltText), (!requireAltTextEnabled || !gallery.needsAltText),
[ [graphemeLength, requireAltTextEnabled, gallery.needsAltText],
graphemeLength,
store.preferences.requireAltTextEnabled,
gallery.needsAltText,
],
) )
const selectTextInputPlaceholder = replyTo ? 'Write your reply' : `What's up?` const selectTextInputPlaceholder = replyTo ? 'Write your reply' : `What's up?`
@@ -321,7 +329,7 @@ export const ComposePost = observer(function ComposePost({
</> </>
)} )}
</View> </View>
{store.preferences.requireAltTextEnabled && gallery.needsAltText && ( {requireAltTextEnabled && gallery.needsAltText && (
<View style={[styles.reminderLine, pal.viewLight]}> <View style={[styles.reminderLine, pal.viewLight]}>
<View style={styles.errorIcon}> <View style={styles.errorIcon}>
<FontAwesomeIcon <FontAwesomeIcon
+3 -3
View File
@@ -3,13 +3,13 @@ import {Keyboard, StyleSheet} from 'react-native'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {Button} from 'view/com/util/forms/Button' import {Button} from 'view/com/util/forms/Button'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useStores} from 'state/index'
import {ShieldExclamation} from 'lib/icons' import {ShieldExclamation} from 'lib/icons'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
import {isNative} from 'platform/detection' import {isNative} from 'platform/detection'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useModalControls} from '#/state/modals'
export const LabelsBtn = observer(function LabelsBtn({ export const LabelsBtn = observer(function LabelsBtn({
labels, labels,
@@ -22,7 +22,7 @@ export const LabelsBtn = observer(function LabelsBtn({
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const store = useStores() const {openModal} = useModalControls()
return ( return (
<Button <Button
@@ -37,7 +37,7 @@ export const LabelsBtn = observer(function LabelsBtn({
Keyboard.dismiss() Keyboard.dismiss()
} }
} }
store.shell.openModal({name: 'self-label', labels, hasMedia, onChange}) openModal({name: 'self-label', labels, hasMedia, onChange})
}}> }}>
<ShieldExclamation style={pal.link} size={26} /> <ShieldExclamation style={pal.link} size={26} />
{labels.length > 0 ? ( {labels.length > 0 ? (
+22 -6
View File
@@ -7,13 +7,13 @@ import {s, colors} from 'lib/styles'
import {StyleSheet, TouchableOpacity, View} from 'react-native' import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {Image} from 'expo-image' import {Image} from 'expo-image'
import {Text} from 'view/com/util/text/Text' import {Text} from 'view/com/util/text/Text'
import {openAltTextModal} from 'lib/media/alt-text'
import {Dimensions} from 'lib/media/types' import {Dimensions} from 'lib/media/types'
import {useStores} from 'state/index'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {isNative} from 'platform/detection'
const IMAGE_GAP = 8 const IMAGE_GAP = 8
@@ -49,10 +49,10 @@ const GalleryInner = observer(function GalleryImpl({
gallery, gallery,
containerInfo, containerInfo,
}: GalleryInnerProps) { }: GalleryInnerProps) {
const store = useStores()
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const {openModal} = useModalControls()
let side: number let side: number
@@ -120,7 +120,10 @@ const GalleryInner = observer(function GalleryImpl({
accessibilityHint="" accessibilityHint=""
onPress={() => { onPress={() => {
Keyboard.dismiss() Keyboard.dismiss()
openAltTextModal(store, image) openModal({
name: 'alt-text-image',
image,
})
}} }}
style={[styles.altTextControl, altTextControlStyle]}> style={[styles.altTextControl, altTextControlStyle]}>
<Text style={styles.altTextControlLabel} accessible={false}> <Text style={styles.altTextControlLabel} accessible={false}>
@@ -140,7 +143,17 @@ const GalleryInner = observer(function GalleryImpl({
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Edit image`)} accessibilityLabel={_(msg`Edit image`)}
accessibilityHint="" accessibilityHint=""
onPress={() => gallery.edit(image)} onPress={() => {
if (isNative) {
gallery.crop(image)
} else {
openModal({
name: 'edit-image',
image,
gallery,
})
}
}}
style={styles.imageControl}> style={styles.imageControl}>
<FontAwesomeIcon <FontAwesomeIcon
icon="pen" icon="pen"
@@ -168,7 +181,10 @@ const GalleryInner = observer(function GalleryImpl({
accessibilityHint="" accessibilityHint=""
onPress={() => { onPress={() => {
Keyboard.dismiss() Keyboard.dismiss()
openAltTextModal(store, image) openModal({
name: 'alt-text-image',
image,
})
}} }}
style={styles.altTextHiddenRegion} style={styles.altTextHiddenRegion}
/> />
@@ -12,16 +12,24 @@ import {
DropdownItemButton, DropdownItemButton,
} from 'view/com/util/forms/DropdownButton' } from 'view/com/util/forms/DropdownButton'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useStores} from 'state/index'
import {isNative} from 'platform/detection' import {isNative} from 'platform/detection'
import {codeToLanguageName} from '../../../../locale/helpers' import {codeToLanguageName} from '../../../../locale/helpers'
import {useModalControls} from '#/state/modals'
import {
useLanguagePrefs,
useLanguagePrefsApi,
toPostLanguages,
hasPostLanguage,
} from '#/state/preferences/languages'
import {t, msg} from '@lingui/macro' import {t, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
export const SelectLangBtn = observer(function SelectLangBtn() { export const SelectLangBtn = observer(function SelectLangBtn() {
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores()
const {_} = useLingui() const {_} = useLingui()
const {openModal} = useModalControls()
const langPrefs = useLanguagePrefs()
const setLangPrefs = useLanguagePrefsApi()
const onPressMore = useCallback(async () => { const onPressMore = useCallback(async () => {
if (isNative) { if (isNative) {
@@ -29,11 +37,10 @@ export const SelectLangBtn = observer(function SelectLangBtn() {
Keyboard.dismiss() Keyboard.dismiss()
} }
} }
store.shell.openModal({name: 'post-languages-settings'}) openModal({name: 'post-languages-settings'})
}, [store]) }, [openModal])
const postLanguagesPref = store.preferences.postLanguages const postLanguagesPref = toPostLanguages(langPrefs.postLanguage)
const postLanguagePref = store.preferences.postLanguage
const items: DropdownItem[] = useMemo(() => { const items: DropdownItem[] = useMemo(() => {
let arr: DropdownItemButton[] = [] let arr: DropdownItemButton[] = []
@@ -52,13 +59,14 @@ export const SelectLangBtn = observer(function SelectLangBtn() {
arr.push({ arr.push({
icon: icon:
langCodes.every(code => store.preferences.hasPostLanguage(code)) && langCodes.every(code =>
langCodes.length === postLanguagesPref.length hasPostLanguage(langPrefs.postLanguage, code),
) && langCodes.length === postLanguagesPref.length
? ['fas', 'circle-dot'] ? ['fas', 'circle-dot']
: ['far', 'circle'], : ['far', 'circle'],
label: langName, label: langName,
onPress() { onPress() {
store.preferences.setPostLanguage(commaSeparatedLangCodes) setLangPrefs.setPostLanguage(commaSeparatedLangCodes)
}, },
}) })
} }
@@ -68,11 +76,11 @@ export const SelectLangBtn = observer(function SelectLangBtn() {
* Re-join here after sanitization bc postLanguageHistory is an array of * Re-join here after sanitization bc postLanguageHistory is an array of
* comma-separated strings too * comma-separated strings too
*/ */
add(postLanguagePref) add(langPrefs.postLanguage)
} }
// comma-separted strings of lang codes that have been used in the past // comma-separted strings of lang codes that have been used in the past
for (const lang of store.preferences.postLanguageHistory) { for (const lang of langPrefs.postLanguageHistory) {
add(lang) add(lang)
} }
@@ -85,7 +93,7 @@ export const SelectLangBtn = observer(function SelectLangBtn() {
onPress: onPressMore, onPress: onPressMore,
}, },
] ]
}, [store.preferences, onPressMore, postLanguagePref, postLanguagesPref]) }, [onPressMore, langPrefs, setLangPrefs, postLanguagesPref])
return ( return (
<DropdownButton <DropdownButton
+4 -4
View File
@@ -10,12 +10,12 @@ import {observer} from 'mobx-react-lite'
import {FeedSourceModel} from 'state/models/content/feed-source' import {FeedSourceModel} from 'state/models/content/feed-source'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
import {NavigationProp} from 'lib/routes/types' import {NavigationProp} from 'lib/routes/types'
import {useStores} from 'state/index'
import {pluralize} from 'lib/strings/helpers' import {pluralize} from 'lib/strings/helpers'
import {AtUri} from '@atproto/api' import {AtUri} from '@atproto/api'
import * as Toast from 'view/com/util/Toast' import * as Toast from 'view/com/util/Toast'
import {sanitizeHandle} from 'lib/strings/handles' import {sanitizeHandle} from 'lib/strings/handles'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useModalControls} from '#/state/modals'
export const FeedSourceCard = observer(function FeedSourceCardImpl({ export const FeedSourceCard = observer(function FeedSourceCardImpl({
item, item,
@@ -30,13 +30,13 @@ export const FeedSourceCard = observer(function FeedSourceCardImpl({
showDescription?: boolean showDescription?: boolean
showLikes?: boolean showLikes?: boolean
}) { }) {
const store = useStores()
const pal = usePalette('default') const pal = usePalette('default')
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const {openModal} = useModalControls()
const onToggleSaved = React.useCallback(async () => { const onToggleSaved = React.useCallback(async () => {
if (item.isSaved) { if (item.isSaved) {
store.shell.openModal({ openModal({
name: 'confirm', name: 'confirm',
title: 'Remove from my feeds', title: 'Remove from my feeds',
message: `Remove ${item.displayName} from my feeds?`, message: `Remove ${item.displayName} from my feeds?`,
@@ -59,7 +59,7 @@ export const FeedSourceCard = observer(function FeedSourceCardImpl({
logger.error('Failed to save feed', {error: e}) logger.error('Failed to save feed', {error: e})
} }
} }
}, [store, item]) }, [openModal, item])
return ( return (
<Pressable <Pressable
+4 -4
View File
@@ -17,11 +17,11 @@ import {Button} from '../util/forms/Button'
import {ListModel} from 'state/models/content/list' import {ListModel} from 'state/models/content/list'
import {useAnalytics} from 'lib/analytics/analytics' import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useStores} from 'state/index'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {OnScrollCb} from 'lib/hooks/useOnMainScroll' import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useModalControls} from '#/state/modals'
const LOADING_ITEM = {_reactKey: '__loading__'} const LOADING_ITEM = {_reactKey: '__loading__'}
const EMPTY_ITEM = {_reactKey: '__empty__'} const EMPTY_ITEM = {_reactKey: '__empty__'}
@@ -54,10 +54,10 @@ export const ListItems = observer(function ListItemsImpl({
desktopFixedHeightOffset?: number desktopFixedHeightOffset?: number
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores()
const {track} = useAnalytics() const {track} = useAnalytics()
const [isRefreshing, setIsRefreshing] = React.useState(false) const [isRefreshing, setIsRefreshing] = React.useState(false)
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const {openModal} = useModalControls()
const data = React.useMemo(() => { const data = React.useMemo(() => {
let items: any[] = [] let items: any[] = []
@@ -115,7 +115,7 @@ export const ListItems = observer(function ListItemsImpl({
const onPressEditMembership = React.useCallback( const onPressEditMembership = React.useCallback(
(profile: AppBskyActorDefs.ProfileViewBasic) => { (profile: AppBskyActorDefs.ProfileViewBasic) => {
store.shell.openModal({ openModal({
name: 'user-add-remove-lists', name: 'user-add-remove-lists',
subject: profile.did, subject: profile.did,
displayName: profile.displayName || profile.handle, displayName: profile.displayName || profile.handle,
@@ -131,7 +131,7 @@ export const ListItems = observer(function ListItemsImpl({
}, },
}) })
}, },
[store, list], [openModal, list],
) )
// rendering // rendering
+4 -2
View File
@@ -15,6 +15,7 @@ import * as Toast from '../util/Toast'
import {logger} from '#/logger' import {logger} from '#/logger'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
export const snapPoints = ['70%'] export const snapPoints = ['70%']
@@ -57,6 +58,7 @@ export function Component({}: {}) {
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores() const store = useStores()
const {_} = useLingui() const {_} = useLingui()
const {closeModal} = useModalControls()
const [name, setName] = useState( const [name, setName] = useState(
shadesOfBlue[Math.floor(Math.random() * shadesOfBlue.length)], shadesOfBlue[Math.floor(Math.random() * shadesOfBlue.length)],
) )
@@ -72,8 +74,8 @@ export function Component({}: {}) {
}, [appPassword]) }, [appPassword])
const onDone = React.useCallback(() => { const onDone = React.useCallback(() => {
store.shell.closeModal() closeModal()
}, [store]) }, [closeModal])
const createAppPassword = async () => { const createAppPassword = async () => {
// if name is all whitespace, we don't allow it // if name is all whitespace, we don't allow it
+5 -5
View File
@@ -17,11 +17,11 @@ import {MAX_ALT_TEXT} from 'lib/constants'
import {useTheme} from 'lib/ThemeContext' import {useTheme} from 'lib/ThemeContext'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import LinearGradient from 'react-native-linear-gradient' import LinearGradient from 'react-native-linear-gradient'
import {useStores} from 'state/index'
import {isAndroid, isWeb} from 'platform/detection' import {isAndroid, isWeb} from 'platform/detection'
import {ImageModel} from 'state/models/media/image' import {ImageModel} from 'state/models/media/image'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useModalControls} from '#/state/modals'
export const snapPoints = ['fullscreen'] export const snapPoints = ['fullscreen']
@@ -31,11 +31,11 @@ interface Props {
export function Component({image}: Props) { export function Component({image}: Props) {
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores()
const theme = useTheme() const theme = useTheme()
const {_} = useLingui() const {_} = useLingui()
const [altText, setAltText] = useState(image.altText) const [altText, setAltText] = useState(image.altText)
const windim = useWindowDimensions() const windim = useWindowDimensions()
const {closeModal} = useModalControls()
const imageStyles = useMemo<ImageStyle>(() => { const imageStyles = useMemo<ImageStyle>(() => {
const maxWidth = isWeb ? 450 : windim.width const maxWidth = isWeb ? 450 : windim.width
@@ -56,11 +56,11 @@ export function Component({image}: Props) {
const onPressSave = useCallback(() => { const onPressSave = useCallback(() => {
image.setAltText(altText) image.setAltText(altText)
store.shell.closeModal() closeModal()
}, [store, image, altText]) }, [closeModal, image, altText])
const onPressCancel = () => { const onPressCancel = () => {
store.shell.closeModal() closeModal()
} }
return ( return (
+3 -1
View File
@@ -17,6 +17,7 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {cleanError} from 'lib/strings/errors' import {cleanError} from 'lib/strings/errors'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
export const snapPoints = ['50%'] export const snapPoints = ['50%']
@@ -24,6 +25,7 @@ export const Component = observer(function Component({}: {}) {
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores() const store = useStores()
const {_} = useLingui() const {_} = useLingui()
const {closeModal} = useModalControls()
const [date, setDate] = useState<Date>( const [date, setDate] = useState<Date>(
store.preferences.birthDate || new Date(), store.preferences.birthDate || new Date(),
) )
@@ -36,7 +38,7 @@ export const Component = observer(function Component({}: {}) {
setIsProcessing(true) setIsProcessing(true)
try { try {
await store.preferences.setBirthDate(date) await store.preferences.setBirthDate(date)
store.shell.closeModal() closeModal()
} catch (e) { } catch (e) {
setError(cleanError(String(e))) setError(cleanError(String(e)))
} finally { } finally {
+5 -3
View File
@@ -14,6 +14,7 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {cleanError} from 'lib/strings/errors' import {cleanError} from 'lib/strings/errors'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
enum Stages { enum Stages {
InputEmail, InputEmail,
@@ -35,6 +36,7 @@ export const Component = observer(function Component({}: {}) {
const [isProcessing, setIsProcessing] = useState<boolean>(false) const [isProcessing, setIsProcessing] = useState<boolean>(false)
const [error, setError] = useState<string>('') const [error, setError] = useState<string>('')
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const {openModal, closeModal} = useModalControls()
const onRequestChange = async () => { const onRequestChange = async () => {
if (email === store.session.currentSession?.email) { if (email === store.session.currentSession?.email) {
@@ -95,8 +97,8 @@ export const Component = observer(function Component({}: {}) {
} }
const onVerify = async () => { const onVerify = async () => {
store.shell.closeModal() closeModal()
store.shell.openModal({name: 'verify-email'}) openModal({name: 'verify-email'})
} }
return ( return (
@@ -212,7 +214,7 @@ export const Component = observer(function Component({}: {}) {
<Button <Button
testID="cancelBtn" testID="cancelBtn"
type="default" type="default"
onPress={() => store.shell.closeModal()} onPress={() => closeModal()}
accessibilityLabel={_(msg`Cancel`)} accessibilityLabel={_(msg`Cancel`)}
accessibilityHint="" accessibilityHint=""
label={_(msg`Cancel`)} label={_(msg`Cancel`)}
+6 -3
View File
@@ -24,6 +24,7 @@ import {cleanError} from 'lib/strings/errors'
import {logger} from '#/logger' import {logger} from '#/logger'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
export const snapPoints = ['100%'] export const snapPoints = ['100%']
@@ -33,6 +34,7 @@ export function Component({onChanged}: {onChanged: () => void}) {
const pal = usePalette('default') const pal = usePalette('default')
const {track} = useAnalytics() const {track} = useAnalytics()
const {_} = useLingui() const {_} = useLingui()
const {closeModal} = useModalControls()
const [isProcessing, setProcessing] = useState<boolean>(false) const [isProcessing, setProcessing] = useState<boolean>(false)
const [retryDescribeTrigger, setRetryDescribeTrigger] = React.useState<any>( const [retryDescribeTrigger, setRetryDescribeTrigger] = React.useState<any>(
@@ -88,8 +90,8 @@ export function Component({onChanged}: {onChanged: () => void}) {
// events // events
// = // =
const onPressCancel = React.useCallback(() => { const onPressCancel = React.useCallback(() => {
store.shell.closeModal() closeModal()
}, [store]) }, [closeModal])
const onPressRetryConnect = React.useCallback( const onPressRetryConnect = React.useCallback(
() => setRetryDescribeTrigger({}), () => setRetryDescribeTrigger({}),
[setRetryDescribeTrigger], [setRetryDescribeTrigger],
@@ -113,7 +115,7 @@ export function Component({onChanged}: {onChanged: () => void}) {
await store.agent.updateHandle({ await store.agent.updateHandle({
handle: newHandle, handle: newHandle,
}) })
store.shell.closeModal() closeModal()
onChanged() onChanged()
} catch (err: any) { } catch (err: any) {
setError(cleanError(err)) setError(cleanError(err))
@@ -130,6 +132,7 @@ export function Component({onChanged}: {onChanged: () => void}) {
isCustom, isCustom,
onChanged, onChanged,
track, track,
closeModal,
]) ])
// rendering // rendering
+4 -5
View File
@@ -6,15 +6,15 @@ import {
View, View,
} from 'react-native' } from 'react-native'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {useStores} from 'state/index'
import {s, colors} from 'lib/styles' import {s, colors} from 'lib/styles'
import {ErrorMessage} from '../util/error/ErrorMessage' import {ErrorMessage} from '../util/error/ErrorMessage'
import {cleanError} from 'lib/strings/errors' import {cleanError} from 'lib/strings/errors'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
import type {ConfirmModal} from 'state/models/ui/shell'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import type {ConfirmModal} from '#/state/modals'
import {useModalControls} from '#/state/modals'
export const snapPoints = ['50%'] export const snapPoints = ['50%']
@@ -28,9 +28,8 @@ export function Component({
cancelBtnText, cancelBtnText,
}: ConfirmModal) { }: ConfirmModal) {
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores()
const {_} = useLingui() const {_} = useLingui()
const {closeModal} = useModalControls()
const [isProcessing, setIsProcessing] = useState<boolean>(false) const [isProcessing, setIsProcessing] = useState<boolean>(false)
const [error, setError] = useState<string>('') const [error, setError] = useState<string>('')
const onPress = async () => { const onPress = async () => {
@@ -38,7 +37,7 @@ export function Component({
setIsProcessing(true) setIsProcessing(true)
try { try {
await onPressConfirm() await onPressConfirm()
store.shell.closeModal() closeModal()
return return
} catch (e: any) { } catch (e: any) {
setError(cleanError(e)) setError(cleanError(e))
@@ -18,6 +18,7 @@ import * as Toast from '../util/Toast'
import {logger} from '#/logger' import {logger} from '#/logger'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
export const snapPoints = ['90%'] export const snapPoints = ['90%']
@@ -27,14 +28,15 @@ export const Component = observer(
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const {closeModal} = useModalControls()
React.useEffect(() => { React.useEffect(() => {
store.preferences.sync() store.preferences.sync()
}, [store]) }, [store])
const onPressDone = React.useCallback(() => { const onPressDone = React.useCallback(() => {
store.shell.closeModal() closeModal()
}, [store]) }, [closeModal])
return ( return (
<View testID="contentFilteringModal" style={[pal.view, styles.container]}> <View testID="contentFilteringModal" style={[pal.view, styles.container]}>
@@ -96,8 +98,9 @@ const AdultContentEnabledPref = observer(
function AdultContentEnabledPrefImpl() { function AdultContentEnabledPrefImpl() {
const store = useStores() const store = useStores()
const pal = usePalette('default') const pal = usePalette('default')
const {openModal} = useModalControls()
const onSetAge = () => store.shell.openModal({name: 'birth-date-settings'}) const onSetAge = () => openModal({name: 'birth-date-settings'})
const onToggleAdultContent = async () => { const onToggleAdultContent = async () => {
if (isIOS) { if (isIOS) {
+6 -3
View File
@@ -26,6 +26,7 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {cleanError, isNetworkError} from 'lib/strings/errors' import {cleanError, isNetworkError} from 'lib/strings/errors'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
const MAX_NAME = 64 // todo const MAX_NAME = 64 // todo
const MAX_DESCRIPTION = 300 // todo const MAX_DESCRIPTION = 300 // todo
@@ -42,6 +43,7 @@ export function Component({
list?: ListModel list?: ListModel
}) { }) {
const store = useStores() const store = useStores()
const {closeModal} = useModalControls()
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const [error, setError] = useState<string>('') const [error, setError] = useState<string>('')
const pal = usePalette('default') const pal = usePalette('default')
@@ -70,8 +72,8 @@ export function Component({
const [newAvatar, setNewAvatar] = useState<RNImage | undefined | null>() const [newAvatar, setNewAvatar] = useState<RNImage | undefined | null>()
const onPressCancel = useCallback(() => { const onPressCancel = useCallback(() => {
store.shell.closeModal() closeModal()
}, [store]) }, [closeModal])
const onSelectNewAvatar = useCallback( const onSelectNewAvatar = useCallback(
async (img: RNImage | null) => { async (img: RNImage | null) => {
@@ -126,7 +128,7 @@ export function Component({
Toast.show(`${purposeLabel} list created`) Toast.show(`${purposeLabel} list created`)
onSave?.(res.uri) onSave?.(res.uri)
} }
store.shell.closeModal() closeModal()
} catch (e: any) { } catch (e: any) {
if (isNetworkError(e)) { if (isNetworkError(e)) {
setError( setError(
@@ -144,6 +146,7 @@ export function Component({
error, error,
onSave, onSave,
store, store,
closeModal,
activePurpose, activePurpose,
isCurateList, isCurateList,
purposeLabel, purposeLabel,
+4 -2
View File
@@ -19,6 +19,7 @@ import {cleanError} from 'lib/strings/errors'
import {resetToTab} from '../../../Navigation' import {resetToTab} from '../../../Navigation'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
export const snapPoints = ['60%'] export const snapPoints = ['60%']
@@ -27,6 +28,7 @@ export function Component({}: {}) {
const theme = useTheme() const theme = useTheme()
const store = useStores() const store = useStores()
const {_} = useLingui() const {_} = useLingui()
const {closeModal} = useModalControls()
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const [isEmailSent, setIsEmailSent] = React.useState<boolean>(false) const [isEmailSent, setIsEmailSent] = React.useState<boolean>(false)
const [confirmCode, setConfirmCode] = React.useState<string>('') const [confirmCode, setConfirmCode] = React.useState<string>('')
@@ -58,14 +60,14 @@ export function Component({}: {}) {
Toast.show('Your account has been deleted') Toast.show('Your account has been deleted')
resetToTab('HomeTab') resetToTab('HomeTab')
store.session.clear() store.session.clear()
store.shell.closeModal() closeModal()
} catch (e: any) { } catch (e: any) {
setError(cleanError(e)) setError(cleanError(e))
} }
setIsProcessing(false) setIsProcessing(false)
} }
const onCancel = () => { const onCancel = () => {
store.shell.closeModal() closeModal()
} }
return ( return (
<View style={[styles.container, pal.view]}> <View style={[styles.container, pal.view]}>
+4 -4
View File
@@ -6,7 +6,6 @@ import {gradients, s} from 'lib/styles'
import {useTheme} from 'lib/ThemeContext' import {useTheme} from 'lib/ThemeContext'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import LinearGradient from 'react-native-linear-gradient' import LinearGradient from 'react-native-linear-gradient'
import {useStores} from 'state/index'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import ImageEditor, {Position} from 'react-avatar-editor' import ImageEditor, {Position} from 'react-avatar-editor'
import {TextInput} from './util' import {TextInput} from './util'
@@ -21,6 +20,7 @@ import {observer} from 'mobx-react-lite'
import {getKeys} from 'lib/type-assertions' import {getKeys} from 'lib/type-assertions'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
export const snapPoints = ['80%'] export const snapPoints = ['80%']
@@ -54,10 +54,10 @@ export const Component = observer(function EditImageImpl({
}: Props) { }: Props) {
const pal = usePalette('default') const pal = usePalette('default')
const theme = useTheme() const theme = useTheme()
const store = useStores()
const {_} = useLingui() const {_} = useLingui()
const windowDimensions = useWindowDimensions() const windowDimensions = useWindowDimensions()
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const {closeModal} = useModalControls()
const { const {
aspectRatio, aspectRatio,
@@ -131,8 +131,8 @@ export const Component = observer(function EditImageImpl({
}, [image]) }, [image])
const onCloseModal = useCallback(() => { const onCloseModal = useCallback(() => {
store.shell.closeModal() closeModal()
}, [store.shell]) }, [closeModal])
const onPressCancel = useCallback(async () => { const onPressCancel = useCallback(async () => {
await gallery.previous(image) await gallery.previous(image)
+5 -5
View File
@@ -13,7 +13,6 @@ import LinearGradient from 'react-native-linear-gradient'
import {Image as RNImage} from 'react-native-image-crop-picker' import {Image as RNImage} from 'react-native-image-crop-picker'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {ErrorMessage} from '../util/error/ErrorMessage' import {ErrorMessage} from '../util/error/ErrorMessage'
import {useStores} from 'state/index'
import {ProfileModel} from 'state/models/content/profile' import {ProfileModel} from 'state/models/content/profile'
import {s, colors, gradients} from 'lib/styles' import {s, colors, gradients} from 'lib/styles'
import {enforceLen} from 'lib/strings/helpers' import {enforceLen} from 'lib/strings/helpers'
@@ -29,6 +28,7 @@ import Animated, {FadeOut} from 'react-native-reanimated'
import {isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
const AnimatedTouchableOpacity = const AnimatedTouchableOpacity =
Animated.createAnimatedComponent(TouchableOpacity) Animated.createAnimatedComponent(TouchableOpacity)
@@ -42,12 +42,12 @@ export function Component({
profileView: ProfileModel profileView: ProfileModel
onUpdate?: () => void onUpdate?: () => void
}) { }) {
const store = useStores()
const [error, setError] = useState<string>('') const [error, setError] = useState<string>('')
const pal = usePalette('default') const pal = usePalette('default')
const theme = useTheme() const theme = useTheme()
const {track} = useAnalytics() const {track} = useAnalytics()
const {_} = useLingui() const {_} = useLingui()
const {closeModal} = useModalControls()
const [isProcessing, setProcessing] = useState<boolean>(false) const [isProcessing, setProcessing] = useState<boolean>(false)
const [displayName, setDisplayName] = useState<string>( const [displayName, setDisplayName] = useState<string>(
@@ -69,7 +69,7 @@ export function Component({
RNImage | undefined | null RNImage | undefined | null
>() >()
const onPressCancel = () => { const onPressCancel = () => {
store.shell.closeModal() closeModal()
} }
const onSelectNewAvatar = useCallback( const onSelectNewAvatar = useCallback(
async (img: RNImage | null) => { async (img: RNImage | null) => {
@@ -126,7 +126,7 @@ export function Component({
) )
Toast.show('Profile updated') Toast.show('Profile updated')
onUpdate?.() onUpdate?.()
store.shell.closeModal() closeModal()
} catch (e: any) { } catch (e: any) {
if (isNetworkError(e)) { if (isNetworkError(e)) {
setError( setError(
@@ -144,7 +144,7 @@ export function Component({
error, error,
profileView, profileView,
onUpdate, onUpdate,
store, closeModal,
displayName, displayName,
description, description,
newUserAvatar, newUserAvatar,
+83 -44
View File
@@ -1,6 +1,7 @@
import React from 'react' import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native' import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {ComAtprotoServerDefs} from '@atproto/api'
import { import {
FontAwesomeIcon, FontAwesomeIcon,
FontAwesomeIconStyle, FontAwesomeIconStyle,
@@ -15,25 +16,33 @@ import {usePalette} from 'lib/hooks/usePalette'
import {isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {Trans} from '@lingui/macro' import {Trans} from '@lingui/macro'
import {useModalControls} from '#/state/modals'
import {useInvitesState, useInvitesAPI} from '#/state/invites'
import {UserInfoText} from '../util/UserInfoText'
import {makeProfileLink} from '#/lib/routes/links'
import {Link} from '../util/Link'
export const snapPoints = ['70%'] export const snapPoints = ['70%']
export function Component({}: {}) { export function Component({}: {}) {
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores() const store = useStores()
const {closeModal} = useModalControls()
const {isTabletOrDesktop} = useWebMediaQueries() const {isTabletOrDesktop} = useWebMediaQueries()
const onClose = React.useCallback(() => { const onClose = React.useCallback(() => {
store.shell.closeModal() closeModal()
}, [store]) }, [closeModal])
if (store.me.invites.length === 0) { if (store.me.invites.length === 0) {
return ( return (
<View style={[styles.container, pal.view]} testID="inviteCodesModal"> <View style={[styles.container, pal.view]} testID="inviteCodesModal">
<View style={[styles.empty, pal.viewLight]}> <View style={[styles.empty, pal.viewLight]}>
<Text type="lg" style={[pal.text, styles.emptyText]}> <Text type="lg" style={[pal.text, styles.emptyText]}>
You don't have any invite codes yet! We'll send you some when you've <Trans>
been on Bluesky for a little longer. You don't have any invite codes yet! We'll send you some when
you've been on Bluesky for a little longer.
</Trans>
</Text> </Text>
</View> </View>
<View style={styles.flex1} /> <View style={styles.flex1} />
@@ -57,17 +66,19 @@ export function Component({}: {}) {
return ( return (
<View style={[styles.container, pal.view]} testID="inviteCodesModal"> <View style={[styles.container, pal.view]} testID="inviteCodesModal">
<Text type="title-xl" style={[styles.title, pal.text]}> <Text type="title-xl" style={[styles.title, pal.text]}>
Invite a Friend <Trans>Invite a Friend</Trans>
</Text> </Text>
<Text type="lg" style={[styles.description, pal.text]}> <Text type="lg" style={[styles.description, pal.text]}>
Each code works once. You'll receive more invite codes periodically. <Trans>
Each code works once. You'll receive more invite codes periodically.
</Trans>
</Text> </Text>
<ScrollView style={[styles.scrollContainer, pal.border]}> <ScrollView style={[styles.scrollContainer, pal.border]}>
{store.me.invites.map((invite, i) => ( {store.me.invites.map((invite, i) => (
<InviteCode <InviteCode
testID={`inviteCode-${i}`} testID={`inviteCode-${i}`}
key={invite.code} key={invite.code}
code={invite.code} invite={invite}
used={invite.available - invite.uses.length <= 0 || invite.disabled} used={invite.available - invite.uses.length <= 0 || invite.disabled}
/> />
))} ))}
@@ -88,54 +99,85 @@ export function Component({}: {}) {
const InviteCode = observer(function InviteCodeImpl({ const InviteCode = observer(function InviteCodeImpl({
testID, testID,
code, invite,
used, used,
}: { }: {
testID: string testID: string
code: string invite: ComAtprotoServerDefs.InviteCode
used?: boolean used?: boolean
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores() const store = useStores()
const {invitesAvailable} = store.me const {invitesAvailable} = store.me
const invitesState = useInvitesState()
const {setInviteCopied} = useInvitesAPI()
const onPress = React.useCallback(() => { const onPress = React.useCallback(() => {
Clipboard.setString(code) Clipboard.setString(invite.code)
Toast.show('Copied to clipboard') Toast.show('Copied to clipboard')
store.invitedUsers.setInviteCopied(code) setInviteCopied(invite.code)
}, [store, code]) }, [setInviteCopied, invite])
return ( return (
<TouchableOpacity <View
testID={testID} style={[
style={[styles.inviteCode, pal.border]} pal.border,
onPress={onPress} {borderBottomWidth: 1, paddingHorizontal: 20, paddingVertical: 14},
accessibilityRole="button" ]}>
accessibilityLabel={ <TouchableOpacity
invitesAvailable === 1 testID={testID}
? 'Invite codes: 1 available' style={[styles.inviteCode]}
: `Invite codes: ${invitesAvailable} available` onPress={onPress}
} accessibilityRole="button"
accessibilityHint="Opens list of invite codes"> accessibilityLabel={
<Text invitesAvailable === 1
testID={`${testID}-code`} ? 'Invite codes: 1 available'
type={used ? 'md' : 'md-bold'} : `Invite codes: ${invitesAvailable} available`
style={used ? [pal.textLight, styles.strikeThrough] : pal.text}> }
{code} accessibilityHint="Opens list of invite codes">
</Text> <Text
<View style={styles.flex1} /> testID={`${testID}-code`}
{!used && store.invitedUsers.isInviteCopied(code) && ( type={used ? 'md' : 'md-bold'}
<Text style={[pal.textLight, styles.codeCopied]}> style={used ? [pal.textLight, styles.strikeThrough] : pal.text}>
<Trans>Copied</Trans> {invite.code}
</Text> </Text>
)} <View style={styles.flex1} />
{!used && ( {!used && invitesState.copiedInvites.includes(invite.code) && (
<FontAwesomeIcon <Text style={[pal.textLight, styles.codeCopied]}>
icon={['far', 'clone']} <Trans>Copied</Trans>
style={pal.text as FontAwesomeIconStyle} </Text>
/> )}
)} {!used && (
</TouchableOpacity> <FontAwesomeIcon
icon={['far', 'clone']}
style={pal.text as FontAwesomeIconStyle}
/>
)}
</TouchableOpacity>
{invite.uses.length > 0 ? (
<View
style={{
flexDirection: 'column',
gap: 8,
paddingTop: 6,
}}>
<Text style={pal.text}>
<Trans>Used by:</Trans>
</Text>
{invite.uses.map(use => (
<Link
key={use.usedBy}
href={makeProfileLink({handle: use.usedBy, did: ''})}
style={{
flexDirection: 'row',
}}>
<Text style={pal.text}> </Text>
<UserInfoText did={use.usedBy} style={pal.link} />
</Link>
))}
</View>
) : null}
</View>
) )
}) })
@@ -179,9 +221,6 @@ const styles = StyleSheet.create({
inviteCode: { inviteCode: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
borderBottomWidth: 1,
paddingHorizontal: 20,
paddingVertical: 14,
}, },
codeCopied: { codeCopied: {
marginRight: 8, marginRight: 8,
+4 -4
View File
@@ -5,7 +5,6 @@ import {observer} from 'mobx-react-lite'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {Button} from '../util/forms/Button' import {Button} from '../util/forms/Button'
import {useStores} from 'state/index'
import {s, colors} from 'lib/styles' import {s, colors} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
@@ -13,6 +12,7 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {isPossiblyAUrl, splitApexDomain} from 'lib/strings/url-helpers' import {isPossiblyAUrl, splitApexDomain} from 'lib/strings/url-helpers'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
export const snapPoints = ['50%'] export const snapPoints = ['50%']
@@ -24,13 +24,13 @@ export const Component = observer(function Component({
href: string href: string
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores() const {closeModal} = useModalControls()
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const {_} = useLingui() const {_} = useLingui()
const potentiallyMisleading = isPossiblyAUrl(text) const potentiallyMisleading = isPossiblyAUrl(text)
const onPressVisit = () => { const onPressVisit = () => {
store.shell.closeModal() closeModal()
Linking.openURL(href) Linking.openURL(href)
} }
@@ -86,7 +86,7 @@ export const Component = observer(function Component({
<Button <Button
testID="cancelBtn" testID="cancelBtn"
type="default" type="default"
onPress={() => store.shell.closeModal()} onPress={() => closeModal()}
accessibilityLabel={_(msg`Cancel`)} accessibilityLabel={_(msg`Cancel`)}
accessibilityHint="" accessibilityHint=""
label="Cancel" label="Cancel"
+3 -1
View File
@@ -28,6 +28,7 @@ import {sanitizeHandle} from 'lib/strings/handles'
import {HITSLOP_20} from '#/lib/constants' import {HITSLOP_20} from '#/lib/constants'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
export const snapPoints = ['90%'] export const snapPoints = ['90%']
@@ -41,6 +42,7 @@ export const Component = observer(function Component({
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores() const store = useStores()
const {_} = useLingui() const {_} = useLingui()
const {closeModal} = useModalControls()
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const [query, setQuery] = useState('') const [query, setQuery] = useState('')
const autocompleteView = useMemo<UserAutocompleteModel>( const autocompleteView = useMemo<UserAutocompleteModel>(
@@ -149,7 +151,7 @@ export const Component = observer(function Component({
<Button <Button
testID="doneBtn" testID="doneBtn"
type="default" type="default"
onPress={() => store.shell.closeModal()} onPress={() => closeModal()}
accessibilityLabel={_(msg`Done`)} accessibilityLabel={_(msg`Done`)}
accessibilityHint="" accessibilityHint=""
label="Done" label="Done"
+12 -12
View File
@@ -3,13 +3,13 @@ import {StyleSheet} from 'react-native'
import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context' import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import BottomSheet from '@gorhom/bottom-sheet' import BottomSheet from '@gorhom/bottom-sheet'
import {useStores} from 'state/index'
import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop' import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {timeout} from 'lib/async/timeout' import {timeout} from 'lib/async/timeout'
import {navigate} from '../../../Navigation' import {navigate} from '../../../Navigation'
import once from 'lodash.once' import once from 'lodash.once'
import {useModals, useModalControls} from '#/state/modals'
import * as ConfirmModal from './Confirm' import * as ConfirmModal from './Confirm'
import * as EditProfileModal from './EditProfile' import * as EditProfileModal from './EditProfile'
import * as ProfilePreviewModal from './ProfilePreview' import * as ProfilePreviewModal from './ProfilePreview'
@@ -41,17 +41,17 @@ const DEFAULT_SNAPPOINTS = ['90%']
const HANDLE_HEIGHT = 24 const HANDLE_HEIGHT = 24
export const ModalsContainer = observer(function ModalsContainer() { export const ModalsContainer = observer(function ModalsContainer() {
const store = useStores() const {isModalActive, activeModals} = useModals()
const {closeModal} = useModalControls()
const bottomSheetRef = useRef<BottomSheet>(null) const bottomSheetRef = useRef<BottomSheet>(null)
const pal = usePalette('default') const pal = usePalette('default')
const safeAreaInsets = useSafeAreaInsets() const safeAreaInsets = useSafeAreaInsets()
const activeModal = const activeModal = activeModals[activeModals.length - 1]
store.shell.activeModals[store.shell.activeModals.length - 1]
const navigateOnce = once(navigate) const navigateOnce = once(navigate)
const onBottomSheetAnimate = (fromIndex: number, toIndex: number) => { const onBottomSheetAnimate = (_fromIndex: number, toIndex: number) => {
if (activeModal?.name === 'profile-preview' && toIndex === 1) { if (activeModal?.name === 'profile-preview' && toIndex === 1) {
// begin loading the profile screen behind the scenes // begin loading the profile screen behind the scenes
navigateOnce('Profile', {name: activeModal.did}) navigateOnce('Profile', {name: activeModal.did})
@@ -59,7 +59,7 @@ export const ModalsContainer = observer(function ModalsContainer() {
} }
const onBottomSheetChange = async (snapPoint: number) => { const onBottomSheetChange = async (snapPoint: number) => {
if (snapPoint === -1) { if (snapPoint === -1) {
store.shell.closeModal() closeModal()
} else if (activeModal?.name === 'profile-preview' && snapPoint === 1) { } else if (activeModal?.name === 'profile-preview' && snapPoint === 1) {
await navigateOnce('Profile', {name: activeModal.did}) await navigateOnce('Profile', {name: activeModal.did})
// There is no particular callback for when the view has actually been presented. // There is no particular callback for when the view has actually been presented.
@@ -67,21 +67,21 @@ export const ModalsContainer = observer(function ModalsContainer() {
// It's acceptable because the data is already being fetched + it usually takes longer anyway. // It's acceptable because the data is already being fetched + it usually takes longer anyway.
// TODO: Figure out why avatar/cover don't always show instantly from cache. // TODO: Figure out why avatar/cover don't always show instantly from cache.
await timeout(200) await timeout(200)
store.shell.closeModal() closeModal()
} }
} }
const onClose = () => { const onClose = () => {
bottomSheetRef.current?.close() bottomSheetRef.current?.close()
store.shell.closeModal() closeModal()
} }
useEffect(() => { useEffect(() => {
if (store.shell.isModalActive) { if (isModalActive) {
bottomSheetRef.current?.expand() bottomSheetRef.current?.expand()
} else { } else {
bottomSheetRef.current?.close() bottomSheetRef.current?.close()
} }
}, [store.shell.isModalActive, bottomSheetRef, activeModal?.name]) }, [isModalActive, bottomSheetRef, activeModal?.name])
let needsSafeTopInset = false let needsSafeTopInset = false
let snapPoints: (string | number)[] = DEFAULT_SNAPPOINTS let snapPoints: (string | number)[] = DEFAULT_SNAPPOINTS
@@ -184,12 +184,12 @@ export const ModalsContainer = observer(function ModalsContainer() {
snapPoints={snapPoints} snapPoints={snapPoints}
topInset={topInset} topInset={topInset}
handleHeight={HANDLE_HEIGHT} handleHeight={HANDLE_HEIGHT}
index={store.shell.isModalActive ? 0 : -1} index={isModalActive ? 0 : -1}
enablePanDownToClose enablePanDownToClose
android_keyboardInputMode="adjustResize" android_keyboardInputMode="adjustResize"
keyboardBlurBehavior="restore" keyboardBlurBehavior="restore"
backdropComponent={ backdropComponent={
store.shell.isModalActive ? createCustomBackdrop(onClose) : undefined isModalActive ? createCustomBackdrop(onClose) : undefined
} }
handleIndicatorStyle={{backgroundColor: pal.text.color}} handleIndicatorStyle={{backgroundColor: pal.text.color}}
handleStyle={[styles.handle, pal.view]} handleStyle={[styles.handle, pal.view]}
+9 -8
View File
@@ -1,11 +1,11 @@
import React from 'react' import React from 'react'
import {TouchableWithoutFeedback, StyleSheet, View} from 'react-native' import {TouchableWithoutFeedback, StyleSheet, View} from 'react-native'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {useStores} from 'state/index'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import type {Modal as ModalIface} from 'state/models/ui/shell' import type {Modal as ModalIface} from '#/state/modals'
import {useModals, useModalControls} from '#/state/modals'
import * as ConfirmModal from './Confirm' import * as ConfirmModal from './Confirm'
import * as EditProfileModal from './EditProfile' import * as EditProfileModal from './EditProfile'
import * as ProfilePreviewModal from './ProfilePreview' import * as ProfilePreviewModal from './ProfilePreview'
@@ -34,15 +34,15 @@ import * as ChangeEmailModal from './ChangeEmail'
import * as LinkWarningModal from './LinkWarning' import * as LinkWarningModal from './LinkWarning'
export const ModalsContainer = observer(function ModalsContainer() { export const ModalsContainer = observer(function ModalsContainer() {
const store = useStores() const {isModalActive, activeModals} = useModals()
if (!store.shell.isModalActive) { if (!isModalActive) {
return null return null
} }
return ( return (
<> <>
{store.shell.activeModals.map((modal, i) => ( {activeModals.map((modal, i) => (
<Modal key={`modal-${i}`} modal={modal} /> <Modal key={`modal-${i}`} modal={modal} />
))} ))}
</> </>
@@ -50,11 +50,12 @@ export const ModalsContainer = observer(function ModalsContainer() {
}) })
function Modal({modal}: {modal: ModalIface}) { function Modal({modal}: {modal: ModalIface}) {
const store = useStores() const {isModalActive} = useModals()
const {closeModal} = useModalControls()
const pal = usePalette('default') const pal = usePalette('default')
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
if (!store.shell.isModalActive) { if (!isModalActive) {
return null return null
} }
@@ -62,7 +63,7 @@ function Modal({modal}: {modal: ModalIface}) {
if (modal.name === 'crop-image' || modal.name === 'edit-image') { if (modal.name === 'crop-image' || modal.name === 'edit-image') {
return // dont close on mask presses during crop return // dont close on mask presses during crop
} }
store.shell.closeModal() closeModal()
} }
const onInnerPress = () => { const onInnerPress = () => {
// TODO: can we use prevent default? // TODO: can we use prevent default?
+3 -6
View File
@@ -1,7 +1,6 @@
import React from 'react' import React from 'react'
import {StyleSheet, View} from 'react-native' import {StyleSheet, View} from 'react-native'
import {ModerationUI} from '@atproto/api' import {ModerationUI} from '@atproto/api'
import {useStores} from 'state/index'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
@@ -10,6 +9,7 @@ import {usePalette} from 'lib/hooks/usePalette'
import {isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
import {listUriToHref} from 'lib/strings/url-helpers' import {listUriToHref} from 'lib/strings/url-helpers'
import {Button} from '../util/forms/Button' import {Button} from '../util/forms/Button'
import {useModalControls} from '#/state/modals'
export const snapPoints = [300] export const snapPoints = [300]
@@ -20,7 +20,7 @@ export function Component({
context: 'account' | 'content' context: 'account' | 'content'
moderation: ModerationUI moderation: ModerationUI
}) { }) {
const store = useStores() const {closeModal} = useModalControls()
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const pal = usePalette('default') const pal = usePalette('default')
@@ -99,10 +99,7 @@ export function Component({
{description} {description}
</Text> </Text>
<View style={s.flex1} /> <View style={s.flex1} />
<Button <Button type="primary" style={styles.btn} onPress={() => closeModal()}>
type="primary"
style={styles.btn}
onPress={() => store.shell.closeModal()}>
<Text type="button-lg" style={[pal.textLight, s.textCenter, s.white]}> <Text type="button-lg" style={[pal.textLight, s.textCenter, s.white]}>
Okay Okay
</Text> </Text>
+3 -4
View File
@@ -1,7 +1,6 @@
import React from 'react' import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native' import {StyleSheet, TouchableOpacity, View} from 'react-native'
import LinearGradient from 'react-native-linear-gradient' import LinearGradient from 'react-native-linear-gradient'
import {useStores} from 'state/index'
import {s, colors, gradients} from 'lib/styles' import {s, colors, gradients} from 'lib/styles'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
@@ -9,6 +8,7 @@ import {RepostIcon} from 'lib/icons'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
export const snapPoints = [250] export const snapPoints = [250]
@@ -22,12 +22,11 @@ export function Component({
isReposted: boolean isReposted: boolean
// TODO: Add author into component // TODO: Add author into component
}) { }) {
const store = useStores()
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const {closeModal} = useModalControls()
const onPress = async () => { const onPress = async () => {
store.shell.closeModal() closeModal()
} }
return ( return (
+3 -3
View File
@@ -2,7 +2,6 @@ import React, {useState} from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native' import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {useStores} from 'state/index'
import {s, colors} from 'lib/styles' import {s, colors} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
@@ -12,6 +11,7 @@ import {SelectableBtn} from '../util/forms/SelectableBtn'
import {ScrollView} from 'view/com/modals/util' import {ScrollView} from 'view/com/modals/util'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
const ADULT_CONTENT_LABELS = ['sexual', 'nudity', 'porn'] const ADULT_CONTENT_LABELS = ['sexual', 'nudity', 'porn']
@@ -27,7 +27,7 @@ export const Component = observer(function Component({
onChange: (labels: string[]) => void onChange: (labels: string[]) => void
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores() const {closeModal} = useModalControls()
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const [selected, setSelected] = useState(labels) const [selected, setSelected] = useState(labels)
const {_} = useLingui() const {_} = useLingui()
@@ -148,7 +148,7 @@ export const Component = observer(function Component({
<TouchableOpacity <TouchableOpacity
testID="confirmBtn" testID="confirmBtn"
onPress={() => { onPress={() => {
store.shell.closeModal() closeModal()
}} }}
style={styles.btn} style={styles.btn}
accessibilityRole="button" accessibilityRole="button"
+3 -3
View File
@@ -6,7 +6,6 @@ import {
} from '@fortawesome/react-native-fontawesome' } from '@fortawesome/react-native-fontawesome'
import {ScrollView, TextInput} from './util' import {ScrollView, TextInput} from './util'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {useStores} from 'state/index'
import {s, colors} from 'lib/styles' import {s, colors} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useTheme} from 'lib/ThemeContext' import {useTheme} from 'lib/ThemeContext'
@@ -14,21 +13,22 @@ import {LOCAL_DEV_SERVICE, STAGING_SERVICE, PROD_SERVICE} from 'state/index'
import {LOGIN_INCLUDE_DEV_SERVERS} from 'lib/build-flags' import {LOGIN_INCLUDE_DEV_SERVERS} from 'lib/build-flags'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
export const snapPoints = ['80%'] export const snapPoints = ['80%']
export function Component({onSelect}: {onSelect: (url: string) => void}) { export function Component({onSelect}: {onSelect: (url: string) => void}) {
const theme = useTheme() const theme = useTheme()
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores()
const [customUrl, setCustomUrl] = useState<string>('') const [customUrl, setCustomUrl] = useState<string>('')
const {_} = useLingui() const {_} = useLingui()
const {closeModal} = useModalControls()
const doSelect = (url: string) => { const doSelect = (url: string) => {
if (!url.startsWith('http://') && !url.startsWith('https://')) { if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = `https://${url}` url = `https://${url}`
} }
store.shell.closeModal() closeModal()
onSelect(url) onSelect(url)
} }
+6 -4
View File
@@ -23,6 +23,7 @@ import isEqual from 'lodash.isequal'
import {logger} from '#/logger' import {logger} from '#/logger'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
export const snapPoints = ['fullscreen'] export const snapPoints = ['fullscreen']
@@ -38,6 +39,7 @@ export const Component = observer(function UserAddRemoveListsImpl({
onRemove?: (listUri: string) => void onRemove?: (listUri: string) => void
}) { }) {
const store = useStores() const store = useStores()
const {closeModal} = useModalControls()
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const palPrimary = usePalette('primary') const palPrimary = usePalette('primary')
@@ -72,8 +74,8 @@ export const Component = observer(function UserAddRemoveListsImpl({
}, [memberships, listsList, store, setSelected, setMembershipsLoaded]) }, [memberships, listsList, store, setSelected, setMembershipsLoaded])
const onPressCancel = useCallback(() => { const onPressCancel = useCallback(() => {
store.shell.closeModal() closeModal()
}, [store]) }, [closeModal])
const onPressSave = useCallback(async () => { const onPressSave = useCallback(async () => {
let changes let changes
@@ -90,8 +92,8 @@ export const Component = observer(function UserAddRemoveListsImpl({
for (const uri of changes.removed) { for (const uri of changes.removed) {
onRemove?.(uri) onRemove?.(uri)
} }
store.shell.closeModal() closeModal()
}, [store, selected, memberships, onAdd, onRemove]) }, [closeModal, selected, memberships, onAdd, onRemove])
const onToggleSelected = useCallback( const onToggleSelected = useCallback(
(uri: string) => { (uri: string) => {
+6 -4
View File
@@ -22,6 +22,7 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {cleanError} from 'lib/strings/errors' import {cleanError} from 'lib/strings/errors'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
export const snapPoints = ['90%'] export const snapPoints = ['90%']
@@ -46,6 +47,7 @@ export const Component = observer(function Component({
const [isProcessing, setIsProcessing] = useState<boolean>(false) const [isProcessing, setIsProcessing] = useState<boolean>(false)
const [error, setError] = useState<string>('') const [error, setError] = useState<string>('')
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const {openModal, closeModal} = useModalControls()
const onSendEmail = async () => { const onSendEmail = async () => {
setError('') setError('')
@@ -70,7 +72,7 @@ export const Component = observer(function Component({
}) })
store.session.updateLocalAccountData({emailConfirmed: true}) store.session.updateLocalAccountData({emailConfirmed: true})
Toast.show('Email verified') Toast.show('Email verified')
store.shell.closeModal() closeModal()
} catch (e) { } catch (e) {
setError(cleanError(String(e))) setError(cleanError(String(e)))
} finally { } finally {
@@ -79,8 +81,8 @@ export const Component = observer(function Component({
} }
const onEmailIncorrect = () => { const onEmailIncorrect = () => {
store.shell.closeModal() closeModal()
store.shell.openModal({name: 'change-email'}) openModal({name: 'change-email'})
} }
return ( return (
@@ -227,7 +229,7 @@ export const Component = observer(function Component({
<Button <Button
testID="cancelBtn" testID="cancelBtn"
type="default" type="default"
onPress={() => store.shell.closeModal()} onPress={() => closeModal()}
accessibilityLabel={ accessibilityLabel={
stage === Stages.Reminder ? 'Not right now' : 'Cancel' stage === Stages.Reminder ? 'Not right now' : 'Cancel'
} }
+3 -3
View File
@@ -12,7 +12,6 @@ import {
} from '@fortawesome/react-native-fontawesome' } from '@fortawesome/react-native-fontawesome'
import LinearGradient from 'react-native-linear-gradient' import LinearGradient from 'react-native-linear-gradient'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {useStores} from 'state/index'
import {s, gradients} from 'lib/styles' import {s, gradients} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useTheme} from 'lib/ThemeContext' import {useTheme} from 'lib/ThemeContext'
@@ -20,14 +19,15 @@ import {ErrorMessage} from '../util/error/ErrorMessage'
import {cleanError} from 'lib/strings/errors' import {cleanError} from 'lib/strings/errors'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
export const snapPoints = ['80%'] export const snapPoints = ['80%']
export function Component({}: {}) { export function Component({}: {}) {
const pal = usePalette('default') const pal = usePalette('default')
const theme = useTheme() const theme = useTheme()
const store = useStores()
const {_} = useLingui() const {_} = useLingui()
const {closeModal} = useModalControls()
const [email, setEmail] = React.useState<string>('') const [email, setEmail] = React.useState<string>('')
const [isEmailSent, setIsEmailSent] = React.useState<boolean>(false) const [isEmailSent, setIsEmailSent] = React.useState<boolean>(false)
const [isProcessing, setIsProcessing] = React.useState<boolean>(false) const [isProcessing, setIsProcessing] = React.useState<boolean>(false)
@@ -57,7 +57,7 @@ export function Component({}: {}) {
setIsProcessing(false) setIsProcessing(false)
} }
const onCancel = () => { const onCancel = () => {
store.shell.closeModal() closeModal()
} }
return ( return (
@@ -7,12 +7,12 @@ import {Text} from 'view/com/util/text/Text'
import {Dimensions} from 'lib/media/types' import {Dimensions} from 'lib/media/types'
import {getDataUriSize} from 'lib/media/util' import {getDataUriSize} from 'lib/media/util'
import {s, gradients} from 'lib/styles' import {s, gradients} from 'lib/styles'
import {useStores} from 'state/index'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {SquareIcon, RectWideIcon, RectTallIcon} from 'lib/icons' import {SquareIcon, RectWideIcon, RectTallIcon} from 'lib/icons'
import {Image as RNImage} from 'react-native-image-crop-picker' import {Image as RNImage} from 'react-native-image-crop-picker'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
enum AspectRatio { enum AspectRatio {
Square = 'square', Square = 'square',
@@ -35,7 +35,7 @@ export function Component({
uri: string uri: string
onSelect: (img?: RNImage) => void onSelect: (img?: RNImage) => void
}) { }) {
const store = useStores() const {closeModal} = useModalControls()
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const [as, setAs] = React.useState<AspectRatio>(AspectRatio.Square) const [as, setAs] = React.useState<AspectRatio>(AspectRatio.Square)
@@ -46,7 +46,7 @@ export function Component({
const onPressCancel = () => { const onPressCancel = () => {
onSelect(undefined) onSelect(undefined)
store.shell.closeModal() closeModal()
} }
const onPressDone = () => { const onPressDone = () => {
const canvas = editorRef.current?.getImageScaledToCanvas() const canvas = editorRef.current?.getImageScaledToCanvas()
@@ -62,7 +62,7 @@ export function Component({
} else { } else {
onSelect(undefined) onSelect(undefined)
} }
store.shell.closeModal() closeModal()
} }
let cropperStyle let cropperStyle
@@ -1,7 +1,6 @@
import React from 'react' import React from 'react'
import {StyleSheet, View} from 'react-native' import {StyleSheet, View} from 'react-native'
import {ScrollView} from '../util' import {ScrollView} from '../util'
import {useStores} from 'state/index'
import {Text} from '../../util/text/Text' import {Text} from '../../util/text/Text'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
@@ -10,16 +9,23 @@ import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../../locale/languages'
import {LanguageToggle} from './LanguageToggle' import {LanguageToggle} from './LanguageToggle'
import {ConfirmLanguagesButton} from './ConfirmLanguagesButton' import {ConfirmLanguagesButton} from './ConfirmLanguagesButton'
import {Trans} from '@lingui/macro' import {Trans} from '@lingui/macro'
import {useModalControls} from '#/state/modals'
import {
useLanguagePrefs,
useLanguagePrefsApi,
} from '#/state/preferences/languages'
export const snapPoints = ['100%'] export const snapPoints = ['100%']
export function Component({}: {}) { export function Component({}: {}) {
const store = useStores() const {closeModal} = useModalControls()
const langPrefs = useLanguagePrefs()
const setLangPrefs = useLanguagePrefsApi()
const pal = usePalette('default') const pal = usePalette('default')
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const onPressDone = React.useCallback(() => { const onPressDone = React.useCallback(() => {
store.shell.closeModal() closeModal()
}, [store]) }, [closeModal])
const languages = React.useMemo(() => { const languages = React.useMemo(() => {
const langs = LANGUAGES.filter( const langs = LANGUAGES.filter(
@@ -30,23 +36,23 @@ export function Component({}: {}) {
// sort so that device & selected languages are on top, then alphabetically // sort so that device & selected languages are on top, then alphabetically
langs.sort((a, b) => { langs.sort((a, b) => {
const hasA = const hasA =
store.preferences.hasContentLanguage(a.code2) || langPrefs.contentLanguages.includes(a.code2) ||
deviceLocales.includes(a.code2) deviceLocales.includes(a.code2)
const hasB = const hasB =
store.preferences.hasContentLanguage(b.code2) || langPrefs.contentLanguages.includes(b.code2) ||
deviceLocales.includes(b.code2) deviceLocales.includes(b.code2)
if (hasA === hasB) return a.name.localeCompare(b.name) if (hasA === hasB) return a.name.localeCompare(b.name)
if (hasA) return -1 if (hasA) return -1
return 1 return 1
}) })
return langs return langs
}, [store]) }, [langPrefs])
const onPress = React.useCallback( const onPress = React.useCallback(
(code2: string) => { (code2: string) => {
store.preferences.toggleContentLanguage(code2) setLangPrefs.toggleContentLanguage(code2)
}, },
[store], [setLangPrefs],
) )
return ( return (
@@ -3,7 +3,7 @@ import {StyleSheet} from 'react-native'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {ToggleButton} from 'view/com/util/forms/ToggleButton' import {ToggleButton} from 'view/com/util/forms/ToggleButton'
import {useStores} from 'state/index' import {useLanguagePrefs, toPostLanguages} from '#/state/preferences/languages'
export const LanguageToggle = observer(function LanguageToggleImpl({ export const LanguageToggle = observer(function LanguageToggleImpl({
code2, code2,
@@ -17,17 +17,17 @@ export const LanguageToggle = observer(function LanguageToggleImpl({
langType: 'contentLanguages' | 'postLanguages' langType: 'contentLanguages' | 'postLanguages'
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores() const langPrefs = useLanguagePrefs()
const isSelected = store.preferences[langType].includes(code2) const values =
langType === 'contentLanguages'
? langPrefs.contentLanguages
: toPostLanguages(langPrefs.postLanguage)
const isSelected = values.includes(code2)
// enforce a max of 3 selections for post languages // enforce a max of 3 selections for post languages
let isDisabled = false let isDisabled = false
if ( if (langType === 'postLanguages' && values.length >= 3 && !isSelected) {
langType === 'postLanguages' &&
store.preferences[langType].length >= 3 &&
!isSelected
) {
isDisabled = true isDisabled = true
} }
@@ -2,7 +2,6 @@ import React from 'react'
import {StyleSheet, View} from 'react-native' import {StyleSheet, View} from 'react-native'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {ScrollView} from '../util' import {ScrollView} from '../util'
import {useStores} from 'state/index'
import {Text} from '../../util/text/Text' import {Text} from '../../util/text/Text'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
@@ -11,16 +10,24 @@ import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../../locale/languages'
import {ConfirmLanguagesButton} from './ConfirmLanguagesButton' import {ConfirmLanguagesButton} from './ConfirmLanguagesButton'
import {ToggleButton} from 'view/com/util/forms/ToggleButton' import {ToggleButton} from 'view/com/util/forms/ToggleButton'
import {Trans} from '@lingui/macro' import {Trans} from '@lingui/macro'
import {useModalControls} from '#/state/modals'
import {
useLanguagePrefs,
useLanguagePrefsApi,
hasPostLanguage,
} from '#/state/preferences/languages'
export const snapPoints = ['100%'] export const snapPoints = ['100%']
export const Component = observer(function PostLanguagesSettingsImpl() { export const Component = observer(function PostLanguagesSettingsImpl() {
const store = useStores() const {closeModal} = useModalControls()
const langPrefs = useLanguagePrefs()
const setLangPrefs = useLanguagePrefsApi()
const pal = usePalette('default') const pal = usePalette('default')
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const onPressDone = React.useCallback(() => { const onPressDone = React.useCallback(() => {
store.shell.closeModal() closeModal()
}, [store]) }, [closeModal])
const languages = React.useMemo(() => { const languages = React.useMemo(() => {
const langs = LANGUAGES.filter( const langs = LANGUAGES.filter(
@@ -31,23 +38,23 @@ export const Component = observer(function PostLanguagesSettingsImpl() {
// sort so that device & selected languages are on top, then alphabetically // sort so that device & selected languages are on top, then alphabetically
langs.sort((a, b) => { langs.sort((a, b) => {
const hasA = const hasA =
store.preferences.hasPostLanguage(a.code2) || hasPostLanguage(langPrefs.postLanguage, a.code2) ||
deviceLocales.includes(a.code2) deviceLocales.includes(a.code2)
const hasB = const hasB =
store.preferences.hasPostLanguage(b.code2) || hasPostLanguage(langPrefs.postLanguage, b.code2) ||
deviceLocales.includes(b.code2) deviceLocales.includes(b.code2)
if (hasA === hasB) return a.name.localeCompare(b.name) if (hasA === hasB) return a.name.localeCompare(b.name)
if (hasA) return -1 if (hasA) return -1
return 1 return 1
}) })
return langs return langs
}, [store]) }, [langPrefs])
const onPress = React.useCallback( const onPress = React.useCallback(
(code2: string) => { (code2: string) => {
store.preferences.togglePostLanguage(code2) setLangPrefs.togglePostLanguage(code2)
}, },
[store], [setLangPrefs],
) )
return ( return (
@@ -73,14 +80,11 @@ export const Component = observer(function PostLanguagesSettingsImpl() {
</Text> </Text>
<ScrollView style={styles.scrollContainer}> <ScrollView style={styles.scrollContainer}>
{languages.map(lang => { {languages.map(lang => {
const isSelected = store.preferences.hasPostLanguage(lang.code2) const isSelected = hasPostLanguage(langPrefs.postLanguage, lang.code2)
// enforce a max of 3 selections for post languages // enforce a max of 3 selections for post languages
let isDisabled = false let isDisabled = false
if ( if (langPrefs.postLanguage.split(',').length >= 3 && !isSelected) {
store.preferences.postLanguage.split(',').length >= 3 &&
!isSelected
) {
isDisabled = true isDisabled = true
} }
+4 -2
View File
@@ -16,6 +16,7 @@ import {ReportReasonOptions} from './ReasonOptions'
import {CollectionId} from './types' import {CollectionId} from './types'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
const DMCA_LINK = 'https://blueskyweb.xyz/support/copyright' const DMCA_LINK = 'https://blueskyweb.xyz/support/copyright'
@@ -39,6 +40,7 @@ type ReportComponentProps =
export function Component(content: ReportComponentProps) { export function Component(content: ReportComponentProps) {
const store = useStores() const store = useStores()
const {closeModal} = useModalControls()
const pal = usePalette('default') const pal = usePalette('default')
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const [isProcessing, setIsProcessing] = useState(false) const [isProcessing, setIsProcessing] = useState(false)
@@ -62,7 +64,7 @@ export function Component(content: ReportComponentProps) {
try { try {
if (issue === '__copyright__') { if (issue === '__copyright__') {
Linking.openURL(DMCA_LINK) Linking.openURL(DMCA_LINK)
store.shell.closeModal() closeModal()
return return
} }
const $type = !isAccountReport const $type = !isAccountReport
@@ -78,7 +80,7 @@ export function Component(content: ReportComponentProps) {
}) })
Toast.show("Thank you for your report! We'll look into it promptly.") Toast.show("Thank you for your report! We'll look into it promptly.")
store.shell.closeModal() closeModal()
return return
} catch (e: any) { } catch (e: any) {
setError(cleanError(e)) setError(cleanError(e))
-114
View File
@@ -1,114 +0,0 @@
import React from 'react'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {StyleSheet, View} from 'react-native'
import {observer} from 'mobx-react-lite'
import {AppBskyActorDefs} from '@atproto/api'
import {UserAvatar} from '../util/UserAvatar'
import {Text} from '../util/text/Text'
import {Link, TextLink} from '../util/Link'
import {Button} from '../util/forms/Button'
import {FollowButton} from '../profile/FollowButton'
import {CenteredView} from '../util/Views.web'
import {useStores} from 'state/index'
import {usePalette} from 'lib/hooks/usePalette'
import {s} from 'lib/styles'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {makeProfileLink} from 'lib/routes/links'
export const InvitedUsers = observer(function InvitedUsersImpl() {
const store = useStores()
return (
<CenteredView>
{store.invitedUsers.profiles.map(profile => (
<InvitedUser key={profile.did} profile={profile} />
))}
</CenteredView>
)
})
function InvitedUser({
profile,
}: {
profile: AppBskyActorDefs.ProfileViewDetailed
}) {
const pal = usePalette('default')
const store = useStores()
const onPressDismiss = React.useCallback(() => {
store.invitedUsers.markSeen(profile.did)
}, [store, profile])
return (
<View
testID="invitedUser"
style={[
styles.layout,
{
backgroundColor: pal.colors.unreadNotifBg,
borderColor: pal.colors.unreadNotifBorder,
},
]}>
<View style={styles.layoutIcon}>
<FontAwesomeIcon
icon="user-plus"
size={24}
style={[styles.icon, s.blue3 as FontAwesomeIconStyle]}
/>
</View>
<View style={s.flex1}>
<Link href={makeProfileLink(profile)}>
<UserAvatar avatar={profile.avatar} size={35} />
</Link>
<Text style={[styles.desc, pal.text]}>
<TextLink
type="md-bold"
style={pal.text}
href={makeProfileLink(profile)}
text={sanitizeDisplayName(profile.displayName || profile.handle)}
/>{' '}
joined using your invite code!
</Text>
<View style={styles.btns}>
<FollowButton
unfollowedType="primary"
followedType="primary-light"
profile={profile}
/>
<Button
testID="dismissBtn"
type="primary-light"
label="Dismiss"
onPress={onPressDismiss}
/>
</View>
</View>
</View>
)
}
const styles = StyleSheet.create({
layout: {
flexDirection: 'row',
borderTopWidth: 1,
padding: 10,
},
layoutIcon: {
width: 70,
alignItems: 'flex-end',
paddingTop: 2,
},
icon: {
marginRight: 10,
marginTop: 4,
},
desc: {
paddingVertical: 6,
},
btns: {
flexDirection: 'row',
gap: 10,
},
})
+16 -11
View File
@@ -38,6 +38,8 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {MAX_POST_LINES} from 'lib/constants' import {MAX_POST_LINES} from 'lib/constants'
import {logger} from '#/logger' import {logger} from '#/logger'
import {Trans} from '@lingui/macro' import {Trans} from '@lingui/macro'
import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads'
import {useLanguagePrefs} from '#/state/preferences'
export const PostThreadItem = observer(function PostThreadItem({ export const PostThreadItem = observer(function PostThreadItem({
item, item,
@@ -52,6 +54,9 @@ export const PostThreadItem = observer(function PostThreadItem({
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores() const store = useStores()
const mutedThreads = useMutedThreads()
const toggleThreadMute = useToggleThreadMute()
const langPrefs = useLanguagePrefs()
const [deleted, setDeleted] = React.useState(false) const [deleted, setDeleted] = React.useState(false)
const [limitLines, setLimitLines] = React.useState( const [limitLines, setLimitLines] = React.useState(
countLines(item.richText?.text) >= MAX_POST_LINES, countLines(item.richText?.text) >= MAX_POST_LINES,
@@ -83,15 +88,15 @@ export const PostThreadItem = observer(function PostThreadItem({
const translatorUrl = getTranslatorLink( const translatorUrl = getTranslatorLink(
record?.text || '', record?.text || '',
store.preferences.primaryLanguage, langPrefs.primaryLanguage,
) )
const needsTranslation = useMemo( const needsTranslation = useMemo(
() => () =>
Boolean( Boolean(
store.preferences.primaryLanguage && langPrefs.primaryLanguage &&
!isPostInLanguage(item.post, [store.preferences.primaryLanguage]), !isPostInLanguage(item.post, [langPrefs.primaryLanguage]),
), ),
[item.post, store.preferences.primaryLanguage], [item.post, langPrefs.primaryLanguage],
) )
const onPressReply = React.useCallback(() => { const onPressReply = React.useCallback(() => {
@@ -131,10 +136,10 @@ export const PostThreadItem = observer(function PostThreadItem({
Linking.openURL(translatorUrl) Linking.openURL(translatorUrl)
}, [translatorUrl]) }, [translatorUrl])
const onToggleThreadMute = React.useCallback(async () => { const onToggleThreadMute = React.useCallback(() => {
try { try {
await item.toggleThreadMute() const muted = toggleThreadMute(item.data.rootUri)
if (item.isThreadMuted) { if (muted) {
Toast.show('You will no longer receive notifications for this thread') Toast.show('You will no longer receive notifications for this thread')
} else { } else {
Toast.show('You will now receive notifications for this thread') Toast.show('You will now receive notifications for this thread')
@@ -142,7 +147,7 @@ export const PostThreadItem = observer(function PostThreadItem({
} catch (e) { } catch (e) {
logger.error('Failed to toggle thread mute', {error: e}) logger.error('Failed to toggle thread mute', {error: e})
} }
}, [item]) }, [item, toggleThreadMute])
const onDeletePost = React.useCallback(() => { const onDeletePost = React.useCallback(() => {
item.delete().then( item.delete().then(
@@ -287,7 +292,7 @@ export const PostThreadItem = observer(function PostThreadItem({
itemHref={itemHref} itemHref={itemHref}
itemTitle={itemTitle} itemTitle={itemTitle}
isAuthor={item.post.author.did === store.me.did} isAuthor={item.post.author.did === store.me.did}
isThreadMuted={item.isThreadMuted} isThreadMuted={mutedThreads.includes(item.data.rootUri)}
onCopyPostText={onCopyPostText} onCopyPostText={onCopyPostText}
onOpenTranslate={onOpenTranslate} onOpenTranslate={onOpenTranslate}
onToggleThreadMute={onToggleThreadMute} onToggleThreadMute={onToggleThreadMute}
@@ -394,7 +399,7 @@ export const PostThreadItem = observer(function PostThreadItem({
isAuthor={item.post.author.did === store.me.did} isAuthor={item.post.author.did === store.me.did}
isReposted={!!item.post.viewer?.repost} isReposted={!!item.post.viewer?.repost}
isLiked={!!item.post.viewer?.like} isLiked={!!item.post.viewer?.like}
isThreadMuted={item.isThreadMuted} isThreadMuted={mutedThreads.includes(item.data.rootUri)}
onPressReply={onPressReply} onPressReply={onPressReply}
onPressToggleRepost={onPressToggleRepost} onPressToggleRepost={onPressToggleRepost}
onPressToggleLike={onPressToggleLike} onPressToggleLike={onPressToggleLike}
@@ -537,7 +542,7 @@ export const PostThreadItem = observer(function PostThreadItem({
likeCount={item.post.likeCount} likeCount={item.post.likeCount}
isReposted={!!item.post.viewer?.repost} isReposted={!!item.post.viewer?.repost}
isLiked={!!item.post.viewer?.like} isLiked={!!item.post.viewer?.like}
isThreadMuted={item.isThreadMuted} isThreadMuted={mutedThreads.includes(item.data.rootUri)}
onPressReply={onPressReply} onPressReply={onPressReply}
onPressToggleRepost={onPressToggleRepost} onPressToggleRepost={onPressToggleRepost}
onPressToggleLike={onPressToggleLike} onPressToggleLike={onPressToggleLike}
+11 -6
View File
@@ -33,6 +33,8 @@ import {makeProfileLink} from 'lib/routes/links'
import {MAX_POST_LINES} from 'lib/constants' import {MAX_POST_LINES} from 'lib/constants'
import {countLines} from 'lib/strings/helpers' import {countLines} from 'lib/strings/helpers'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads'
import {useLanguagePrefs} from '#/state/preferences'
export const Post = observer(function PostImpl({ export const Post = observer(function PostImpl({
view, view,
@@ -106,6 +108,9 @@ const PostLoaded = observer(function PostLoadedImpl({
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores() const store = useStores()
const mutedThreads = useMutedThreads()
const toggleThreadMute = useToggleThreadMute()
const langPrefs = useLanguagePrefs()
const [limitLines, setLimitLines] = React.useState( const [limitLines, setLimitLines] = React.useState(
countLines(item.richText?.text) >= MAX_POST_LINES, countLines(item.richText?.text) >= MAX_POST_LINES,
) )
@@ -122,7 +127,7 @@ const PostLoaded = observer(function PostLoadedImpl({
const translatorUrl = getTranslatorLink( const translatorUrl = getTranslatorLink(
record?.text || '', record?.text || '',
store.preferences.primaryLanguage, langPrefs.primaryLanguage,
) )
const onPressReply = React.useCallback(() => { const onPressReply = React.useCallback(() => {
@@ -161,10 +166,10 @@ const PostLoaded = observer(function PostLoadedImpl({
Linking.openURL(translatorUrl) Linking.openURL(translatorUrl)
}, [translatorUrl]) }, [translatorUrl])
const onToggleThreadMute = React.useCallback(async () => { const onToggleThreadMute = React.useCallback(() => {
try { try {
await item.toggleThreadMute() const muted = toggleThreadMute(item.data.rootUri)
if (item.isThreadMuted) { if (muted) {
Toast.show('You will no longer receive notifications for this thread') Toast.show('You will no longer receive notifications for this thread')
} else { } else {
Toast.show('You will now receive notifications for this thread') Toast.show('You will now receive notifications for this thread')
@@ -172,7 +177,7 @@ const PostLoaded = observer(function PostLoadedImpl({
} catch (e) { } catch (e) {
logger.error('Failed to toggle thread mute', {error: e}) logger.error('Failed to toggle thread mute', {error: e})
} }
}, [item]) }, [item, toggleThreadMute])
const onDeletePost = React.useCallback(() => { const onDeletePost = React.useCallback(() => {
item.delete().then( item.delete().then(
@@ -286,7 +291,7 @@ const PostLoaded = observer(function PostLoadedImpl({
likeCount={item.post.likeCount} likeCount={item.post.likeCount}
isReposted={!!item.post.viewer?.repost} isReposted={!!item.post.viewer?.repost}
isLiked={!!item.post.viewer?.like} isLiked={!!item.post.viewer?.like}
isThreadMuted={item.isThreadMuted} isThreadMuted={mutedThreads.includes(item.data.rootUri)}
onPressReply={onPressReply} onPressReply={onPressReply}
onPressToggleRepost={onPressToggleRepost} onPressToggleRepost={onPressToggleRepost}
onPressToggleLike={onPressToggleLike} onPressToggleLike={onPressToggleLike}
+5 -3
View File
@@ -11,6 +11,7 @@ import {useNavigation} from '@react-navigation/native'
import {NavigationProp} from 'lib/routes/types' import {NavigationProp} from 'lib/routes/types'
import {useStores} from 'state/index' import {useStores} from 'state/index'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useModalControls} from '#/state/modals'
const MESSAGES = { const MESSAGES = {
[KnownError.Unknown]: '', [KnownError.Unknown]: '',
@@ -57,13 +58,14 @@ function FeedgenErrorMessage({
const msg = MESSAGES[knownError] const msg = MESSAGES[knownError]
const uri = (feed.params as GetCustomFeed.QueryParams).feed const uri = (feed.params as GetCustomFeed.QueryParams).feed
const [ownerDid] = safeParseFeedgenUri(uri) const [ownerDid] = safeParseFeedgenUri(uri)
const {openModal, closeModal} = useModalControls()
const onViewProfile = React.useCallback(() => { const onViewProfile = React.useCallback(() => {
navigation.navigate('Profile', {name: ownerDid}) navigation.navigate('Profile', {name: ownerDid})
}, [navigation, ownerDid]) }, [navigation, ownerDid])
const onRemoveFeed = React.useCallback(async () => { const onRemoveFeed = React.useCallback(async () => {
store.shell.openModal({ openModal({
name: 'confirm', name: 'confirm',
title: 'Remove feed', title: 'Remove feed',
message: 'Remove this feed from your saved feeds?', message: 'Remove this feed from your saved feeds?',
@@ -78,10 +80,10 @@ function FeedgenErrorMessage({
} }
}, },
onPressCancel() { onPressCancel() {
store.shell.closeModal() closeModal()
}, },
}) })
}, [store, uri]) }, [store, openModal, closeModal, uri])
return ( return (
<View <View
+11 -6
View File
@@ -33,6 +33,8 @@ import {isEmbedByEmbedder} from 'lib/embeds'
import {MAX_POST_LINES} from 'lib/constants' import {MAX_POST_LINES} from 'lib/constants'
import {countLines} from 'lib/strings/helpers' import {countLines} from 'lib/strings/helpers'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads'
import {useLanguagePrefs} from '#/state/preferences'
export const FeedItem = observer(function FeedItemImpl({ export const FeedItem = observer(function FeedItemImpl({
item, item,
@@ -49,7 +51,10 @@ export const FeedItem = observer(function FeedItemImpl({
showReplyLine?: boolean showReplyLine?: boolean
}) { }) {
const store = useStores() const store = useStores()
const langPrefs = useLanguagePrefs()
const pal = usePalette('default') const pal = usePalette('default')
const mutedThreads = useMutedThreads()
const toggleThreadMute = useToggleThreadMute()
const {track} = useAnalytics() const {track} = useAnalytics()
const [deleted, setDeleted] = useState(false) const [deleted, setDeleted] = useState(false)
const [limitLines, setLimitLines] = useState( const [limitLines, setLimitLines] = useState(
@@ -72,7 +77,7 @@ export const FeedItem = observer(function FeedItemImpl({
}, [record?.reply]) }, [record?.reply])
const translatorUrl = getTranslatorLink( const translatorUrl = getTranslatorLink(
record?.text || '', record?.text || '',
store.preferences.primaryLanguage, langPrefs.primaryLanguage,
) )
const onPressReply = React.useCallback(() => { const onPressReply = React.useCallback(() => {
@@ -114,11 +119,11 @@ export const FeedItem = observer(function FeedItemImpl({
Linking.openURL(translatorUrl) Linking.openURL(translatorUrl)
}, [translatorUrl]) }, [translatorUrl])
const onToggleThreadMute = React.useCallback(async () => { const onToggleThreadMute = React.useCallback(() => {
track('FeedItem:ThreadMute') track('FeedItem:ThreadMute')
try { try {
await item.toggleThreadMute() const muted = toggleThreadMute(item.rootUri)
if (item.isThreadMuted) { if (muted) {
Toast.show('You will no longer receive notifications for this thread') Toast.show('You will no longer receive notifications for this thread')
} else { } else {
Toast.show('You will now receive notifications for this thread') Toast.show('You will now receive notifications for this thread')
@@ -126,7 +131,7 @@ export const FeedItem = observer(function FeedItemImpl({
} catch (e) { } catch (e) {
logger.error('Failed to toggle thread mute', {error: e}) logger.error('Failed to toggle thread mute', {error: e})
} }
}, [track, item]) }, [track, toggleThreadMute, item])
const onDeletePost = React.useCallback(() => { const onDeletePost = React.useCallback(() => {
track('FeedItem:PostDelete') track('FeedItem:PostDelete')
@@ -360,7 +365,7 @@ export const FeedItem = observer(function FeedItemImpl({
likeCount={item.post.likeCount} likeCount={item.post.likeCount}
isReposted={!!item.post.viewer?.repost} isReposted={!!item.post.viewer?.repost}
isLiked={!!item.post.viewer?.like} isLiked={!!item.post.viewer?.like}
isThreadMuted={item.isThreadMuted} isThreadMuted={mutedThreads.includes(item.rootUri)}
onPressReply={onPressReply} onPressReply={onPressReply}
onPressToggleRepost={onPressToggleRepost} onPressToggleRepost={onPressToggleRepost}
onPressToggleLike={onPressToggleLike} onPressToggleLike={onPressToggleLike}
+12 -10
View File
@@ -42,6 +42,7 @@ import {ProfileHeaderSuggestedFollows} from './ProfileHeaderSuggestedFollows'
import {logger} from '#/logger' import {logger} from '#/logger'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
interface Props { interface Props {
view: ProfileModel view: ProfileModel
@@ -116,6 +117,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
const palInverted = usePalette('inverted') const palInverted = usePalette('inverted')
const store = useStores() const store = useStores()
const {_} = useLingui() const {_} = useLingui()
const {openModal} = useModalControls()
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const {track} = useAnalytics() const {track} = useAnalytics()
const invalidHandle = isInvalidHandle(view.handle) const invalidHandle = isInvalidHandle(view.handle)
@@ -160,12 +162,12 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
const onPressEditProfile = React.useCallback(() => { const onPressEditProfile = React.useCallback(() => {
track('ProfileHeader:EditProfileButtonClicked') track('ProfileHeader:EditProfileButtonClicked')
store.shell.openModal({ openModal({
name: 'edit-profile', name: 'edit-profile',
profileView: view, profileView: view,
onUpdate: onRefreshAll, onUpdate: onRefreshAll,
}) })
}, [track, store, view, onRefreshAll]) }, [track, openModal, view, onRefreshAll])
const trackPress = React.useCallback( const trackPress = React.useCallback(
(f: 'Followers' | 'Follows') => { (f: 'Followers' | 'Follows') => {
@@ -184,12 +186,12 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
const onPressAddRemoveLists = React.useCallback(() => { const onPressAddRemoveLists = React.useCallback(() => {
track('ProfileHeader:AddToListsButtonClicked') track('ProfileHeader:AddToListsButtonClicked')
store.shell.openModal({ openModal({
name: 'user-add-remove-lists', name: 'user-add-remove-lists',
subject: view.did, subject: view.did,
displayName: view.displayName || view.handle, displayName: view.displayName || view.handle,
}) })
}, [track, view, store]) }, [track, view, openModal])
const onPressMuteAccount = React.useCallback(async () => { const onPressMuteAccount = React.useCallback(async () => {
track('ProfileHeader:MuteAccountButtonClicked') track('ProfileHeader:MuteAccountButtonClicked')
@@ -215,7 +217,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
const onPressBlockAccount = React.useCallback(async () => { const onPressBlockAccount = React.useCallback(async () => {
track('ProfileHeader:BlockAccountButtonClicked') track('ProfileHeader:BlockAccountButtonClicked')
store.shell.openModal({ openModal({
name: 'confirm', name: 'confirm',
title: 'Block Account', title: 'Block Account',
message: message:
@@ -231,11 +233,11 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
} }
}, },
}) })
}, [track, view, store, onRefreshAll]) }, [track, view, openModal, onRefreshAll])
const onPressUnblockAccount = React.useCallback(async () => { const onPressUnblockAccount = React.useCallback(async () => {
track('ProfileHeader:UnblockAccountButtonClicked') track('ProfileHeader:UnblockAccountButtonClicked')
store.shell.openModal({ openModal({
name: 'confirm', name: 'confirm',
title: 'Unblock Account', title: 'Unblock Account',
message: message:
@@ -251,15 +253,15 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
} }
}, },
}) })
}, [track, view, store, onRefreshAll]) }, [track, view, openModal, onRefreshAll])
const onPressReportAccount = React.useCallback(() => { const onPressReportAccount = React.useCallback(() => {
track('ProfileHeader:ReportAccountButtonClicked') track('ProfileHeader:ReportAccountButtonClicked')
store.shell.openModal({ openModal({
name: 'report', name: 'report',
did: view.did, did: view.did,
}) })
}, [track, store, view]) }, [track, openModal, view])
const isMe = React.useMemo( const isMe = React.useMemo(
() => store.me.did === view.did, () => store.me.did === view.did,
+3 -1
View File
@@ -2,6 +2,7 @@ import React from 'react'
import {Pressable, View} from 'react-native' import {Pressable, View} from 'react-native'
import {useStores} from 'state/index' import {useStores} from 'state/index'
import {navigate} from '../../../Navigation' import {navigate} from '../../../Navigation'
import {useModalControls} from '#/state/modals'
/** /**
* This utility component is only included in the test simulator * This utility component is only included in the test simulator
@@ -13,6 +14,7 @@ const BTN = {height: 1, width: 1, backgroundColor: 'red'}
export function TestCtrls() { export function TestCtrls() {
const store = useStores() const store = useStores()
const {openModal} = useModalControls()
const onPressSignInAlice = async () => { const onPressSignInAlice = async () => {
await store.session.login({ await store.session.login({
service: 'http://localhost:3000', service: 'http://localhost:3000',
@@ -85,7 +87,7 @@ export function TestCtrls() {
/> />
<Pressable <Pressable
testID="e2eOpenInviteCodesModal" testID="e2eOpenInviteCodesModal"
onPress={() => store.shell.openModal({name: 'invite-codes'})} onPress={() => openModal({name: 'invite-codes'})}
accessibilityRole="button" accessibilityRole="button"
style={BTN} style={BTN}
/> />
+18 -10
View File
@@ -21,7 +21,6 @@ import {Text} from './text/Text'
import {TypographyVariant} from 'lib/ThemeContext' import {TypographyVariant} from 'lib/ThemeContext'
import {NavigationProp} from 'lib/routes/types' import {NavigationProp} from 'lib/routes/types'
import {router} from '../../../routes' import {router} from '../../../routes'
import {useStores, RootStoreModel} from 'state/index'
import { import {
convertBskyAppUrlIfNeeded, convertBskyAppUrlIfNeeded,
isExternalUrl, isExternalUrl,
@@ -31,6 +30,7 @@ import {isAndroid, isWeb} from 'platform/detection'
import {sanitizeUrl} from '@braintree/sanitize-url' import {sanitizeUrl} from '@braintree/sanitize-url'
import {PressableWithHover} from './PressableWithHover' import {PressableWithHover} from './PressableWithHover'
import FixedTouchableHighlight from '../pager/FixedTouchableHighlight' import FixedTouchableHighlight from '../pager/FixedTouchableHighlight'
import {useModalControls} from '#/state/modals'
type Event = type Event =
| React.MouseEvent<HTMLAnchorElement, MouseEvent> | React.MouseEvent<HTMLAnchorElement, MouseEvent>
@@ -60,17 +60,17 @@ export const Link = memo(function Link({
anchorNoUnderline, anchorNoUnderline,
...props ...props
}: Props) { }: Props) {
const store = useStores() const {closeModal} = useModalControls()
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const anchorHref = asAnchor ? sanitizeUrl(href) : undefined const anchorHref = asAnchor ? sanitizeUrl(href) : undefined
const onPress = React.useCallback( const onPress = React.useCallback(
(e?: Event) => { (e?: Event) => {
if (typeof href === 'string') { if (typeof href === 'string') {
return onPressInner(store, navigation, sanitizeUrl(href), e) return onPressInner(closeModal, navigation, sanitizeUrl(href), e)
} }
}, },
[store, navigation, href], [closeModal, navigation, href],
) )
if (noFeedback) { if (noFeedback) {
@@ -160,8 +160,8 @@ export const TextLink = memo(function TextLink({
warnOnMismatchingLabel?: boolean warnOnMismatchingLabel?: boolean
} & TextProps) { } & TextProps) {
const {...props} = useLinkProps({to: sanitizeUrl(href)}) const {...props} = useLinkProps({to: sanitizeUrl(href)})
const store = useStores()
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const {openModal, closeModal} = useModalControls()
if (warnOnMismatchingLabel && typeof text !== 'string') { if (warnOnMismatchingLabel && typeof text !== 'string') {
console.error('Unable to detect mismatching label') console.error('Unable to detect mismatching label')
@@ -174,7 +174,7 @@ export const TextLink = memo(function TextLink({
linkRequiresWarning(href, typeof text === 'string' ? text : '') linkRequiresWarning(href, typeof text === 'string' ? text : '')
if (requiresWarning) { if (requiresWarning) {
e?.preventDefault?.() e?.preventDefault?.()
store.shell.openModal({ openModal({
name: 'link-warning', name: 'link-warning',
text: typeof text === 'string' ? text : '', text: typeof text === 'string' ? text : '',
href, href,
@@ -185,9 +185,17 @@ export const TextLink = memo(function TextLink({
// @ts-ignore function signature differs by platform -prf // @ts-ignore function signature differs by platform -prf
return onPress() return onPress()
} }
return onPressInner(store, navigation, sanitizeUrl(href), e) return onPressInner(closeModal, navigation, sanitizeUrl(href), e)
}, },
[onPress, store, navigation, href, text, warnOnMismatchingLabel], [
onPress,
closeModal,
openModal,
navigation,
href,
text,
warnOnMismatchingLabel,
],
) )
const hrefAttrs = useMemo(() => { const hrefAttrs = useMemo(() => {
const isExternal = isExternalUrl(href) const isExternal = isExternalUrl(href)
@@ -285,7 +293,7 @@ export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({
// needed customizations // needed customizations
// -prf // -prf
function onPressInner( function onPressInner(
store: RootStoreModel, closeModal = () => {},
navigation: NavigationProp, navigation: NavigationProp,
href: string, href: string,
e?: Event, e?: Event,
@@ -318,7 +326,7 @@ function onPressInner(
if (newTab || href.startsWith('http') || href.startsWith('mailto')) { if (newTab || href.startsWith('http') || href.startsWith('mailto')) {
Linking.openURL(href) Linking.openURL(href)
} else { } else {
store.shell.closeModal() // close any active modals closeModal() // close any active modals
// @ts-ignore we're not able to type check on this one -prf // @ts-ignore we're not able to type check on this one -prf
navigation.dispatch(StackActions.push(...router.matchPath(href))) navigation.dispatch(StackActions.push(...router.matchPath(href)))
+3 -3
View File
@@ -1,9 +1,9 @@
import React from 'react' import React from 'react'
import {Pressable, StyleProp, ViewStyle} from 'react-native' import {Pressable, StyleProp, ViewStyle} from 'react-native'
import {useStores} from 'state/index'
import {Link} from './Link' import {Link} from './Link'
import {isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
import {makeProfileLink} from 'lib/routes/links' import {makeProfileLink} from 'lib/routes/links'
import {useModalControls} from '#/state/modals'
interface UserPreviewLinkProps { interface UserPreviewLinkProps {
did: string did: string
@@ -13,7 +13,7 @@ interface UserPreviewLinkProps {
export function UserPreviewLink( export function UserPreviewLink(
props: React.PropsWithChildren<UserPreviewLinkProps>, props: React.PropsWithChildren<UserPreviewLinkProps>,
) { ) {
const store = useStores() const {openModal} = useModalControls()
if (isWeb) { if (isWeb) {
return ( return (
@@ -29,7 +29,7 @@ export function UserPreviewLink(
return ( return (
<Pressable <Pressable
onPress={() => onPress={() =>
store.shell.openModal({ openModal({
name: 'profile-preview', name: 'profile-preview',
did: props.did, did: props.did,
}) })
+4 -4
View File
@@ -2,7 +2,6 @@ import React from 'react'
import {StyleProp, View, ViewStyle} from 'react-native' import {StyleProp, View, ViewStyle} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {toShareUrl} from 'lib/strings/url-helpers' import {toShareUrl} from 'lib/strings/url-helpers'
import {useStores} from 'state/index'
import {useTheme} from 'lib/ThemeContext' import {useTheme} from 'lib/ThemeContext'
import {shareUrl} from 'lib/sharing' import {shareUrl} from 'lib/sharing'
import { import {
@@ -12,6 +11,7 @@ import {
import {EventStopper} from '../EventStopper' import {EventStopper} from '../EventStopper'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useModalControls} from '#/state/modals'
export function PostDropdownBtn({ export function PostDropdownBtn({
testID, testID,
@@ -39,10 +39,10 @@ export function PostDropdownBtn({
onDeletePost: () => void onDeletePost: () => void
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
}) { }) {
const store = useStores()
const theme = useTheme() const theme = useTheme()
const {_} = useLingui() const {_} = useLingui()
const defaultCtrlColor = theme.palette.default.postCtrl const defaultCtrlColor = theme.palette.default.postCtrl
const {openModal} = useModalControls()
const dropdownItems: NativeDropdownItem[] = [ const dropdownItems: NativeDropdownItem[] = [
{ {
@@ -111,7 +111,7 @@ export function PostDropdownBtn({
!isAuthor && { !isAuthor && {
label: 'Report post', label: 'Report post',
onPress() { onPress() {
store.shell.openModal({ openModal({
name: 'report', name: 'report',
uri: itemUri, uri: itemUri,
cid: itemCid, cid: itemCid,
@@ -132,7 +132,7 @@ export function PostDropdownBtn({
isAuthor && { isAuthor && {
label: 'Delete post', label: 'Delete post',
onPress() { onPress() {
store.shell.openModal({ openModal({
name: 'confirm', name: 'confirm',
title: 'Delete this post?', title: 'Delete this post?',
message: 'Are you sure? This can not be undone.', message: 'Are you sure? This can not be undone.',
@@ -6,9 +6,9 @@ import {ModerationUI} from '@atproto/api'
import {Text} from '../text/Text' import {Text} from '../text/Text'
import {ShieldExclamation} from 'lib/icons' import {ShieldExclamation} from 'lib/icons'
import {describeModerationCause} from 'lib/moderation' import {describeModerationCause} from 'lib/moderation'
import {useStores} from 'state/index'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useModalControls} from '#/state/modals'
export function ContentHider({ export function ContentHider({
testID, testID,
@@ -24,11 +24,11 @@ export function ContentHider({
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
childContainerStyle?: StyleProp<ViewStyle> childContainerStyle?: StyleProp<ViewStyle>
}>) { }>) {
const store = useStores()
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const [override, setOverride] = React.useState(false) const [override, setOverride] = React.useState(false)
const {openModal} = useModalControls()
if (!moderation.blur || (ignoreMute && moderation.cause?.type === 'muted')) { if (!moderation.blur || (ignoreMute && moderation.cause?.type === 'muted')) {
return ( return (
@@ -46,7 +46,7 @@ export function ContentHider({
if (!moderation.noOverride) { if (!moderation.noOverride) {
setOverride(v => !v) setOverride(v => !v)
} else { } else {
store.shell.openModal({ openModal({
name: 'moderation-details', name: 'moderation-details',
context: 'content', context: 'content',
moderation, moderation,
@@ -65,7 +65,7 @@ export function ContentHider({
]}> ]}>
<Pressable <Pressable
onPress={() => { onPress={() => {
store.shell.openModal({ openModal({
name: 'moderation-details', name: 'moderation-details',
context: 'content', context: 'content',
moderation, moderation,
+3 -3
View File
@@ -5,9 +5,9 @@ import {Text} from '../text/Text'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {ShieldExclamation} from 'lib/icons' import {ShieldExclamation} from 'lib/icons'
import {describeModerationCause} from 'lib/moderation' import {describeModerationCause} from 'lib/moderation'
import {useStores} from 'state/index'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
export function PostAlerts({ export function PostAlerts({
moderation, moderation,
@@ -17,9 +17,9 @@ export function PostAlerts({
includeMute?: boolean includeMute?: boolean
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
}) { }) {
const store = useStores()
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const {openModal} = useModalControls()
const shouldAlert = !!moderation.cause && moderation.alert const shouldAlert = !!moderation.cause && moderation.alert
if (!shouldAlert) { if (!shouldAlert) {
@@ -30,7 +30,7 @@ export function PostAlerts({
return ( return (
<Pressable <Pressable
onPress={() => { onPress={() => {
store.shell.openModal({ openModal({
name: 'moderation-details', name: 'moderation-details',
context: 'content', context: 'content',
moderation, moderation,
+3 -3
View File
@@ -8,9 +8,9 @@ import {Text} from '../text/Text'
import {addStyle} from 'lib/styles' import {addStyle} from 'lib/styles'
import {describeModerationCause} from 'lib/moderation' import {describeModerationCause} from 'lib/moderation'
import {ShieldExclamation} from 'lib/icons' import {ShieldExclamation} from 'lib/icons'
import {useStores} from 'state/index'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useModalControls} from '#/state/modals'
interface Props extends ComponentProps<typeof Link> { interface Props extends ComponentProps<typeof Link> {
// testID?: string // testID?: string
@@ -27,11 +27,11 @@ export function PostHider({
children, children,
...props ...props
}: Props) { }: Props) {
const store = useStores()
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const [override, setOverride] = React.useState(false) const [override, setOverride] = React.useState(false)
const {openModal} = useModalControls()
if (!moderation.blur) { if (!moderation.blur) {
return ( return (
@@ -66,7 +66,7 @@ export function PostHider({
]}> ]}>
<Pressable <Pressable
onPress={() => { onPress={() => {
store.shell.openModal({ openModal({
name: 'moderation-details', name: 'moderation-details',
context: 'content', context: 'content',
moderation, moderation,
@@ -8,9 +8,9 @@ import {
describeModerationCause, describeModerationCause,
getProfileModerationCauses, getProfileModerationCauses,
} from 'lib/moderation' } from 'lib/moderation'
import {useStores} from 'state/index'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
export function ProfileHeaderAlerts({ export function ProfileHeaderAlerts({
moderation, moderation,
@@ -19,9 +19,9 @@ export function ProfileHeaderAlerts({
moderation: ProfileModeration moderation: ProfileModeration
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
}) { }) {
const store = useStores()
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const {openModal} = useModalControls()
const causes = getProfileModerationCauses(moderation) const causes = getProfileModerationCauses(moderation)
if (!causes.length) { if (!causes.length) {
@@ -37,7 +37,7 @@ export function ProfileHeaderAlerts({
testID="profileHeaderAlert" testID="profileHeaderAlert"
key={desc.name} key={desc.name}
onPress={() => { onPress={() => {
store.shell.openModal({ openModal({
name: 'moderation-details', name: 'moderation-details',
context: 'content', context: 'content',
moderation: {cause}, moderation: {cause},
+7 -7
View File
@@ -18,9 +18,10 @@ import {NavigationProp} from 'lib/routes/types'
import {Text} from '../text/Text' import {Text} from '../text/Text'
import {Button} from '../forms/Button' import {Button} from '../forms/Button'
import {describeModerationCause} from 'lib/moderation' import {describeModerationCause} from 'lib/moderation'
import {useStores} from 'state/index'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {s} from '#/lib/styles'
export function ScreenHider({ export function ScreenHider({
testID, testID,
@@ -36,13 +37,13 @@ export function ScreenHider({
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
containerStyle?: StyleProp<ViewStyle> containerStyle?: StyleProp<ViewStyle>
}>) { }>) {
const store = useStores()
const pal = usePalette('default') const pal = usePalette('default')
const palInverted = usePalette('inverted') const palInverted = usePalette('inverted')
const {_} = useLingui() const {_} = useLingui()
const [override, setOverride] = React.useState(false) const [override, setOverride] = React.useState(false)
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const {openModal} = useModalControls()
if (!moderation.blur || override) { if (!moderation.blur || override) {
return ( return (
@@ -68,14 +69,13 @@ export function ScreenHider({
<Trans>Content Warning</Trans> <Trans>Content Warning</Trans>
</Text> </Text>
<Text type="2xl" style={[styles.description, pal.textLight]}> <Text type="2xl" style={[styles.description, pal.textLight]}>
<Trans>This {screenDescription} has been flagged: </Trans> <Trans>This {screenDescription} has been flagged:</Trans>
<Text type="2xl-medium" style={pal.text}> <Text type="2xl-medium" style={[pal.text, s.ml5]}>
{desc.name} {desc.name}.
</Text> </Text>
.{' '}
<TouchableWithoutFeedback <TouchableWithoutFeedback
onPress={() => { onPress={() => {
store.shell.openModal({ openModal({
name: 'moderation-details', name: 'moderation-details',
context: 'account', context: 'account',
moderation, moderation,
+6 -3
View File
@@ -16,6 +16,7 @@ import {useStores} from 'state/index'
import {RepostButton} from './RepostButton' import {RepostButton} from './RepostButton'
import {Haptics} from 'lib/haptics' import {Haptics} from 'lib/haptics'
import {HITSLOP_10, HITSLOP_20} from 'lib/constants' import {HITSLOP_10, HITSLOP_20} from 'lib/constants'
import {useModalControls} from '#/state/modals'
interface PostCtrlsOpts { interface PostCtrlsOpts {
itemUri: string itemUri: string
@@ -51,6 +52,7 @@ interface PostCtrlsOpts {
export function PostCtrls(opts: PostCtrlsOpts) { export function PostCtrls(opts: PostCtrlsOpts) {
const store = useStores() const store = useStores()
const theme = useTheme() const theme = useTheme()
const {closeModal} = useModalControls()
const defaultCtrlColor = React.useMemo( const defaultCtrlColor = React.useMemo(
() => ({ () => ({
color: theme.palette.default.postCtrl, color: theme.palette.default.postCtrl,
@@ -58,17 +60,17 @@ export function PostCtrls(opts: PostCtrlsOpts) {
[theme], [theme],
) as StyleProp<ViewStyle> ) as StyleProp<ViewStyle>
const onRepost = useCallback(() => { const onRepost = useCallback(() => {
store.shell.closeModal() closeModal()
if (!opts.isReposted) { if (!opts.isReposted) {
Haptics.default() Haptics.default()
opts.onPressToggleRepost().catch(_e => undefined) opts.onPressToggleRepost().catch(_e => undefined)
} else { } else {
opts.onPressToggleRepost().catch(_e => undefined) opts.onPressToggleRepost().catch(_e => undefined)
} }
}, [opts, store.shell]) }, [opts, closeModal])
const onQuote = useCallback(() => { const onQuote = useCallback(() => {
store.shell.closeModal() closeModal()
store.shell.openComposer({ store.shell.openComposer({
quote: { quote: {
uri: opts.itemUri, uri: opts.itemUri,
@@ -86,6 +88,7 @@ export function PostCtrls(opts: PostCtrlsOpts) {
opts.itemUri, opts.itemUri,
opts.text, opts.text,
store.shell, store.shell,
closeModal,
]) ])
const onPressToggleLikeWrapper = async () => { const onPressToggleLikeWrapper = async () => {
@@ -5,8 +5,8 @@ import {s, colors} from 'lib/styles'
import {useTheme} from 'lib/ThemeContext' import {useTheme} from 'lib/ThemeContext'
import {Text} from '../text/Text' import {Text} from '../text/Text'
import {pluralize} from 'lib/strings/helpers' import {pluralize} from 'lib/strings/helpers'
import {useStores} from 'state/index'
import {HITSLOP_10, HITSLOP_20} from 'lib/constants' import {HITSLOP_10, HITSLOP_20} from 'lib/constants'
import {useModalControls} from '#/state/modals'
interface Props { interface Props {
isReposted: boolean isReposted: boolean
@@ -23,8 +23,8 @@ export const RepostButton = ({
onRepost, onRepost,
onQuote, onQuote,
}: Props) => { }: Props) => {
const store = useStores()
const theme = useTheme() const theme = useTheme()
const {openModal} = useModalControls()
const defaultControlColor = React.useMemo( const defaultControlColor = React.useMemo(
() => ({ () => ({
@@ -34,13 +34,13 @@ export const RepostButton = ({
) )
const onPressToggleRepostWrapper = useCallback(() => { const onPressToggleRepostWrapper = useCallback(() => {
store.shell.openModal({ openModal({
name: 'repost', name: 'repost',
onRepost: onRepost, onRepost: onRepost,
onQuote: onQuote, onQuote: onQuote,
isReposted, isReposted,
}) })
}, [onRepost, onQuote, isReposted, store.shell]) }, [onRepost, onQuote, isReposted, openModal])
return ( return (
<TouchableOpacity <TouchableOpacity
+9 -6
View File
@@ -19,6 +19,8 @@ import {CenteredView} from 'view/com/util/Views'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useSetMinimalShellMode} from '#/state/shell' import {useSetMinimalShellMode} from '#/state/shell'
import {useModalControls} from '#/state/modals'
import {useLanguagePrefs} from '#/state/preferences'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppPasswords'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppPasswords'>
export const AppPasswords = withAuthRequired( export const AppPasswords = withAuthRequired(
@@ -28,6 +30,7 @@ export const AppPasswords = withAuthRequired(
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const {screen} = useAnalytics() const {screen} = useAnalytics()
const {isTabletOrDesktop} = useWebMediaQueries() const {isTabletOrDesktop} = useWebMediaQueries()
const {openModal} = useModalControls()
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
@@ -37,8 +40,8 @@ export const AppPasswords = withAuthRequired(
) )
const onAdd = React.useCallback(async () => { const onAdd = React.useCallback(async () => {
store.shell.openModal({name: 'add-app-password'}) openModal({name: 'add-app-password'})
}, [store]) }, [openModal])
// no app passwords (empty) state // no app passwords (empty) state
if (store.me.appPasswords.length === 0) { if (store.me.appPasswords.length === 0) {
@@ -168,9 +171,11 @@ function AppPassword({
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores() const store = useStores()
const {_} = useLingui() const {_} = useLingui()
const {openModal} = useModalControls()
const {contentLanguages} = useLanguagePrefs()
const onDelete = React.useCallback(async () => { const onDelete = React.useCallback(async () => {
store.shell.openModal({ openModal({
name: 'confirm', name: 'confirm',
title: 'Delete App Password', title: 'Delete App Password',
message: `Are you sure you want to delete the app password "${name}"?`, message: `Are you sure you want to delete the app password "${name}"?`,
@@ -179,9 +184,7 @@ function AppPassword({
Toast.show('App password deleted') Toast.show('App password deleted')
}, },
}) })
}, [store, name]) }, [store, openModal, name])
const {contentLanguages} = store.preferences
const primaryLocale = const primaryLocale =
contentLanguages.length > 0 ? contentLanguages[0] : 'en-US' contentLanguages.length > 0 ? contentLanguages[0] : 'en-US'
+14 -9
View File
@@ -2,7 +2,6 @@ import React from 'react'
import {StyleSheet, View} from 'react-native' import {StyleSheet, View} from 'react-native'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {Text} from '../com/util/text/Text' import {Text} from '../com/util/text/Text'
import {useStores} from 'state/index'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
@@ -19,6 +18,8 @@ import {useFocusEffect} from '@react-navigation/native'
import {LANGUAGES} from 'lib/../locale/languages' import {LANGUAGES} from 'lib/../locale/languages'
import RNPickerSelect, {PickerSelectProps} from 'react-native-picker-select' import RNPickerSelect, {PickerSelectProps} from 'react-native-picker-select'
import {useSetMinimalShellMode} from '#/state/shell' import {useSetMinimalShellMode} from '#/state/shell'
import {useModalControls} from '#/state/modals'
import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'LanguageSettings'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'LanguageSettings'>
@@ -26,10 +27,12 @@ export const LanguageSettingsScreen = observer(function LanguageSettingsImpl(
_: Props, _: Props,
) { ) {
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores() const langPrefs = useLanguagePrefs()
const setLangPrefs = useLanguagePrefsApi()
const {isTabletOrDesktop} = useWebMediaQueries() const {isTabletOrDesktop} = useWebMediaQueries()
const {screen, track} = useAnalytics() const {screen, track} = useAnalytics()
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const {openModal} = useModalControls()
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
@@ -40,26 +43,28 @@ export const LanguageSettingsScreen = observer(function LanguageSettingsImpl(
const onPressContentLanguages = React.useCallback(() => { const onPressContentLanguages = React.useCallback(() => {
track('Settings:ContentlanguagesButtonClicked') track('Settings:ContentlanguagesButtonClicked')
store.shell.openModal({name: 'content-languages-settings'}) openModal({name: 'content-languages-settings'})
}, [track, store]) }, [track, openModal])
const onChangePrimaryLanguage = React.useCallback( const onChangePrimaryLanguage = React.useCallback(
(value: Parameters<PickerSelectProps['onValueChange']>[0]) => { (value: Parameters<PickerSelectProps['onValueChange']>[0]) => {
store.preferences.setPrimaryLanguage(value) if (langPrefs.primaryLanguage !== value) {
setLangPrefs.setPrimaryLanguage(value)
}
}, },
[store.preferences], [langPrefs, setLangPrefs],
) )
const myLanguages = React.useMemo(() => { const myLanguages = React.useMemo(() => {
return ( return (
store.preferences.contentLanguages langPrefs.contentLanguages
.map(lang => LANGUAGES.find(l => l.code2 === lang)) .map(lang => LANGUAGES.find(l => l.code2 === lang))
.filter(Boolean) .filter(Boolean)
// @ts-ignore // @ts-ignore
.map(l => l.name) .map(l => l.name)
.join(', ') .join(', ')
) )
}, [store.preferences.contentLanguages]) }, [langPrefs.contentLanguages])
return ( return (
<CenteredView <CenteredView
@@ -82,7 +87,7 @@ export const LanguageSettingsScreen = observer(function LanguageSettingsImpl(
<View style={{position: 'relative'}}> <View style={{position: 'relative'}}>
<RNPickerSelect <RNPickerSelect
value={store.preferences.primaryLanguage} value={langPrefs.primaryLanguage}
onValueChange={onChangePrimaryLanguage} onValueChange={onChangePrimaryLanguage}
items={LANGUAGES.filter(l => Boolean(l.code2)).map(l => ({ items={LANGUAGES.filter(l => Boolean(l.code2)).map(l => ({
label: l.name, label: l.name,
+4 -2
View File
@@ -17,6 +17,7 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {SimpleViewHeader} from 'view/com/util/SimpleViewHeader' import {SimpleViewHeader} from 'view/com/util/SimpleViewHeader'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {useSetMinimalShellMode} from '#/state/shell' import {useSetMinimalShellMode} from '#/state/shell'
import {useModalControls} from '#/state/modals'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Lists'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'Lists'>
export const ListsScreen = withAuthRequired( export const ListsScreen = withAuthRequired(
@@ -26,6 +27,7 @@ export const ListsScreen = withAuthRequired(
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const {openModal} = useModalControls()
const listsLists: ListsListModel = React.useMemo( const listsLists: ListsListModel = React.useMemo(
() => new ListsListModel(store, 'my-curatelists'), () => new ListsListModel(store, 'my-curatelists'),
@@ -40,7 +42,7 @@ export const ListsScreen = withAuthRequired(
) )
const onPressNewList = React.useCallback(() => { const onPressNewList = React.useCallback(() => {
store.shell.openModal({ openModal({
name: 'create-or-edit-list', name: 'create-or-edit-list',
purpose: 'app.bsky.graph.defs#curatelist', purpose: 'app.bsky.graph.defs#curatelist',
onSave: (uri: string) => { onSave: (uri: string) => {
@@ -53,7 +55,7 @@ export const ListsScreen = withAuthRequired(
} catch {} } catch {}
}, },
}) })
}, [store, navigation]) }, [openModal, navigation])
return ( return (
<View style={s.hContentRegion} testID="listsScreen"> <View style={s.hContentRegion} testID="listsScreen">
+4 -4
View File
@@ -8,7 +8,6 @@ import {
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {useStores} from 'state/index'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {CenteredView} from '../com/util/Views' import {CenteredView} from '../com/util/Views'
import {ViewHeader} from '../com/util/ViewHeader' import {ViewHeader} from '../com/util/ViewHeader'
@@ -18,15 +17,16 @@ import {usePalette} from 'lib/hooks/usePalette'
import {useAnalytics} from 'lib/analytics/analytics' import {useAnalytics} from 'lib/analytics/analytics'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {useSetMinimalShellMode} from '#/state/shell' import {useSetMinimalShellMode} from '#/state/shell'
import {useModalControls} from '#/state/modals'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Moderation'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'Moderation'>
export const ModerationScreen = withAuthRequired( export const ModerationScreen = withAuthRequired(
observer(function Moderation({}: Props) { observer(function Moderation({}: Props) {
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores()
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const {screen, track} = useAnalytics() const {screen, track} = useAnalytics()
const {isTabletOrDesktop} = useWebMediaQueries() const {isTabletOrDesktop} = useWebMediaQueries()
const {openModal} = useModalControls()
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
@@ -37,8 +37,8 @@ export const ModerationScreen = withAuthRequired(
const onPressContentFiltering = React.useCallback(() => { const onPressContentFiltering = React.useCallback(() => {
track('Moderation:ContentfilteringButtonClicked') track('Moderation:ContentfilteringButtonClicked')
store.shell.openModal({name: 'content-filtering-settings'}) openModal({name: 'content-filtering-settings'})
}, [track, store]) }, [track, openModal])
return ( return (
<CenteredView <CenteredView
+4 -2
View File
@@ -17,6 +17,7 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {SimpleViewHeader} from 'view/com/util/SimpleViewHeader' import {SimpleViewHeader} from 'view/com/util/SimpleViewHeader'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {useSetMinimalShellMode} from '#/state/shell' import {useSetMinimalShellMode} from '#/state/shell'
import {useModalControls} from '#/state/modals'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ModerationModlists'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'ModerationModlists'>
export const ModerationModlistsScreen = withAuthRequired( export const ModerationModlistsScreen = withAuthRequired(
@@ -26,6 +27,7 @@ export const ModerationModlistsScreen = withAuthRequired(
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const {openModal} = useModalControls()
const mutelists: ListsListModel = React.useMemo( const mutelists: ListsListModel = React.useMemo(
() => new ListsListModel(store, 'my-modlists'), () => new ListsListModel(store, 'my-modlists'),
@@ -40,7 +42,7 @@ export const ModerationModlistsScreen = withAuthRequired(
) )
const onPressNewList = React.useCallback(() => { const onPressNewList = React.useCallback(() => {
store.shell.openModal({ openModal({
name: 'create-or-edit-list', name: 'create-or-edit-list',
purpose: 'app.bsky.graph.defs#modlist', purpose: 'app.bsky.graph.defs#modlist',
onSave: (uri: string) => { onSave: (uri: string) => {
@@ -53,7 +55,7 @@ export const ModerationModlistsScreen = withAuthRequired(
} catch {} } catch {}
}, },
}) })
}, [store, navigation]) }, [openModal, navigation])
return ( return (
<View style={s.hContentRegion} testID="moderationModlistsScreen"> <View style={s.hContentRegion} testID="moderationModlistsScreen">
-2
View File
@@ -10,7 +10,6 @@ import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {ViewHeader} from '../com/util/ViewHeader' import {ViewHeader} from '../com/util/ViewHeader'
import {Feed} from '../com/notifications/Feed' import {Feed} from '../com/notifications/Feed'
import {TextLink} from 'view/com/util/Link' import {TextLink} from 'view/com/util/Link'
import {InvitedUsers} from '../com/notifications/InvitedUsers'
import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
import {useStores} from 'state/index' import {useStores} from 'state/index'
import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
@@ -145,7 +144,6 @@ export const NotificationsScreen = withAuthRequired(
return ( return (
<View testID="notificationsScreen" style={s.hContentRegion}> <View testID="notificationsScreen" style={s.hContentRegion}>
<ViewHeader title="Notifications" canGoBack={false} /> <ViewHeader title="Notifications" canGoBack={false} />
<InvitedUsers />
<Feed <Feed
view={store.me.notifications} view={store.me.notifications}
onPressTryAgain={onPressTryAgain} onPressTryAgain={onPressTryAgain}

Some files were not shown because too many files have changed in this diff Show More