about summary refs log tree commit diff
path: root/src/lib/hooks/useIntentHandler.ts
blob: 460df3753dbe597831058c6b870f7f533b2f334a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import React from 'react'
import * as Linking from 'expo-linking'

import {logEvent} from 'lib/statsig/statsig'
import {isNative} from 'platform/detection'
import {useSession} from 'state/session'
import {useComposerControls} from 'state/shell'
import {useCloseAllActiveElements} from 'state/util'
import {Referrer} from '../../../modules/expo-bluesky-swiss-army'

type IntentType = 'compose'

const VALID_IMAGE_REGEX = /^[\w.:\-_/]+\|\d+(\.\d+)?\|\d+(\.\d+)?$/

export function useIntentHandler() {
  const incomingUrl = Linking.useURL()
  const composeIntent = useComposeIntent()

  React.useEffect(() => {
    const handleIncomingURL = (url: string) => {
      const referrerInfo = Referrer.getReferrerInfo()
      if (referrerInfo && referrerInfo.hostname !== 'bsky.app') {
        logEvent('deepLink:referrerReceived', {
          to: url,
          referrer: referrerInfo?.referrer,
          hostname: referrerInfo?.hostname,
        })
      }

      // We want to be able to support bluesky:// deeplinks. It's unnatural for someone to use a deeplink with three
      // slashes, like bluesky:///intent/follow. However, supporting just two slashes causes us to have to take care
      // of two cases when parsing the url. If we ensure there is a third slash, we can always ensure the first
      // path parameter is in pathname rather than in hostname.
      if (url.startsWith('bluesky://') && !url.startsWith('bluesky:///')) {
        url = url.replace('bluesky://', 'bluesky:///')
      }

      const urlp = new URL(url)
      const [_, intent, intentType] = 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 isIntent = intent === 'intent'
      const params = urlp.searchParams

      if (!isIntent) return

      switch (intentType as IntentType) {
        case 'compose': {
          composeIntent({
            text: params.get('text'),
            imageUrisStr: params.get('imageUris'),
          })
        }
      }
    }

    if (incomingUrl) handleIncomingURL(incomingUrl)
  }, [incomingUrl, composeIntent])
}

function useComposeIntent() {
  const closeAllActiveElements = useCloseAllActiveElements()
  const {openComposer} = useComposerControls()
  const {hasSession} = useSession()

  return React.useCallback(
    ({
      text,
      imageUrisStr,
    }: {
      text: string | null
      imageUrisStr: string | null // unused for right now, will be used later with intents
    }) => {
      if (!hasSession) return

      closeAllActiveElements()

      const imageUris = imageUrisStr
        ?.split(',')
        .filter(part => {
          // For some security, we're going to filter out any image uri that is external. We don't want someone to
          // be able to provide some link like "bluesky://intent/compose?imageUris=https://IHaveYourIpNow.com/image.jpeg
          // and we load that image
          if (part.includes('https://') || part.includes('http://')) {
            return false
          }
          // We also should just filter out cases that don't have all the info we need
          return VALID_IMAGE_REGEX.test(part)
        })
        .map(part => {
          const [uri, width, height] = part.split('|')
          return {uri, width: Number(width), height: Number(height)}
        })

      setTimeout(() => {
        openComposer({
          text: text ?? undefined,
          imageUris: isNative ? imageUris : undefined,
        })
      }, 500)
    },
    [hasSession, closeAllActiveElements, openComposer],
  )
}