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/Navigation.tsx | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

(limited to 'src/Navigation.tsx')

diff --git a/src/Navigation.tsx b/src/Navigation.tsx
index 6ca4212e2..dfbe816f4 100644
--- a/src/Navigation.tsx
+++ b/src/Navigation.tsx
@@ -497,7 +497,8 @@ const LINKING = {
         },
       ])
     } else {
-      return buildStateObject('Flat', name, params)
+      const res = buildStateObject('Flat', name, params)
+      return res
     }
   },
 }
-- 
cgit 1.4.1


From 2a04546c7305b9bf03ea6cd26ce728ed773e2673 Mon Sep 17 00:00:00 2001
From: Hailey <me@haileyok.com>
Date: Tue, 27 Feb 2024 10:35:38 -0800
Subject: Intent handler (#2992)

* Handle URL params

* Add resources

* Add other params

* refactor for scope

* modify the pr to support intents rather than utm

remove linebreak

remove linebreak

handle web

adjust path check to work on web

add a short delay for opening the composer

setup compose intent, move to `intents` directory

fix intent logic

ignore incoming intents in the navigation router

* refactor

---------

Co-authored-by: Eric Bailey <git@esb.lol>
---
 app.config.js                     |  4 +++
 package.json                      |  1 +
 src/App.native.tsx                |  2 ++
 src/App.web.tsx                   |  2 ++
 src/Navigation.tsx                |  8 ++++-
 src/lib/hooks/useIntentHandler.ts | 64 +++++++++++++++++++++++++++++++++++++++
 yarn.lock                         |  8 +++++
 7 files changed, 88 insertions(+), 1 deletion(-)
 create mode 100644 src/lib/hooks/useIntentHandler.ts

(limited to 'src/Navigation.tsx')

diff --git a/app.config.js b/app.config.js
index e710420b0..5bbe864a3 100644
--- a/app.config.js
+++ b/app.config.js
@@ -89,6 +89,10 @@ module.exports = function (config) {
                 scheme: 'https',
                 host: 'bsky.app',
               },
+              {
+                scheme: 'http',
+                host: 'localhost:19006',
+              },
             ],
             category: ['BROWSABLE', 'DEFAULT'],
           },
diff --git a/package.json b/package.json
index 3d1516034..4051849bf 100644
--- a/package.json
+++ b/package.json
@@ -109,6 +109,7 @@
     "expo-image": "~1.10.3",
     "expo-image-manipulator": "^11.8.0",
     "expo-image-picker": "~14.7.1",
+    "expo-linking": "^6.2.2",
     "expo-localization": "~14.8.2",
     "expo-media-library": "~15.9.1",
     "expo-notifications": "~0.27.3",
diff --git a/src/App.native.tsx b/src/App.native.tsx
index 1284154f3..f08a6235b 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -45,6 +45,7 @@ import {Splash} from '#/Splash'
 import {Provider as PortalProvider} from '#/components/Portal'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
+import {useIntentHandler} from 'lib/hooks/useIntentHandler'
 
 SplashScreen.preventAutoHideAsync()
 
