From d76f9abdd718e24848a9b8f67486129aee421427 Mon Sep 17 00:00:00 2001
From: Eric Bailey <git@esb.lol>
Date: Fri, 13 Sep 2024 16:48:28 -0500
Subject: "N" keyboard shortcut to open a new post modal (#5197)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* feat: Add hook on web app to open composer with 'N' keyboard shortcut

* Extract, don't fire open composer if already open

* Ignore interactive elements

---------

Co-authored-by: João Gabriel <joaog@nocorp.io>
Co-authored-by: Hailey <me@haileyok.com>
---
 src/state/shell/composer/index.tsx | 107 +++++++++++++++++++++++++++++++++++++
 1 file changed, 107 insertions(+)
 create mode 100644 src/state/shell/composer/index.tsx

(limited to 'src/state/shell/composer/index.tsx')

diff --git a/src/state/shell/composer/index.tsx b/src/state/shell/composer/index.tsx
new file mode 100644
index 000000000..612388ff8
--- /dev/null
+++ b/src/state/shell/composer/index.tsx
@@ -0,0 +1,107 @@
+import React from 'react'
+import {
+  AppBskyActorDefs,
+  AppBskyEmbedRecord,
+  AppBskyRichtextFacet,
+  ModerationDecision,
+} from '@atproto/api'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
+import * as Toast from '#/view/com/util/Toast'
+
+export interface ComposerOptsPostRef {
+  uri: string
+  cid: string
+  text: string
+  author: AppBskyActorDefs.ProfileViewBasic
+  embed?: AppBskyEmbedRecord.ViewRecord['embed']
+  moderation?: ModerationDecision
+}
+export interface ComposerOptsQuote {
+  uri: string
+  cid: string
+  text: string
+  facets?: AppBskyRichtextFacet.Main[]
+  indexedAt: string
+  author: AppBskyActorDefs.ProfileViewBasic
+  embeds?: AppBskyEmbedRecord.ViewRecord['embeds']
+}
+export interface ComposerOpts {
+  replyTo?: ComposerOptsPostRef
+  onPost?: (postUri: string | undefined) => void
+  quote?: ComposerOptsQuote
+  quoteCount?: number
+  mention?: string // handle of user to mention
+  openEmojiPicker?: (pos: DOMRect | undefined) => void
+  text?: string
+  imageUris?: {uri: string; width: number; height: number}[]
+}
+
+type StateContext = ComposerOpts | undefined
+type ControlsContext = {
+  openComposer: (opts: ComposerOpts) => void
+  closeComposer: () => boolean
+}
+
+const stateContext = React.createContext<StateContext>(undefined)
+const controlsContext = React.createContext<ControlsContext>({
+  openComposer(_opts: ComposerOpts) {},
+  closeComposer() {
+    return false
+  },
+})
+
+export function Provider({children}: React.PropsWithChildren<{}>) {
+  const {_} = useLingui()
+  const [state, setState] = React.useState<StateContext>()
+
+  const openComposer = useNonReactiveCallback((opts: ComposerOpts) => {
+    const author = opts.replyTo?.author || opts.quote?.author
+    const isBlocked = Boolean(
+      author &&
+        (author.viewer?.blocking ||
+          author.viewer?.blockedBy ||
+          author.viewer?.blockingByList),
+    )
+    if (isBlocked) {
+      Toast.show(
+        _(msg`Cannot interact with a blocked user`),
+        'exclamation-circle',
+      )
+    } else {
+      setState(opts)
+    }
+  })
+
+  const closeComposer = useNonReactiveCallback(() => {
+    let wasOpen = !!state
+    setState(undefined)
+    return wasOpen
+  })
+
+  const api = React.useMemo(
+    () => ({
+      openComposer,
+      closeComposer,
+    }),
+    [openComposer, closeComposer],
+  )
+
+  return (
+    <stateContext.Provider value={state}>
+      <controlsContext.Provider value={api}>
+        {children}
+      </controlsContext.Provider>
+    </stateContext.Provider>
+  )
+}
+
+export function useComposerState() {
+  return React.useContext(stateContext)
+}
+
+export function useComposerControls() {
+  return React.useContext(controlsContext)
+}
-- 
cgit 1.4.1

