about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/alf/util/platform.ts26
-rw-r--r--src/components/Dialog/index.tsx15
-rw-r--r--src/components/Dialog/index.web.tsx2
-rw-r--r--src/components/Dialog/types.ts12
-rw-r--r--src/components/Prompt.tsx2
-rw-r--r--src/lib/__tests__/moderatePost_wrapped.test.ts481
-rw-r--r--src/lib/hooks/useIntentHandler.ts6
-rw-r--r--src/lib/moderatePost_wrapped.ts80
-rw-r--r--src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx2
-rw-r--r--src/state/queries/feed.ts129
-rw-r--r--src/state/util.ts11
-rw-r--r--src/view/com/home/HomeHeader.tsx15
-rw-r--r--src/view/screens/Home.tsx8
-rw-r--r--src/view/shell/desktop/Feeds.tsx6
14 files changed, 434 insertions, 361 deletions
diff --git a/src/alf/util/platform.ts b/src/alf/util/platform.ts
index 544f5480b..294e08a8b 100644
--- a/src/alf/util/platform.ts
+++ b/src/alf/util/platform.ts
@@ -1,25 +1,25 @@
-import {Platform} from 'react-native'
+import {isAndroid, isIOS, isNative, isWeb} from 'platform/detection'
 
 export function web(value: any) {
-  return Platform.select({
-    web: value,
-  })
+  if (isWeb) {
+    return value
+  }
 }
 
 export function ios(value: any) {
-  return Platform.select({
-    ios: value,
-  })
+  if (isIOS) {
+    return value
+  }
 }
 
 export function android(value: any) {
-  return Platform.select({
-    android: value,
-  })
+  if (isAndroid) {
+    return value
+  }
 }
 
 export function native(value: any) {
-  return Platform.select({
-    native: value,
-  })
+  if (isNative) {
+    return value
+  }
 }
diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx
index 27f43afd3..5c0350274 100644
--- a/src/components/Dialog/index.tsx
+++ b/src/components/Dialog/index.tsx
@@ -11,6 +11,7 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
 import {useTheme, atoms as a, flatten} from '#/alf'
 import {Portal} from '#/components/Portal'
 import {createInput} from '#/components/forms/TextField'
