From 58aaad704aa971c5ebbf5a5f330a2e2129b557f6 Mon Sep 17 00:00:00 2001
From: Eric Bailey <git@esb.lol>
Date: Mon, 26 Feb 2024 22:33:48 -0600
Subject: Add tags and mute words (#2968)

* Add bare minimum hashtags support (#2804)

* Add bare minimum hashtags support

As atproto/api already parses hashtags, this is as simple as hooking it
up like link segments.

This is "bare minimum" because:

- Opening hashtag "#foo" is actually just a search for "foo" right now
  to work around #2491.
- There is no integration in the composer. This hasn't stopped people
  from using hashtags already, and can be added later.
- This change itself only had to hook things up - thank you for having
  already put the hashtag parsing in place.

* Remove workaround for hash search not working now that it's fixed

* Add RichTextTag and TagMenu

* Sketch

* Remove hackfix

* Some cleanup

* Sketch web

* Mobile design

* Mobile handling of tags search

* Web only

* Fix navigation woes

* Use new callback

* Hook it up

* Integrate muted tags

* Fix dropdown styles

* Type error

* Use close callback

* Fix styles

* Cleanup, install latest sdk

* Quick muted words screen

* Targets

* Dir structure

* Icons, list view

* Move to dialog

* Add removal confirmation

* Swap copy

* Improve checkboxees

* Update matching, add tests

* Moderate embeds

* Create global dialogs concept again to prevent flashing

* Add access from moderation screen

* Highlight tags on native

* Add web highlighting

* Add close to web modal

* Adjust close color

* Rename toggles and adjust logic

* Icon update

* Load states

* Improve regex

* Improve regex

* Improve regex

* Revert link test

* Hyphenated words

* Improve matching

* Enhance

* Some tweaks

* Muted words modal changes

* Handle invalid handles, handle long tags

* Remove main regex

* Better test

* Space/punct check drop to includes

* Lowercase post text before comparison

* Add better real world test case

---------

Co-authored-by: Kisaragi Hiu <mail@kisaragi-hiu.com>
---
 src/state/dialogs/index.tsx | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

(limited to 'src/state/dialogs/index.tsx')

diff --git a/src/state/dialogs/index.tsx b/src/state/dialogs/index.tsx
index 4cafaa086..ae762bd97 100644
--- a/src/state/dialogs/index.tsx
+++ b/src/state/dialogs/index.tsx
@@ -1,5 +1,6 @@
 import React from 'react'
 import {DialogControlProps} from '#/components/Dialog'
+import {Provider as GlobalDialogsProvider} from '#/components/dialogs/Context'
 
 const DialogContext = React.createContext<{
   activeDialogs: React.MutableRefObject<
@@ -37,7 +38,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
   return (
     <DialogContext.Provider value={context}>
       <DialogControlContext.Provider value={controls}>
-        {children}
+        <GlobalDialogsProvider>{children}</GlobalDialogsProvider>
       </DialogControlContext.Provider>
     </DialogContext.Provider>
   )
-- 
cgit 1.4.1


From 2440975bd224e4c39b633b21c461cb8b8d3c1f9b Mon Sep 17 00:00:00 2001
From: Eric Bailey <git@esb.lol>
Date: Wed, 28 Feb 2024 20:06:26 -0600
Subject: Integrate new dialogs into old back handling (#3023)

---
 src/components/Dialog/context.ts    | 13 +++++++++----
 src/components/Dialog/index.tsx     |  8 ++++++--
 src/components/Dialog/index.web.tsx |  8 ++++++--
 src/components/Dialog/types.ts      | 19 +++++++++++++++----
 src/state/dialogs/index.tsx         | 28 ++++++++++++++++++++++------
 src/state/util.ts                   |  8 ++++++--
 6 files changed, 64 insertions(+), 20 deletions(-)

(limited to 'src/state/dialogs/index.tsx')

diff --git a/src/components/Dialog/context.ts b/src/components/Dialog/context.ts
index f0c7c983a..eb717d8e2 100644
--- a/src/components/Dialog/context.ts
+++ b/src/components/Dialog/context.ts
@@ -3,7 +3,7 @@ import React from 'react'
 import {useDialogStateContext} from '#/state/dialogs'
 import {
   DialogContextProps,
-  DialogControlProps,
+  DialogControlRefProps,
   DialogOuterProps,
 } from '#/components/Dialog/types'
 
@@ -17,7 +17,7 @@ export function useDialogContext() {
 
 export function useDialogControl(): DialogOuterProps['control'] {
   const id = React.useId()
-  const control = React.useRef<DialogControlProps>({
+  const control = React.useRef<DialogControlRefProps>({
     open: () => {},
     close: () => {},
   })
@@ -32,8 +32,13 @@ export function useDialogControl(): DialogOuterProps['control'] {
   }, [id, activeDialogs])
 
   return {
+    id,
     ref: control,
-    open: () => control.current.open(),
-    close: cb => control.current.close(cb),
+    open: () => {
+      control.current.open()
+    },
+    close: cb => {
+      control.current.close(cb)
+    },
   }
 }
diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx
index 5c0350274..6dfc24f3b 100644
--- a/src/components/Dialog/index.tsx
+++ b/src/components/Dialog/index.tsx
@@ -12,6 +12,7 @@ import {useTheme, atoms as a, flatten} from '#/alf'
 import {Portal} from '#/components/Portal'
 import {createInput} from '#/components/forms/TextField'
 import {logger} from '#/logger'
+import {useDialogStateContext} from '#/state/dialogs'
 
 import {
   DialogOuterProps,
@@ -37,6 +38,7 @@ export function Outer({
   const hasSnapPoints = !!sheetOptions.snapPoints
   const insets = useSafeAreaInsets()
   const closeCallback = React.useRef<() => void>()
+  const {openDialogs} = useDialogStateContext()
 
   /*
    * Used to manage open/closed, but index is otherwise handled internally by `BottomSheet`
@@ -50,10 +52,11 @@ export function Outer({
 
   const open = React.useCallback<DialogControlProps['open']>(
     ({index} = {}) => {
+      openDialogs.current.add(control.id)
       // can be set to any index of `snapPoints`, but `0` is the first i.e. "open"
       setOpenIndex(index || 0)
     },
-    [setOpenIndex],
+    [setOpenIndex, openDialogs, control.id],
   )
 
   const close = React.useCallback<DialogControlProps['close']>(cb => {
@@ -85,11 +88,12 @@ export function Outer({
           closeCallback.current = undefined
         }
 
+        openDialogs.current.delete(control.id)
         onClose?.()
         setOpenIndex(-1)
       }
     },
-    [onClose, setOpenIndex],
+    [onClose, setOpenIndex, openDialogs, control.id],
   )
 
   const context = React.useMemo(() => ({close}), [close])
diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx
index ff05fed91..32163e735 100644
--- a/src/components/Dialog/index.web.tsx
+++ b/src/components/Dialog/index.web.tsx
@@ -12,6 +12,7 @@ import {DialogOuterProps, DialogInnerProps} from '#/components/Dialog/types'
 import {Context} from '#/components/Dialog/context'
 import {Button, ButtonIcon} from '#/components/Button'
 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
+import {useDialogStateContext} from '#/state/dialogs'
 
 export {useDialogControl, useDialogContext} from '#/components/Dialog/context'
 export * from '#/components/Dialog/types'
@@ -29,18 +30,21 @@ export function Outer({
   const {gtMobile} = useBreakpoints()
   const [isOpen, setIsOpen] = React.useState(false)
   const [isVisible, setIsVisible] = React.useState(true)
+  const {openDialogs} = useDialogStateContext()
 
   const open = React.useCallback(() => {
     setIsOpen(true)
-  }, [setIsOpen])
+    openDialogs.current.add(control.id)
+  }, [setIsOpen, openDialogs, control.id])
 
   const close = React.useCallback(async () => {
     setIsVisible(false)
     await new Promise(resolve => setTimeout(resolve, 150))
     setIsOpen(false)
     setIsVisible(true)
+    openDialogs.current.delete(control.id)
     onClose?.()
-  }, [onClose, setIsOpen])
+  }, [onClose, setIsOpen, openDialogs, control.id])
 
   useImperativeHandle(
     control.ref,
diff --git a/src/components/Dialog/types.ts b/src/components/Dialog/types.ts
index 161c03734..78dfedf5a 100644
--- a/src/components/Dialog/types.ts
+++ b/src/components/Dialog/types.ts
@@ -6,11 +6,24 @@ import {ViewStyleProp} from '#/alf'
 
 type A11yProps = Required<AccessibilityProps>
 
-export type DialogControlProps = {
+/**
+ * Mutated by useImperativeHandle to provide a public API for controlling the
+ * dialog. The methods here will actually become the handlers defined within
+ * the `Dialog.Outer` component.
+ */
+export type DialogControlRefProps = {
   open: (options?: DialogControlOpenOptions) => void
   close: (callback?: () => void) => void
 }
 
+/**
+ * The return type of the useDialogControl hook.
+ */
+export type DialogControlProps = DialogControlRefProps & {
+  id: string
+  ref: React.RefObject<DialogControlRefProps>
+}
+
 export type DialogContextProps = {
   close: DialogControlProps['close']
 }
@@ -26,9 +39,7 @@ export type DialogControlOpenOptions = {
 }
 
 export type DialogOuterProps = {
-  control: {
-    ref: React.RefObject<DialogControlProps>
-  } & DialogControlProps
+  control: DialogControlProps
   onClose?: () => void
   nativeOptions?: {
     sheet?: Omit<BottomSheetProps, 'children'>
diff --git a/src/state/dialogs/index.tsx b/src/state/dialogs/index.tsx
index ae762bd97..9fc70c178 100644
--- a/src/state/dialogs/index.tsx
+++ b/src/state/dialogs/index.tsx
@@ -1,21 +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() {
@@ -28,13 +39,18 @@ 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}>
diff --git a/src/state/util.ts b/src/state/util.ts
index 7b49b5b46..f65d14a84 100644
--- a/src/state/util.ts
+++ b/src/state/util.ts
@@ -3,7 +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'
+import {useDialogStateControlContext} from '#/state/dialogs'
 
 /**
  * returns true if something was closed
@@ -13,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()) {
@@ -24,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])
 }
 
 /**
-- 
cgit 1.4.1

