about summary refs log tree commit diff
path: root/src/state
diff options
context:
space:
mode:
Diffstat (limited to 'src/state')
-rw-r--r--src/state/dialogs/index.tsx31
-rw-r--r--src/state/models/media/gallery.ts25
-rw-r--r--src/state/persisted/__tests__/migrate.test.ts6
-rw-r--r--src/state/persisted/index.ts6
-rw-r--r--src/state/persisted/legacy.ts8
-rw-r--r--src/state/queries/feed.ts129
-rw-r--r--src/state/queries/post-feed.ts61
-rw-r--r--src/state/queries/preferences/const.ts2
-rw-r--r--src/state/queries/preferences/index.ts49
-rw-r--r--src/state/session/index.tsx18
-rw-r--r--src/state/shell/composer.tsx2
-rw-r--r--src/state/util.ts17
12 files changed, 226 insertions, 128 deletions
diff --git a/src/state/dialogs/index.tsx b/src/state/dialogs/index.tsx
index 4cafaa086..9fc70c178 100644
--- a/src/state/dialogs/index.tsx
+++ b/src/state/dialogs/index.tsx
@@ -1,20 +1,32 @@
 import React from 'react'
-import {DialogControlProps} from '#/components/Dialog'
+import {DialogControlRefProps} from '#/components/Dialog'
+import {Provider as GlobalDialogsProvider} from '#/components/dialogs/Context'
 
 const DialogContext = React.createContext<{
+  /**
+   * The currently active `useDialogControl` hooks.
+   */
   activeDialogs: React.MutableRefObject<
-    Map<string, React.MutableRefObject<DialogControlProps>>
+    Map<string, React.MutableRefObject<DialogControlRefProps>>
   >
+  /**
+   * The currently open dialogs, referenced by their IDs, generated from
+   * `useId`.
+   */
+  openDialogs: React.MutableRefObject<Set<string>>
 }>({
   activeDialogs: {
     current: new Map(),
   },
+  openDialogs: {
+    current: new Set(),
+  },
 })
 
 const DialogControlContext = React.createContext<{
-  closeAllDialogs(): void
+  closeAllDialogs(): boolean
 }>({
-  closeAllDialogs: () => {},
+  closeAllDialogs: () => false,
 })
 
 export function useDialogStateContext() {
@@ -27,17 +39,22 @@ export function useDialogStateControlContext() {
 
 export function Provider({children}: React.PropsWithChildren<{}>) {
   const activeDialogs = React.useRef<
-    Map<string, React.MutableRefObject<DialogControlProps>>
+    Map<string, React.MutableRefObject<DialogControlRefProps>>
   >(new Map())
+  const openDialogs = React.useRef<Set<string>>(new Set())
+
   const closeAllDialogs = React.useCallback(() => {
     activeDialogs.current.forEach(dialog => dialog.current.close())
+    return openDialogs.current.size > 0
   }, [])
-  const context = React.useMemo(() => ({activeDialogs}), [])
+
+  const context = React.useMemo(() => ({activeDialogs, openDialogs}), [])
   const controls = React.useMemo(() => ({closeAllDialogs}), [closeAllDialogs])
+
   return (
     <DialogContext.Provider value={context}>
       <DialogControlContext.Provider value={controls}>
-        {children}
+        <GlobalDialogsProvider>{children}</GlobalDialogsProvider>
       </DialogControlContext.Provider>
     </DialogContext.Provider>
   )
diff --git a/src/state/models/media/gallery.ts b/src/state/models/media/gallery.ts
index 04023bf82..9c8c13010 100644
--- a/src/state/models/media/gallery.ts
+++ b/src/state/models/media/gallery.ts
@@ -4,11 +4,21 @@ import {Image as RNImage} from 'react-native-image-crop-picker'
 import {openPicker} from 'lib/media/picker'
 import {getImageDim} from 'lib/media/manip'
 
+interface InitialImageUri {
+  uri: string
+  width: number
+  height: number
+}
+
 export class GalleryModel {
   images: ImageModel[] = []
 
-  constructor() {
+  constructor(uris?: {uri: string; width: number; height: number}[]) {
     makeAutoObservable(this)
+
+    if (uris) {
+      this.addFromUris(uris)
+    }
   }
 
   get isEmpty() {
@@ -23,7 +33,7 @@ export class GalleryModel {
     return this.images.some(image => image.altText.trim() === '')
   }
 
-  async add(image_: Omit<RNImage, 'size'>) {
+  *add(image_: Omit<RNImage, 'size'>) {
     if (this.size >= 4) {
       return
     }
@@ -86,4 +96,15 @@ export class GalleryModel {
       }),
     )
   }
+
+  async addFromUris(uris: InitialImageUri[]) {
+    for (const uriObj of uris) {
+      this.add({
+        mime: 'image/jpeg',
+        height: uriObj.height,
+        width: uriObj.width,
+        path: uriObj.uri,
+      })
+    }
+  }
 }
diff --git a/src/state/persisted/__tests__/migrate.test.ts b/src/state/persisted/__tests__/migrate.test.ts
index e4b55d5da..97767e273 100644
--- a/src/state/persisted/__tests__/migrate.test.ts
+++ b/src/state/persisted/__tests__/migrate.test.ts
@@ -26,7 +26,7 @@ test('migrate: fresh install', async () => {
 
   expect(AsyncStorage.getItem).toHaveBeenCalledWith('root')
   expect(read).toHaveBeenCalledTimes(1)
-  expect(logger.info).toHaveBeenCalledWith(
+  expect(logger.debug).toHaveBeenCalledWith(
     'persisted state: no migration needed',
   )
 })
@@ -38,7 +38,7 @@ test('migrate: fresh install, existing new storage', async () => {
 
   expect(AsyncStorage.getItem).toHaveBeenCalledWith('root')
   expect(read).toHaveBeenCalledTimes(1)
-  expect(logger.info).toHaveBeenCalledWith(
+  expect(logger.debug).toHaveBeenCalledWith(
     'persisted state: no migration needed',
   )
 })
@@ -68,7 +68,7 @@ test('migrate: has legacy data', async () => {
   await migrate()
 
   expect(write).toHaveBeenCalledWith(transform(fixtures.LEGACY_DATA_DUMP))
-  expect(logger.info).toHaveBeenCalledWith(
+  expect(logger.debug).toHaveBeenCalledWith(
     'persisted state: migrated legacy storage',
   )
 })
diff --git a/src/state/persisted/index.ts b/src/state/persisted/index.ts
index 2f34c2dbf..f57172d2f 100644
--- a/src/state/persisted/index.ts
+++ b/src/state/persisted/index.ts
@@ -19,7 +19,7 @@ const _emitter = new EventEmitter()
  * the Provider.
  */
 export async function init() {
-  logger.info('persisted state: initializing')
+  logger.debug('persisted state: initializing')
 
   broadcast.onmessage = onBroadcastMessage
 
@@ -27,11 +27,11 @@ export async function init() {
     await migrate() // migrate old store
     const stored = await store.read() // check for new store
     if (!stored) {
-      logger.info('persisted state: initializing default storage')
+      logger.debug('persisted state: initializing default storage')
       await store.write(defaults) // opt: init new store
     }
     _state = stored || defaults // return new store
-    logger.log('persisted state: initialized')
+    logger.debug('persisted state: initialized')
   } catch (e) {
     logger.error('persisted state: failed to load root state from storage', {
       message: e,
diff --git a/src/state/persisted/legacy.ts b/src/state/persisted/legacy.ts
index cce080c84..fd94a96a2 100644
--- a/src/state/persisted/legacy.ts
+++ b/src/state/persisted/legacy.ts
@@ -121,7 +121,7 @@ export function transform(legacy: Partial<LegacySchema>): Schema {
  * local storage AND old storage exists.
  */
 export async function migrate() {
-  logger.info('persisted state: check need to migrate')
+  logger.debug('persisted state: check need to migrate')
 
   try {
     const rawLegacyData = await AsyncStorage.getItem(
@@ -131,7 +131,7 @@ export async function migrate() {
     const alreadyMigrated = Boolean(newData)
 
     if (!alreadyMigrated && rawLegacyData) {
-      logger.info('persisted state: migrating legacy storage')
+      logger.debug('persisted state: migrating legacy storage')
 
       const legacyData = JSON.parse(rawLegacyData)
       const newData = transform(legacyData)
@@ -139,14 +139,14 @@ export async function migrate() {
 
       if (validate.success) {
         await write(newData)
-        logger.info('persisted state: migrated legacy storage')
+        logger.debug('persisted state: migrated legacy storage')
       } else {
         logger.error('persisted state: legacy data failed validation', {
           message: validate.error,
         })
       }
     } else {
-      logger.info('persisted state: no migration needed')
+      logger.debug('persisted state: no migration needed')
     }
   } catch (e: any) {
     logger.error(e, {
diff --git a/src/state/queries/feed.ts b/src/state/queries/feed.ts
index 67294ece2..1fa92c291 100644
--- a/src/state/queries/feed.ts
+++ b/src/state/queries/feed.ts
@@ -1,11 +1,9 @@
-import React from 'react'
 import {
   useQuery,
   useInfiniteQuery,
   InfiniteData,
   QueryKey,
   useMutation,
-  useQueryClient,
 } from '@tanstack/react-query'
 import {
   AtUri,
@@ -15,7 +13,6 @@ import {
   AppBskyUnspeccedGetPopularFeedGenerators,
 } from '@atproto/api'
 
-import {logger} from '#/logger'
 import {router} from '#/routes'
 import {sanitizeDisplayName} from '#/lib/strings/display-names'
 import {sanitizeHandle} from '#/lib/strings/handles'
@@ -219,83 +216,59 @@ const FOLLOWING_FEED_STUB: FeedSourceInfo = {
   likeUri: '',
 }
 
-export function usePinnedFeedsInfos(): {
-  feeds: FeedSourceInfo[]
-  hasPinnedCustom: boolean
-  isLoading: boolean
-} {
-  const queryClient = useQueryClient()
-  const [tabs, setTabs] = React.useState<FeedSourceInfo[]>([
-    FOLLOWING_FEED_STUB,
-  ])
-  const [isLoading, setLoading] = React.useState(true)
-  const {data: preferences} = usePreferencesQuery()
+export function usePinnedFeedsInfos() {
+  const {data: preferences, isLoading: isLoadingPrefs} = usePreferencesQuery()
+  const pinnedUris = preferences?.feeds?.pinned ?? []
 
-  const hasPinnedCustom = React.useMemo<boolean>(() => {
-    return tabs.some(tab => tab !== FOLLOWING_FEED_STUB)
-  }, [tabs])
-
-  React.useEffect(() => {
-    if (!preferences?.feeds?.pinned) return
-    const uris = preferences.feeds.pinned
-
-    async function fetchFeedInfo() {
-      const reqs = []
-
-      for (const uri of uris) {
-        const cached = queryClient.getQueryData<FeedSourceInfo>(
-          feedSourceInfoQueryKey({uri}),
-        )
-
-        if (cached) {
-          reqs.push(cached)
-        } else {
-          reqs.push(
-            (async () => {
-              // these requests can fail, need to filter those out
-              try {
-                return await queryClient.fetchQuery({
-                  staleTime: STALE.SECONDS.FIFTEEN,
-                  queryKey: feedSourceInfoQueryKey({uri}),
-                  queryFn: async () => {
-                    const type = getFeedTypeFromUri(uri)
+  return useQuery({
+    staleTime: STALE.INFINITY,
+    enabled: !isLoadingPrefs,
+    queryKey: ['pinnedFeedsInfos', pinnedUris.join(',')],
+    queryFn: async () => {
+      let resolved = new Map()
+
+      // Get all feeds. We can do this in a batch.
+      const feedUris = pinnedUris.filter(
+        uri => getFeedTypeFromUri(uri) === 'feed',
+      )
+      let feedsPromise = Promise.resolve()
+      if (feedUris.length > 0) {
+        feedsPromise = getAgent()
+          .app.bsky.feed.getFeedGenerators({
+            feeds: feedUris,
+          })
+          .then(res => {
+            for (let feedView of res.data.feeds) {
+              resolved.set(feedView.uri, hydrateFeedGenerator(feedView))
+            }
+          })
+      }
 
-                    if (type === 'feed') {
-                      const res =
-                        await getAgent().app.bsky.feed.getFeedGenerator({
-                          feed: uri,
-                        })
-                      return hydrateFeedGenerator(res.data.view)
-                    } else {
-                      const res = await getAgent().app.bsky.graph.getList({
-                        list: uri,
-                        limit: 1,
-                      })
-                      return hydrateList(res.data.list)
-                    }
-                  },
-                })
-              } catch (e) {
-                // expected failure
-                logger.info(`usePinnedFeedsInfos: failed to fetch ${uri}`, {
-                  error: e,
-                })
-              }
-            })(),
-          )
+      // Get all lists. This currently has to be done individually.
+      const listUris = pinnedUris.filter(
+        uri => getFeedTypeFromUri(uri) === 'list',
+      )
+      const listsPromises = listUris.map(listUri =>
+        getAgent()
+          .app.bsky.graph.getList({
+            list: listUri,
+            limit: 1,
+          })
+          .then(res => {
+            const listView = res.data.list
+            resolved.set(listView.uri, hydrateList(listView))
+          }),
+      )
+
+      // The returned result will have the original order.
+      const result = [FOLLOWING_FEED_STUB]
+      await Promise.allSettled([feedsPromise, ...listsPromises])
+      for (let pinnedUri of pinnedUris) {
+        if (resolved.has(pinnedUri)) {
+          result.push(resolved.get(pinnedUri))
         }
       }
-
-      const views = (await Promise.all(reqs)).filter(
-        Boolean,
-      ) as FeedSourceInfo[]
-
-      setTabs([FOLLOWING_FEED_STUB].concat(views))
-      setLoading(false)
-    }
-
-    fetchFeedInfo()
-  }, [queryClient, setTabs, preferences?.feeds?.pinned])
-
-  return {feeds: tabs, hasPinnedCustom, isLoading}
+      return result
+    },
+  })
 }
diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts
index 40399395a..c295ffcb0 100644
--- a/src/state/queries/post-feed.ts
+++ b/src/state/queries/post-feed.ts
@@ -1,6 +1,11 @@
 import React, {useCallback, useEffect, useRef} from 'react'
 import {AppState} from 'react-native'
-import {AppBskyFeedDefs, AppBskyFeedPost, PostModeration} from '@atproto/api'
+import {
+  AppBskyFeedDefs,
+  AppBskyFeedPost,
+  AtUri,
+  PostModeration,
+} from '@atproto/api'
 import {
   useInfiniteQuery,
   InfiniteData,
@@ -29,6 +34,7 @@ import {KnownError} from '#/view/com/posts/FeedErrorMessage'
 import {embedViewRecordToPostView, getEmbeddedPost} from './util'
 import {useModerationOpts} from './preferences'
 import {queryClient} from 'lib/react-query'
+import {BSKY_FEED_OWNER_DIDS} from 'lib/constants'
 
 type ActorDid = string
 type AuthorFilter =
@@ -137,24 +143,41 @@ export function usePostFeedQuery(
             cursor: undefined,
           }
 
-      const res = await api.fetch({cursor, limit: PAGE_SIZE})
-      precacheFeedPostProfiles(queryClient, res.feed)
-
-      /*
-       * If this is a public view, we need to check if posts fail moderation.
-       * If all fail, we throw an error. If only some fail, we continue and let
-       * moderations happen later, which results in some posts being shown and
-       * some not.
-       */
-      if (!getAgent().session) {
-        assertSomePostsPassModeration(res.feed)
-      }
+      try {
+        const res = await api.fetch({cursor, limit: PAGE_SIZE})
+        precacheFeedPostProfiles(queryClient, res.feed)
+
+        /*
+         * If this is a public view, we need to check if posts fail moderation.
+         * If all fail, we throw an error. If only some fail, we continue and let
+         * moderations happen later, which results in some posts being shown and
+         * some not.
+         */
+        if (!getAgent().session) {
+          assertSomePostsPassModeration(res.feed)
+        }
+
+        return {
+          api,
+          cursor: res.cursor,
+          feed: res.feed,
+          fetchedAt: Date.now(),
+        }
+      } catch (e) {
+        const feedDescParts = feedDesc.split('|')
+        const feedOwnerDid = new AtUri(feedDescParts[1]).hostname
+
+        if (
+          feedDescParts[0] === 'feedgen' &&
+          BSKY_FEED_OWNER_DIDS.includes(feedOwnerDid)
+        ) {
+          logger.error(`Bluesky feed may be offline: ${feedOwnerDid}`, {
+            feedDesc,
+            jsError: e,
+          })
+        }
 
-      return {
-        api,
-        cursor: res.cursor,
-        feed: res.feed,
-        fetchedAt: Date.now(),
+        throw e
       }
     },
     initialPageParam: undefined,
@@ -253,7 +276,7 @@ export function usePostFeedQuery(
                             .success
                         ) {
                           return {
-                            _reactKey: `${slice._reactKey}-${i}`,
+                            _reactKey: `${slice._reactKey}-${i}-${item.post.uri}`,
                             uri: item.post.uri,
                             post: item.post,
                             record: item.post.record,
diff --git a/src/state/queries/preferences/const.ts b/src/state/queries/preferences/const.ts
index 2d9d02994..25d284998 100644
--- a/src/state/queries/preferences/const.ts
+++ b/src/state/queries/preferences/const.ts
@@ -49,4 +49,6 @@ export const DEFAULT_LOGGED_OUT_PREFERENCES: UsePreferencesQueryResponse = {
   threadViewPrefs: DEFAULT_THREAD_VIEW_PREFS,
   userAge: 13, // TODO(pwi)
   interests: {tags: []},
+  mutedWords: [],
+  hiddenPosts: [],
 }
diff --git a/src/state/queries/preferences/index.ts b/src/state/queries/preferences/index.ts
index 632d31a13..07198de77 100644
--- a/src/state/queries/preferences/index.ts
+++ b/src/state/queries/preferences/index.ts
@@ -1,6 +1,10 @@
 import {useMemo} from 'react'
 import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query'
-import {LabelPreference, BskyFeedViewPreference} from '@atproto/api'
+import {
+  LabelPreference,
+  BskyFeedViewPreference,
+  AppBskyActorDefs,
+} from '@atproto/api'
 
 import {track} from '#/lib/analytics/analytics'
 import {getAge} from '#/lib/strings/time'
@@ -108,6 +112,7 @@ export function useModerationOpts() {
     return {
       ...moderationOpts,
       hiddenPosts,
+      mutedWords: prefs.data.mutedWords || [],
     }
   }, [currentAccount?.did, prefs.data, hiddenPosts])
   return opts
@@ -278,3 +283,45 @@ export function useUnpinFeedMutation() {
     },
   })
 }
+
+export function useUpsertMutedWordsMutation() {
+  const queryClient = useQueryClient()
+
+  return useMutation({
+    mutationFn: async (mutedWords: AppBskyActorDefs.MutedWord[]) => {
+      await getAgent().upsertMutedWords(mutedWords)
+      // triggers a refetch
+      await queryClient.invalidateQueries({
+        queryKey: preferencesQueryKey,
+      })
+    },
+  })
+}
+
+export function useUpdateMutedWordMutation() {
+  const queryClient = useQueryClient()
+
+  return useMutation({
+    mutationFn: async (mutedWord: AppBskyActorDefs.MutedWord) => {
+      await getAgent().updateMutedWord(mutedWord)
+      // triggers a refetch
+      await queryClient.invalidateQueries({
+        queryKey: preferencesQueryKey,
+      })
+    },
+  })
+}
+
+export function useRemoveMutedWordMutation() {
+  const queryClient = useQueryClient()
+
+  return useMutation({
+    mutationFn: async (mutedWord: AppBskyActorDefs.MutedWord) => {
+      await getAgent().removeMutedWord(mutedWord)
+      // triggers a refetch
+      await queryClient.invalidateQueries({
+        queryKey: preferencesQueryKey,
+      })
+    },
+  })
+}
diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx
index bd3b157bc..46628318c 100644
--- a/src/state/session/index.tsx
+++ b/src/state/session/index.tsx
@@ -133,7 +133,7 @@ function createPersistSessionHandler(
       accessJwt: session?.accessJwt,
     }
 
-    logger.info(`session: persistSession`, {
+    logger.debug(`session: persistSession`, {
       event,
       deactivated: refreshedAccount.deactivated,
     })
@@ -320,7 +320,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
   )
 
   const logout = React.useCallback<ApiContext['logout']>(async () => {
-    logger.info(`session: logout`)
+    logger.debug(`session: logout`)
     clearCurrentAccount()
     setStateAndPersist(s => {
       return {
@@ -374,7 +374,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
       }
 
       if (canReusePrevSession) {
-        logger.info(`session: attempting to reuse previous session`)
+        logger.debug(`session: attempting to reuse previous session`)
 
         agent.session = prevSession
         __globalAgent = agent
@@ -384,7 +384,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
         if (prevSession.deactivated) {
           // don't attempt to resume
           // use will be taken to the deactivated screen
-          logger.info(`session: reusing session for deactivated account`)
+          logger.debug(`session: reusing session for deactivated account`)
           return
         }
 
@@ -410,7 +410,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
             __globalAgent = PUBLIC_BSKY_AGENT
           })
       } else {
-        logger.info(`session: attempting to resume using previous session`)
+        logger.debug(`session: attempting to resume using previous session`)
 
         try {
           const freshAccount = await resumeSessionWithFreshAccount()
@@ -431,7 +431,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
       }
 
       async function resumeSessionWithFreshAccount(): Promise<SessionAccount> {
-        logger.info(`session: resumeSessionWithFreshAccount`)
+        logger.debug(`session: resumeSessionWithFreshAccount`)
 
         await networkRetry(1, () => agent.resumeSession(prevSession))
 
@@ -552,11 +552,11 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
     return persisted.onUpdate(() => {
       const session = persisted.get('session')
 
-      logger.info(`session: persisted onUpdate`, {})
+      logger.debug(`session: persisted onUpdate`, {})
 
       if (session.currentAccount && session.currentAccount.refreshJwt) {
         if (session.currentAccount?.did !== state.currentAccount?.did) {
-          logger.info(`session: persisted onUpdate, switching accounts`, {
+          logger.debug(`session: persisted onUpdate, switching accounts`, {
             from: {
               did: state.currentAccount?.did,
               handle: state.currentAccount?.handle,
@@ -569,7 +569,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
 
           initSession(session.currentAccount)
         } else {
-          logger.info(`session: persisted onUpdate, updating session`, {})
+          logger.debug(`session: persisted onUpdate, updating session`, {})
 
           /*
            * Use updated session in this tab's agent. Do not call
diff --git a/src/state/shell/composer.tsx b/src/state/shell/composer.tsx
index 696a3c5ba..c9dbfbeac 100644
--- a/src/state/shell/composer.tsx
+++ b/src/state/shell/composer.tsx
@@ -38,6 +38,8 @@ export interface ComposerOpts {
   quote?: ComposerOptsQuote
   mention?: string // handle of user to mention
   openPicker?: (pos: DOMRect | undefined) => void
+  text?: string
+  imageUris?: {uri: string; width: number; height: number}[]
 }
 
 type StateContext = ComposerOpts | undefined
diff --git a/src/state/util.ts b/src/state/util.ts
index 57f4331b0..f65d14a84 100644
--- a/src/state/util.ts
+++ b/src/state/util.ts
@@ -3,6 +3,7 @@ import {useLightboxControls} from './lightbox'
 import {useModalControls} from './modals'
 import {useComposerControls} from './shell/composer'
 import {useSetDrawerOpen} from './shell/drawer-open'
+import {useDialogStateControlContext} from '#/state/dialogs'
 
 /**
  * returns true if something was closed
@@ -12,6 +13,7 @@ export function useCloseAnyActiveElement() {
   const {closeLightbox} = useLightboxControls()
   const {closeModal} = useModalControls()
   const {closeComposer} = useComposerControls()
+  const {closeAllDialogs} = useDialogStateControlContext()
   const setDrawerOpen = useSetDrawerOpen()
   return useCallback(() => {
     if (closeLightbox()) {
@@ -23,9 +25,12 @@ export function useCloseAnyActiveElement() {
     if (closeComposer()) {
       return true
     }
+    if (closeAllDialogs()) {
+      return true
+    }
     setDrawerOpen(false)
     return false
-  }, [closeLightbox, closeModal, closeComposer, setDrawerOpen])
+  }, [closeLightbox, closeModal, closeComposer, setDrawerOpen, closeAllDialogs])
 }
 
 /**
@@ -35,11 +40,19 @@ export function useCloseAllActiveElements() {
   const {closeLightbox} = useLightboxControls()
   const {closeAllModals} = useModalControls()
   const {closeComposer} = useComposerControls()
+  const {closeAllDialogs: closeAlfDialogs} = useDialogStateControlContext()
   const setDrawerOpen = useSetDrawerOpen()
   return useCallback(() => {
     closeLightbox()
     closeAllModals()
     closeComposer()
+    closeAlfDialogs()
     setDrawerOpen(false)
-  }, [closeLightbox, closeAllModals, closeComposer, setDrawerOpen])
+  }, [
+    closeLightbox,
+    closeAllModals,
+    closeComposer,
+    closeAlfDialogs,
+    setDrawerOpen,
+  ])
 }