+import {logger} from '#/logger'
 
 import {
   DialogOuterProps,
@@ -56,7 +57,7 @@ export function Outer({
   )
 
   const close = React.useCallback<DialogControlProps['close']>(cb => {
-    if (cb) {
+    if (cb && typeof cb === 'function') {
       closeCallback.current = cb
     }
     sheet.current?.close()
@@ -74,8 +75,16 @@ export function Outer({
   const onChange = React.useCallback(
     (index: number) => {
       if (index === -1) {
-        closeCallback.current?.()
-        closeCallback.current = undefined
+        try {
+          closeCallback.current?.()
+        } catch (e: any) {
+          logger.error(`Dialog closeCallback failed`, {
+            message: e.message,
+          })
+        } finally {
+          closeCallback.current = undefined
+        }
+
         onClose?.()
         setOpenIndex(-1)
       }
diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx
index fa29fbd6c..ff05fed91 100644
--- a/src/components/Dialog/index.web.tsx
+++ b/src/components/Dialog/index.web.tsx
@@ -190,7 +190,7 @@ export function Close() {
         variant="ghost"
         color="secondary"
         shape="round"
-        onPress={close}
+        onPress={() => close()}
         label={_(msg`Close active dialog`)}>
         <ButtonIcon icon={X} size="md" />
       </Button>
diff --git a/src/components/Dialog/types.ts b/src/components/Dialog/types.ts
index 75ba825ac..161c03734 100644
--- a/src/components/Dialog/types.ts
+++ b/src/components/Dialog/types.ts
@@ -6,8 +6,13 @@ import {ViewStyleProp} from '#/alf'
 
 type A11yProps = Required<AccessibilityProps>
 
+export type DialogControlProps = {
+  open: (options?: DialogControlOpenOptions) => void
+  close: (callback?: () => void) => void
+}
+
 export type DialogContextProps = {
-  close: () => void
+  close: DialogControlProps['close']
 }
 
 export type DialogControlOpenOptions = {
@@ -20,11 +25,6 @@ export type DialogControlOpenOptions = {
   index?: number
 }
 
-export type DialogControlProps = {
-  open: (options?: DialogControlOpenOptions) => void
-  close: (callback?: () => void) => void
-}
-
 export type DialogOuterProps = {
   control: {
     ref: React.RefObject<DialogControlProps>
diff --git a/src/components/Prompt.tsx b/src/components/Prompt.tsx
index 411679102..8e55bd834 100644
--- a/src/components/Prompt.tsx
+++ b/src/components/Prompt.tsx
@@ -89,7 +89,7 @@ export function Cancel({
       color="secondary"
       size="small"
       label={_(msg`Cancel`)}
-      onPress={close}>
+      onPress={() => close()}>
       {children}
     </Button>
   )
diff --git a/src/lib/__tests__/moderatePost_wrapped.test.ts b/src/lib/__tests__/moderatePost_wrapped.test.ts
index 1d907963f..c35c1ef77 100644
--- a/src/lib/__tests__/moderatePost_wrapped.test.ts
+++ b/src/lib/__tests__/moderatePost_wrapped.test.ts
@@ -11,12 +11,12 @@ describe(`hasMutedWord`, () => {
       })
       rt.detectFacetsWithoutResolution()
 
-      const match = hasMutedWord(
-        [{value: 'outlineTag', targets: ['tag']}],
-        rt.text,
-        rt.facets,
-        ['outlineTag'],
-      )
+      const match = hasMutedWord({
+        mutedWords: [{value: 'outlineTag', targets: ['tag']}],
+        text: rt.text,
+        facets: rt.facets,
+        outlineTags: ['outlineTag'],
+      })
 
       expect(match).toBe(true)
     })
@@ -27,12 +27,12 @@ describe(`hasMutedWord`, () => {
       })
       rt.detectFacetsWithoutResolution()
 
-      const match = hasMutedWord(
-        [{value: 'inlineTag', targets: ['tag']}],
-        rt.text,
-        rt.facets,
-        ['outlineTag'],
-      )
+      const match = hasMutedWord({
+        mutedWords: [{value: 'inlineTag', targets: ['tag']}],
+        text: rt.text,
+        facets: rt.facets,
+        outlineTags: ['outlineTag'],
+      })
 
       expect(match).toBe(true)
     })
@@ -43,12 +43,12 @@ describe(`hasMutedWord`, () => {
       })
       rt.detectFacetsWithoutResolution()
 
-      const match = hasMutedWord(
-        [{value: 'inlineTag', targets: ['content']}],
-        rt.text,
-        rt.facets,
-        ['outlineTag'],
-      )
+      const match = hasMutedWord({
+        mutedWords: [{value: 'inlineTag', targets: ['content']}],
+        text: rt.text,
+        facets: rt.facets,
+        outlineTags: ['outlineTag'],
+      })
 
       expect(match).toBe(true)
     })
@@ -59,12 +59,12 @@ describe(`hasMutedWord`, () => {
       })
       rt.detectFacetsWithoutResolution()
 
-      const match = hasMutedWord(
-        [{value: 'inlineTag', targets: ['tag']}],
-        rt.text,
-        rt.facets,
-        [],
-      )
+      const match = hasMutedWord({
+        mutedWords: [{value: 'inlineTag', targets: ['tag']}],
+        text: rt.text,
+        facets: rt.facets,
+        outlineTags: [],
+      })
 
       expect(match).toBe(false)
     })
@@ -80,12 +80,12 @@ describe(`hasMutedWord`, () => {
       })
       rt.detectFacetsWithoutResolution()
 
-      const match = hasMutedWord(
-        [{value: 'ๅธŒ', targets: ['content']}],
-        rt.text,
-        rt.facets,
-        [],
-      )
+      const match = hasMutedWord({
+        mutedWords: [{value: 'ๅธŒ', targets: ['content']}],
+        text: rt.text,
+        facets: rt.facets,
+        outlineTags: [],
+      })
 
       expect(match).toBe(true)
     })
@@ -96,12 +96,12 @@ describe(`hasMutedWord`, () => {
       })
       rt.detectFacetsWithoutResolution()
 
-      const match = hasMutedWord(
-        [{value: 'politics', targets: ['content']}],
-        rt.text,
-        rt.facets,
-        [],
-      )
+      const match = hasMutedWord({
+        mutedWords: [{value: 'politics', targets: ['content']}],
+        text: rt.text,
+        facets: rt.facets,
+        outlineTags: [],
+      })
 
       expect(match).toBe(false)
     })
@@ -112,12 +112,12 @@ describe(`hasMutedWord`, () => {
       })
       rt.detectFacetsWithoutResolution()
 
-      const match = hasMutedWord(
-        [{value: 'javascript', targets: ['content']}],
-        rt.text,
-        rt.facets,
-        [],
-      )
+      const match = hasMutedWord({
+        mutedWords: [{value: 'javascript', targets: ['content']}],
+        text: rt.text,
+        facets: rt.facets,
+        outlineTags: [],
+      })
 
       expect(match).toBe(true)
     })
@@ -130,12 +130,12 @@ describe(`hasMutedWord`, () => {
       })
       rt.detectFacetsWithoutResolution()
 
