Merge branch 'main' into internationalization
This commit is contained in:
+13
-1
@@ -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
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
// =
|
// =
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -193,7 +193,6 @@ export class MeModel {
|
|||||||
error: e,
|
error: e,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
await this.rootStore.invitedUsers.fetch(this.invites)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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') {
|
||||||
|
|||||||
@@ -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() {}
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
|
|||||||
@@ -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 +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)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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',
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import {SessionModel} from '../models/session'
|
||||||
|
|
||||||
|
export function shouldRequestEmailConfirmation(_session: SessionModel) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setEmailConfirmationRequested() {}
|
||||||
@@ -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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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,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 ? (
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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`)}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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]}>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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]}
|
||||||
|
|||||||
@@ -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?
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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)))
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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},
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user