@@ -53,6 +54,7 @@ function InnerApp() {
   const {resumeSession} = useSessionApi()
   const theme = useColorModeTheme()
   const {_} = useLingui()
+  useIntentHandler()
 
   // init
   useEffect(() => {
diff --git a/src/App.web.tsx b/src/App.web.tsx
index f10bb194d..6ac32a011 100644
--- a/src/App.web.tsx
+++ b/src/App.web.tsx
@@ -32,11 +32,13 @@ import {
 import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
 import * as persisted from '#/state/persisted'
 import {Provider as PortalProvider} from '#/components/Portal'
+import {useIntentHandler} from 'lib/hooks/useIntentHandler'
 
 function InnerApp() {
   const {isInitialLoad, currentAccount} = useSession()
   const {resumeSession} = useSessionApi()
   const theme = useColorModeTheme()
+  useIntentHandler()
 
   // init
   useEffect(() => {
diff --git a/src/Navigation.tsx b/src/Navigation.tsx
index dfbe816f4..0aeeeb6ad 100644
--- a/src/Navigation.tsx
+++ b/src/Navigation.tsx
@@ -460,7 +460,8 @@ const FlatNavigator = () => {
  */
 
 const LINKING = {
-  prefixes: ['bsky://', 'https://bsky.app'],
+  // TODO figure out what we are going to use
+  prefixes: ['bsky://', 'bluesky://', 'https://bsky.app'],
 
   getPathFromState(state: State) {
     // find the current node in the navigation tree
@@ -478,6 +479,11 @@ const LINKING = {
   },
 
   getStateFromPath(path: string) {
+    // Any time we receive a url that starts with `intent/` we want to ignore it here. It will be handled in the
+    // intent handler hook. We should check for the trailing slash, because if there isn't one then it isn't a valid
+    // intent
+    if (path.includes('intent/')) return
+
     const [name, params] = router.matchPath(path)
     if (isNative) {
       if (name === 'Search') {
diff --git a/src/lib/hooks/useIntentHandler.ts b/src/lib/hooks/useIntentHandler.ts
new file mode 100644
index 000000000..249e6898e
--- /dev/null
+++ b/src/lib/hooks/useIntentHandler.ts
@@ -0,0 +1,64 @@
+import React from 'react'
+import * as Linking from 'expo-linking'
+import {isNative} from 'platform/detection'
+import {useComposerControls} from 'state/shell'
+import {useSession} from 'state/session'
+
+type IntentType = 'compose'
+
+export function useIntentHandler() {
+  const incomingUrl = Linking.useURL()
+  const composeIntent = useComposeIntent()
+
+  React.useEffect(() => {
+    const handleIncomingURL = (url: string) => {
+      const urlp = new URL(url)
+      const [_, intentTypeNative, intentTypeWeb] = urlp.pathname.split('/')
+
+      // On native, our links look like bluesky://intent/SomeIntent, so we have to check the hostname for the
+      // intent check. On web, we have to check the first part of the path since we have an actual hostname
+      const intentType = isNative ? intentTypeNative : intentTypeWeb
+      const isIntent = isNative
+        ? urlp.hostname === 'intent'
+        : intentTypeNative === 'intent'
+      const params = urlp.searchParams
+
+      if (!isIntent) return
+
+      switch (intentType as IntentType) {
+        case 'compose': {
+          composeIntent({
+            text: params.get('text'),
+            imageUris: params.get('imageUris'),
+          })
+        }
+      }
+    }
+
+    if (incomingUrl) handleIncomingURL(incomingUrl)
+  }, [incomingUrl, composeIntent])
+}
+
+function useComposeIntent() {
+  const {openComposer} = useComposerControls()
+  const {hasSession} = useSession()
+
+  return React.useCallback(
+    ({
+      // eslint-disable-next-line @typescript-eslint/no-unused-vars
+      text,
+      // eslint-disable-next-line @typescript-eslint/no-unused-vars
+      imageUris,
+    }: {
+      text: string | null
+      imageUris: string | null // unused for right now, will be used later with intents
+    }) => {
+      if (!hasSession) return
+
+      setTimeout(() => {
+        openComposer({}) // will pass in values to the composer here in the share extension
+      }, 500)
+    },
+    [openComposer, hasSession],
+  )
+}
diff --git a/yarn.lock b/yarn.lock
index 3cec585ba..a62ff2f83 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -11739,6 +11739,14 @@ expo-keep-awake@~12.8.1:
   resolved "https://registry.yarnpkg.com/expo-keep-awake/-/expo-keep-awake-12.8.1.tgz#3c8df9d86c265741b5e7bdd36965aa0c6fc17df0"
   integrity sha512-P/VZFV02Rzgj13skMwH+ceGOGZSEdaUu5n7pCS3wThh2LppZjPJ7sBxUwyzeLa3DXEVUtwLZi+BiQ91wPwy9Gg==
 
+expo-linking@^6.2.2:
+  version "6.2.2"
+  resolved "https://registry.yarnpkg.com/expo-linking/-/expo-linking-6.2.2.tgz#b7e148068ae49fd9ad814428c16fdf7a236e8aca"
+  integrity sha512-FEe6lP4f7xFT/vjoHRG+tt6EPVtkEGaWNK1smpaUevmNdyCJKqW0PDB8o8sfG6y7fly8ULe8qg3HhKh5J7aqUQ==
+  dependencies:
+    expo-constants "~15.4.3"
+    invariant "^2.2.4"
+
 expo-localization@~14.8.2:
   version "14.8.2"
   resolved "https://registry.yarnpkg.com/expo-localization/-/expo-localization-14.8.2.tgz#e0bbed2293265834d21a1c58d3a5f8d265bd04ae"
-- 
cgit 1.4.1