-      const match = hasMutedWord(
-        [{value: 'javascript', targets: ['content']}],
-        rt.text,
-        rt.facets,
-        [],
-      )
+      const match = hasMutedWord({
+        mutedWords: [{value: 'javascript', targets: ['content']}],
+        text: rt.text,
+        facets: rt.facets,
+        outlineTags: [],
+      })
 
       expect(match).toBe(true)
     })
@@ -146,12 +146,12 @@ describe(`hasMutedWord`, () => {
       })
       rt.detectFacetsWithoutResolution()
 
-      const match = hasMutedWord(
-        [{value: 'ai', targets: ['content']}],
-        rt.text,
-        rt.facets,
-        [],
-      )
+      const match = hasMutedWord({
+        mutedWords: [{value: 'ai', targets: ['content']}],
+        text: rt.text,
+        facets: rt.facets,
+        outlineTags: [],
+      })
 
       expect(match).toBe(false)
     })
@@ -162,12 +162,12 @@ describe(`hasMutedWord`, () => {
       })
       rt.detectFacetsWithoutResolution()
 
-      const match = hasMutedWord(
-        [{value: 'brain', targets: ['content']}],
-        rt.text,
-        rt.facets,
-        [],
-      )
+      const match = hasMutedWord({
+        mutedWords: [{value: 'brain', targets: ['content']}],
+        text: rt.text,
+        facets: rt.facets,
+        outlineTags: [],
+      })
 
       expect(match).toBe(true)
     })
@@ -178,12 +178,12 @@ describe(`hasMutedWord`, () => {
       })
       rt.detectFacetsWithoutResolution()
 
-      const match = hasMutedWord(
-        [{value: `:)`, targets: ['content']}],
-        rt.text,
-        rt.facets,
-        [],
-      )
+      const match = hasMutedWord({
+        mutedWords: [{value: `:)`, targets: ['content']}],
+        text: rt.text,
+        facets: rt.facets,
+        outlineTags: [],
+      })
 
       expect(match).toBe(true)
     })
@@ -197,23 +197,23 @@ describe(`hasMutedWord`, () => {
       rt.detectFacetsWithoutResolution()
 
       it(`match: yay!`, () => {
-        const match = hasMutedWord(
-          [{value: 'yay!', targets: ['content']}],
-          rt.text,
-          rt.facets,
-          [],
-        )
+        const match = hasMutedWord({
+          mutedWords: [{value: 'yay!', targets: ['content']}],
+          text: rt.text,
+          facets: rt.facets,
+          outlineTags: [],
+        })
 
         expect(match).toBe(true)
       })
 
       it(`match: yay`, () => {
-        const match = hasMutedWord(
-          [{value: 'yay', targets: ['content']}],
-          rt.text,
-          rt.facets,
-          [],
-        )
+        const match = hasMutedWord({
+          mutedWords: [{value: 'yay', targets: ['content']}],
+          text: rt.text,
+          facets: rt.facets,
+          outlineTags: [],
+        })
 
         expect(match).toBe(true)
       })
@@ -226,24 +226,24 @@ describe(`hasMutedWord`, () => {
       rt.detectFacetsWithoutResolution()
 
       it(`match: y!ppee`, () => {
-        const match = hasMutedWord(
-          [{value: 'y!ppee', targets: ['content']}],
-          rt.text,
-          rt.facets,
-          [],
-        )
+        const match = hasMutedWord({
+          mutedWords: [{value: 'y!ppee', targets: ['content']}],
+          text: rt.text,
+          facets: rt.facets,
+          outlineTags: [],
+        })
 
         expect(match).toBe(true)
       })
 
       // single exclamation point, source has double
       it(`no match: y!ppee!`, () => {
-        const match = hasMutedWord(
-          [{value: 'y!ppee!', targets: ['content']}],
-          rt.text,
-          rt.facets,
-          [],
-        )
+        const match = hasMutedWord({
+          mutedWords: [{value: 'y!ppee!', targets: ['content']}],
+          text: rt.text,
+          facets: rt.facets,
+          outlineTags: [],
+        })
 
         expect(match).toBe(true)
       })
@@ -256,23 +256,23 @@ describe(`hasMutedWord`, () => {
       rt.detectFacetsWithoutResolution()
 
       it(`match: S@assy`, () => {
-        const match = hasMutedWord(
-          [{value: 'S@assy', targets: ['content']}],
-          rt.text,
-          rt.facets,
-          [],
-        )
+        const match = hasMutedWord({
+          mutedWords: [{value: 'S@assy', targets: ['content']}],
+          text: rt.text,
+          facets: rt.facets,
+          outlineTags: [],
+        })
 
         expect(match).toBe(true)
       })
 
       it(`match: s@assy`, () => {
-        const match = hasMutedWord(
-          [{value: 's@assy', targets: ['content']}],
-          rt.text,
-          rt.facets,
-          [],
-        )
+        const match = hasMutedWord({
+          mutedWords: [{value: 's@assy', targets: ['content']}],
+          text: rt.text,
+          facets: rt.facets,
+          outlineTags: [],
+        })
 
         expect(match).toBe(true)
       })
@@ -286,12 +286,12 @@ describe(`hasMutedWord`, () => {
 
       // case insensitive
       it(`match: new york times`, () => {
-        const match = hasMutedWord(
-          [{value: 'new york times', targets: ['content']}],
-          rt.text,
-          rt.facets,
-          [],
-        )
+        const match = hasMutedWord({
+          mutedWords: [{value: 'new york times', targets: ['content']}],
+          text: rt.text,
+          facets: rt.facets,
+          outlineTags: [],
+        })
 
         expect(match).toBe(true)
       })
@@ -304,23 +304,23 @@ describe(`hasMutedWord`, () => {
       rt.detectFacetsWithoutResolution()
 
       it(`match: !command`, () => {
-        const match = hasMutedWord(
-          [{value: `!command`, targets: ['content']}],
-          rt.text,
-          rt.facets,
-          [],
-        )
+        const match = hasMutedWord({
+          mutedWords: [{value: `!command`, targets: ['content']}],
+          text: rt.text,
+          facets: rt.facets,
+          outlineTags: [],
+        })
 
         expect(match).toBe(true)
       })
 
       it(`match: command`, () => {
-        const match = hasMutedWord(
-          [{value: `command`, targets: ['content']}],
-          rt.text,
-          rt.facets,
-          [],
-        )
+        const match = hasMutedWord({
+          mutedWords: [{value: `command`, targets: ['content']}],
+          text: rt.text,
+          facets: rt.facets,
+          outlineTags: [],
+        })
 
         expect(match).toBe(true)
       })
@@ -331,12 +331,12 @@ describe(`hasMutedWord`, () => {
         })
         rt.detectFacetsWithoutResolution()
 
-        const match = hasMutedWord(
-          [{value: `!command`, targets: ['content']}],
-          rt.text,
-          rt.facets,
-          [],
-        )
+        const match = hasMutedWord({
+          mutedWords: [{value: `!command`, targets: ['content']}],
+          text: rt.text,
+          facets: rt.facets,
+          outlineTags: [],
+        })
 
         expect(match).toBe(false)
       })
@@ -349,23 +349,23 @@ describe(`hasMutedWord`, () => {
       rt.detectFacetsWithoutResolution()
 
       it(`match: e/acc`, () => {
-        const match = hasMutedWord(
-          [{value: `e/acc`, targets: ['content']}],
-          rt.text,
-          rt.facets,
-          [],
-        )
+        const match = hasMutedWord({
+          mutedWords: [{value: `e/acc`, targets: ['content']}],
+          text: rt.text,
+          facets: rt.facets,
+          outlineTags: [],
+        })
 
         expect(match).toBe(true)
       })
 
       it(`match: acc`, () => {
-        const match = hasMutedWord(
-          [{value: `acc`, targets: ['content']}],
-          rt.text,
-          rt.facets,
-          [],
-        )
+        const match = hasMutedWord({
+          mutedWords: [{value: `acc`, targets: ['content']}],
+          text: rt.text,
+          facets: rt.facets,
+          outlineTags: [],
+        })
 
         expect(match).toBe(true)
       })
@@ -378,45 +378,45 @@ describe(`hasMutedWord`, () => {
       rt.detectFacetsWithoutResolution()
 
       it(`match: super-bad`, () => {
-        const match = hasMutedWord(
-          [{value: `super-bad`, targets: ['content']}],
-          rt.text,
-          rt.facets,
-          [],
-        )
+        const match = hasMutedWord({
+          mutedWords: [{value: `super-bad`, targets: ['content']}],
+          text: rt.text,
+          facets: rt.facets,
+          outlineTags: [],
+        })
 
         expect(match).toBe(true)
       })
 
       it(`match: super`, () => {
-        const match = hasMutedWord(
-          [{value: `super`, targets: ['content']}],
-          rt.text,
-          rt.facets,
-          [],
-        )
+        const match = hasMutedWord({
+          mutedWords: [{value: `super`, targets: ['content']}],
+          text: rt.text,
+          facets: rt.facets,
+          outlineTags: [],
+        })
 
         expect(match).toBe(true)
       })
 
       it(`match: super bad`, () => {
-        const match = hasMutedWord(
-          [{value: `super bad`, targets: ['content']}],
-          rt.text,
-          rt.facets,
-          [],
-        )
+        const match = hasMutedWord({
+          mutedWords: [{value: `super bad`, targets: ['content']}],
+          text: rt.text,
+          facets: rt.facets,
+          outlineTags: [],
+        })
 
         expect(match).toBe(true)
       })
 
       it(`match: superbad`, () => {
-        const match = hasMutedWord(
-          [{value: `superbad`, targets: ['content']}],
-          rt.text,
-          rt.facets,
-          [],
-        )
+        const match = hasMutedWord({
+          mutedWords: [{value: `superbad`, targets: ['content']}],
+          text: rt.text,
+          facets: rt.facets,
+          outlineTags: [],
+        })
 
         expect(match).toBe(false)
       })
@@ -429,47 +429,49 @@ describe(`hasMutedWord`, () => {
       rt.detectFacetsWithoutResolution()
 
       it(`match: idk what this would be`, () => {
-        const match = hasMutedWord(
-          [{value: `idk what this would be`, targets: ['content']}],
-          rt.text,
-          rt.facets,
-          [],
-        )
+        const match = hasMutedWord({
+          mutedWords: [{value: `idk what this would be`, targets: ['content']}],
+          text: rt.text,
+          facets: rt.facets,
+          outlineTags: [],
+        })
 
         expect(match).toBe(true)
       })
 
       it(`no match: idk what this would be for`, () => {
         // extra word
-        const match = hasMutedWord(
-          [{value: `idk what this would be for`, targets: ['content']}],
-          rt.text,
-          rt.facets,
-          [],
-        )
+        const match = hasMutedWord({
+          mutedWords: [
+            {value: `idk what this would be for`, targets: ['content']},
+          ],
+          text: rt.text,
+          facets: rt.facets,
+          outlineTags: [],
+        })
 
         expect(match).toBe(false)
       })
 
       it(`match: idk`, () => {
         // extra word
-        const match = hasMutedWord(
-          [{value: `idk`, targets: ['content']}],
-          rt.text,
-          rt.facets,
-          [],
-        )
+        const match = hasMutedWord({
+          mutedWords: [{value: `idk`, targets: ['content']}],
+          text: rt.text,
+          facets: rt.facets,
+          outlineTags: [],
+        })
 
         expect(match).toBe(true)
       })
 
       it(`match: idkwhatthiswouldbe`, () => {
-        const match = hasMutedWord(
-          [{value: `idkwhatthiswouldbe`, targets: ['content']}],
-          rt.text,
-          rt.facets,
-          [],
-        )
+        const match = hasMutedWord({
+          mutedWords: [{value: `idkwhatthiswouldbe`, targets: ['content']}],
+          text: rt.text,
+          facets: rt.facets,
+          outlineTags: [],
+        })
 
         expect(match).toBe(false)
       })
@@ -482,45 +484,45 @@ describe(`hasMutedWord`, () => {
       rt.detectFacetsWithoutResolution()
 
       it(`match: context(iykyk)`, () => {
-        const match = hasMutedWord(
-          [{value: `context(iykyk)`, targets: ['content']}],
-          rt.text,
-          rt.facets,
-          [],
-        )
+        const match = hasMutedWord({
+          mutedWords: [{value: `context(iykyk)`, targets: ['content']}],
+          text: rt.text,
+          facets: rt.facets,
+          outlineTags: [],
+        })
 
         expect(match).toBe(true)
       })
 
       it(`match: context`, () => {
-        const match = hasMutedWord(
-          [{value: `context`, targets: ['content']}],
-          rt.text,
-          rt.facets,
-          [],
-        )
+        const match = hasMutedWord({
+          mutedWords: [{value: `context`, targets: ['content']}],
+          text: rt.text,
+          facets: rt.facets,
+          outlineTags: [],
+        })
 
         expect(match).toBe(true)
       })
 
       it(`match: iykyk`, () => {
-        const match = hasMutedWord(
-          [{value: `iykyk`, targets: ['content']}],
-          rt.text,
-          rt.facets,
-          [],
-        )
+        const match = hasMutedWord({
+          mutedWords: [{value: `iykyk`, targets: ['content']}],
+          text: rt.text,
+          facets: rt.facets,
+          outlineTags: [],
+        })
 
         expect(match).toBe(true)
       })
 
       it(`match: (iykyk)`, () => {
-        const match = hasMutedWord(
-          [{value: `(iykyk)`, targets: ['content']}],
-          rt.text,
-          rt.facets,
-          [],
-        )
+        const match = hasMutedWord({
+          mutedWords: [{value: `(iykyk)`, targets: ['content']}],
+          text: rt.text,
+          facets: rt.facets,
+          outlineTags: [],
+        })
 
         expect(match).toBe(true)
       })
@@ -533,12 +535,12 @@ describe(`hasMutedWord`, () => {
       rt.detectFacetsWithoutResolution()
 
       it(`match: ๐Ÿฆ‹`, () => {
-        const match = hasMutedWord(
-          [{value: `๐Ÿฆ‹`, targets: ['content']}],
-          rt.text,
-          rt.facets,
-          [],
-        )
+        const match = hasMutedWord({
+          mutedWords: [{value: `๐Ÿฆ‹`, targets: ['content']}],
+          text: rt.text,
+          facets: rt.facets,
+          outlineTags: [],
+        })
 
         expect(match).toBe(true)
       })
@@ -553,23 +555,46 @@ describe(`hasMutedWord`, () => {
       rt.detectFacetsWithoutResolution()
 
       it(`match: stop worrying`, () => {
-        const match = hasMutedWord(
-          [{value: 'stop worrying', targets: ['content']}],
-          rt.text,
-          rt.facets,
-          [],
-        )
+        const match = hasMutedWord({
+          mutedWords: [{value: 'stop worrying', targets: ['content']}],
+          text: rt.text,
+          facets: rt.facets,
+          outlineTags: [],
+        })
 
         expect(match).toBe(true)
       })
 
       it(`match: turtles, or how`, () => {
-        const match = hasMutedWord(
-          [{value: 'turtles, or how', targets: ['content']}],
-          rt.text,
-          rt.facets,
-          [],
-        )
+        const match = hasMutedWord({
+          mutedWords: [{value: 'turtles, or how', targets: ['content']}],
+          text: rt.text,
+          facets: rt.facets,
+          outlineTags: [],
+        })
+
+        expect(match).toBe(true)
+      })
+    })
+  })
+
+  describe(`languages without spaces`, () => {
+    // I love turtles, or how I learned to stop worrying and love the internet
+    describe(`็งใฏใ‚ซใƒกใŒๅฅฝใใงใ™ใ€ใพใŸใฏใฉใฎใ‚ˆใ†ใซใ—ใฆๅฟƒ้…ใ™ใ‚‹ใฎใ‚’ใ‚„ใ‚ใฆใ‚คใƒณใ‚ฟใƒผใƒใƒƒใƒˆใ‚’ๆ„›ใ™ใ‚‹ใ‚ˆใ†ใซใชใฃใŸใฎใ‹`, () => {
+      const rt = new RichText({
+        text: `็งใฏใ‚ซใƒกใŒๅฅฝใใงใ™ใ€ใพใŸใฏใฉใฎใ‚ˆใ†ใซใ—ใฆๅฟƒ้…ใ™ใ‚‹ใฎใ‚’ใ‚„ใ‚ใฆใ‚คใƒณใ‚ฟใƒผใƒใƒƒใƒˆใ‚’ๆ„›ใ™ใ‚‹ใ‚ˆใ†ใซใชใฃใŸใฎใ‹`,
+      })
+      rt.detectFacetsWithoutResolution()
+
+      // internet
+      it(`match: ใ‚คใƒณใ‚ฟใƒผใƒใƒƒใƒˆ`, () => {
+        const match = hasMutedWord({
+          mutedWords: [{value: 'ใ‚คใƒณใ‚ฟใƒผใƒใƒƒใƒˆ', targets: ['content']}],
+          text: rt.text,
+          facets: rt.facets,
+          outlineTags: [],
+          languages: ['ja'],
+        })
 
         expect(match).toBe(true)
       })
diff --git a/src/lib/hooks/useIntentHandler.ts b/src/lib/hooks/useIntentHandler.ts
index de9a96da9..d1e2de31d 100644
--- a/src/lib/hooks/useIntentHandler.ts
+++ b/src/lib/hooks/useIntentHandler.ts
@@ -3,6 +3,7 @@ import * as Linking from 'expo-linking'
 import {isNative} from 'platform/detection'
 import {useComposerControls} from 'state/shell'
 import {useSession} from 'state/session'
+import {useCloseAllActiveElements} from 'state/util'
 
 type IntentType = 'compose'
 
@@ -42,6 +43,7 @@ export function useIntentHandler() {
 }
 
 function useComposeIntent() {
+  const closeAllActiveElements = useCloseAllActiveElements()
   const {openComposer} = useComposerControls()
   const {hasSession} = useSession()
 
@@ -55,6 +57,8 @@ function useComposeIntent() {
     }) => {
       if (!hasSession) return
 
+      closeAllActiveElements()
+
       const imageUris = imageUrisStr
         ?.split(',')
         .filter(part => {
@@ -82,6 +86,6 @@ function useComposeIntent() {
         })
       }, 500)
     },
-    [openComposer, hasSession],
+    [hasSession, closeAllActiveElements, openComposer],
   )
 }
diff --git a/src/lib/moderatePost_wrapped.ts b/src/lib/moderatePost_wrapped.ts
index 862f2de6f..428dbabf4 100644
--- a/src/lib/moderatePost_wrapped.ts
+++ b/src/lib/moderatePost_wrapped.ts
@@ -21,12 +21,34 @@ const REGEX = {
   WORD_BOUNDARY: /[\s\n\t\r\f\v]+?/g,
 }
 
-export function hasMutedWord(
-  mutedWords: AppBskyActorDefs.MutedWord[],
-  text: string,
-  facets?: AppBskyRichtextFacet.Main[],
-  outlineTags?: string[],
-) {
+/**
+ * List of 2-letter lang codes for languages that either don't use spaces, or
+ * don't use spaces in a way conducive to word-based filtering.
+ *
+ * For these, we use a simple `String.includes` to check for a match.
+ */
+const LANGUAGE_EXCEPTIONS = [
+  'ja', // Japanese
+  'zh', // Chinese
+  'ko', // Korean
+  'th', // Thai
+  'vi', // Vietnamese
+]
+
+export function hasMutedWord({
+  mutedWords,
+  text,
+  facets,
+  outlineTags,
+  languages,
+}: {
+  mutedWords: AppBskyActorDefs.MutedWord[]
+  text: string
+  facets?: AppBskyRichtextFacet.Main[]
+  outlineTags?: string[]
+  languages?: string[]
+}) {
+  const exception = LANGUAGE_EXCEPTIONS.includes(languages?.[0] || '')
   const tags = ([] as string[])
     .concat(outlineTags || [])
     .concat(
@@ -48,8 +70,9 @@ export function hasMutedWord(
     if (tags.includes(mutedWord)) return true
     // rest of the checks are for `content` only
     if (!mute.targets.includes('content')) continue
-    // single character, has to use includes
-    if (mutedWord.length === 1 && postText.includes(mutedWord)) return true
+    // single character or other exception, has to use includes
+    if ((mutedWord.length === 1 || exception) && postText.includes(mutedWord))
+      return true
     // too long
     if (mutedWord.length > postText.length) continue
     // exact match
@@ -134,19 +157,28 @@ export function moderatePost_wrapped(
   }
 
   if (AppBskyFeedPost.isRecord(subject.record)) {
-    let muted = hasMutedWord(
+    let muted = hasMutedWord({
       mutedWords,
-      subject.record.text,
-      subject.record.facets || [],
-      subject.record.tags || [],
-    )
+      text: subject.record.text,
+      facets: subject.record.facets || [],
+      outlineTags: subject.record.tags || [],
+      languages: subject.record.langs,
+    })
 
     if (
       subject.record.embed &&
       AppBskyEmbedImages.isMain(subject.record.embed)
     ) {
       for (const image of subject.record.embed.images) {
-        muted = muted || hasMutedWord(mutedWords, image.alt, [], [])
+        muted =
+          muted ||
+          hasMutedWord({
+            mutedWords,
+            text: image.alt,
+            facets: [],
+            outlineTags: [],
+            languages: subject.record.langs,
+          })
       }
     }
 
@@ -172,17 +204,25 @@ export function moderatePost_wrapped(
       if (AppBskyFeedPost.isRecord(subject.embed.record.value)) {
         embedHidden =
           embedHidden ||
-          hasMutedWord(
+          hasMutedWord({
             mutedWords,
-            subject.embed.record.value.text,
-            subject.embed.record.value.facets,
-            subject.embed.record.value.tags,
-          )
+            text: subject.embed.record.value.text,
+            facets: subject.embed.record.value.facets,
+            outlineTags: subject.embed.record.value.tags,
+            languages: subject.embed.record.value.langs,
+          })
 
         if (AppBskyEmbedImages.isMain(subject.embed.record.value.embed)) {
           for (const image of subject.embed.record.value.embed.images) {
             embedHidden =
-              embedHidden || hasMutedWord(mutedWords, image.alt, [], [])
+              embedHidden ||
+              hasMutedWord({
+                mutedWords,
+                text: image.alt,
+                facets: [],
+                outlineTags: [],
+                languages: subject.embed.record.value.langs,
+              })
           }
         }
       }
diff --git a/src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx b/src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx
index c3d616407..9d9cc5602 100644
--- a/src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx
+++ b/src/screens/Onboarding/StepModeration/AdultContentEnabledPref.tsx
@@ -118,7 +118,7 @@ export function AdultContentEnabledPref({
           </Trans>
         </Prompt.Description>
         <Prompt.Actions>
-          <Prompt.Action onPress={prompt.close}>
+          <Prompt.Action onPress={() => prompt.close()}>
             <Trans>OK</Trans>
           </Prompt.Action>
         </Prompt.Actions>
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/util.ts b/src/state/util.ts
index 57f4331b0..7b49b5b46 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
@@ -35,11 +36,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,
+  ])
 }
diff --git a/src/view/com/home/HomeHeader.tsx b/src/view/com/home/HomeHeader.tsx
index 3df3858ba..bbd16465a 100644
--- a/src/view/com/home/HomeHeader.tsx
+++ b/src/view/com/home/HomeHeader.tsx
@@ -1,7 +1,7 @@
 import React from 'react'
 import {RenderTabBarFnProps} from 'view/com/pager/Pager'
 import {HomeHeaderLayout} from './HomeHeaderLayout'
-import {usePinnedFeedsInfos} from '#/state/queries/feed'
+import {FeedSourceInfo} from '#/state/queries/feed'
 import {useNavigation} from '@react-navigation/native'
 import {NavigationProp} from 'lib/routes/types'
 import {isWeb} from 'platform/detection'
@@ -9,15 +9,22 @@ import {TabBar} from '../pager/TabBar'
 import {usePalette} from '#/lib/hooks/usePalette'
 
 export function HomeHeader(
-  props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
+  props: RenderTabBarFnProps & {
+    testID?: string
+    onPressSelected: () => void
+    feeds: FeedSourceInfo[]
+  },
 ) {
+  const {feeds} = props
   const navigation = useNavigation<NavigationProp>()
-  const {feeds, hasPinnedCustom} = usePinnedFeedsInfos()
   const pal = usePalette('default')
 
+  const hasPinnedCustom = React.useMemo<boolean>(() => {
+    return feeds.some(tab => tab.uri !== '')
+  }, [feeds])
+
   const items = React.useMemo(() => {
     const pinnedNames = feeds.map(f => f.displayName)
-
     if (!hasPinnedCustom) {
       return pinnedNames.concat('Feeds โœจ')
     }
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index 856c237f6..7ad9beb56 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -17,11 +17,12 @@ import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
 import {emitSoftReset} from '#/state/events'
 import {useSession} from '#/state/session'
 import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed'
+import {useSetTitle} from '#/lib/hooks/useSetTitle'
 
 type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
 export function HomeScreen(props: Props) {
   const {data: preferences} = usePreferencesQuery()
-  const {feeds: pinnedFeedInfos, isLoading: isPinnedFeedsLoading} =
+  const {data: pinnedFeedInfos, isLoading: isPinnedFeedsLoading} =
     usePinnedFeedsInfos()
   if (preferences && pinnedFeedInfos && !isPinnedFeedsLoading) {
     return (
@@ -66,6 +67,8 @@ function HomeScreenReady({
   const selectedIndex = Math.max(0, maybeFoundIndex)
   const selectedFeed = allFeeds[selectedIndex]
 
+  useSetTitle(pinnedFeedInfos[selectedIndex]?.displayName)
+
   const pagerRef = React.useRef<PagerRef>(null)
   const lastPagerReportedIndexRef = React.useRef(selectedIndex)
   React.useLayoutEffect(() => {
@@ -124,10 +127,11 @@ function HomeScreenReady({
           onSelect={props.onSelect}
           testID="homeScreenFeedTabs"
           onPressSelected={onPressSelected}
+          feeds={pinnedFeedInfos}
         />
       )
     },
-    [onPressSelected],
+    [onPressSelected, pinnedFeedInfos],
   )
 
   const renderFollowingEmptyState = React.useCallback(() => {
diff --git a/src/view/shell/desktop/Feeds.tsx b/src/view/shell/desktop/Feeds.tsx
index c3b1caa35..f447490b3 100644
--- a/src/view/shell/desktop/Feeds.tsx
+++ b/src/view/shell/desktop/Feeds.tsx
@@ -15,7 +15,7 @@ import {emitSoftReset} from '#/state/events'
 export function DesktopFeeds() {
   const pal = usePalette('default')
   const {_} = useLingui()
-  const {feeds: pinnedFeedInfos} = usePinnedFeedsInfos()
+  const {data: pinnedFeedInfos} = usePinnedFeedsInfos()
   const selectedFeed = useSelectedFeed()
   const setSelectedFeed = useSetSelectedFeed()
   const navigation = useNavigation<NavigationProp>()
@@ -25,7 +25,9 @@ export function DesktopFeeds() {
     }
     return getCurrentRoute(state)
   })
-
+  if (!pinnedFeedInfos) {
+    return null
+  }
   return (
     <View style={[styles.container, pal.view]}>
       {pinnedFeedInfos.map(feedInfo => {