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/components/TagMenu/index.web.tsx | 127 +++++++++++++++++++++++++++++++++++
 1 file changed, 127 insertions(+)
 create mode 100644 src/components/TagMenu/index.web.tsx

(limited to 'src/components/TagMenu/index.web.tsx')

diff --git a/src/components/TagMenu/index.web.tsx b/src/components/TagMenu/index.web.tsx
new file mode 100644
index 000000000..930e47a1a
--- /dev/null
+++ b/src/components/TagMenu/index.web.tsx
@@ -0,0 +1,127 @@
+import React from 'react'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useNavigation} from '@react-navigation/native'
+
+import {isInvalidHandle} from '#/lib/strings/handles'
+import {EventStopper} from '#/view/com/util/EventStopper'
+import {NativeDropdown} from '#/view/com/util/forms/NativeDropdown'
+import {NavigationProp} from '#/lib/routes/types'
+import {
+  usePreferencesQuery,
+  useUpsertMutedWordsMutation,
+  useRemoveMutedWordMutation,
+} from '#/state/queries/preferences'
+
+export function useTagMenuControl() {}
+
+export function TagMenu({
+  children,
+  tag,
+  authorHandle,
+}: React.PropsWithChildren<{
+  tag: string
+  authorHandle?: string
+}>) {
+  const sanitizedTag = tag.replace(/^#/, '')
+  const {_} = useLingui()
+  const navigation = useNavigation<NavigationProp>()
+  const {data: preferences} = usePreferencesQuery()
+  const {mutateAsync: upsertMutedWord, variables: optimisticUpsert} =
+    useUpsertMutedWordsMutation()
+  const {mutateAsync: removeMutedWord, variables: optimisticRemove} =
+    useRemoveMutedWordMutation()
+  const isMuted = Boolean(
+    (preferences?.mutedWords?.find(
+      m => m.value === sanitizedTag && m.targets.includes('tag'),
+    ) ??
+      optimisticUpsert?.find(
+        m => m.value === sanitizedTag && m.targets.includes('tag'),
+      )) &&
+      !(optimisticRemove?.value === sanitizedTag),
+  )
+
+  const dropdownItems = React.useMemo(() => {
+    return [
+      {
+        label: _(msg`See ${tag} posts`),
+        onPress() {
+          navigation.navigate('Search', {
+            q: tag,
+          })
+        },
+        testID: 'tagMenuSearch',
+        icon: {
+          ios: {
+            name: 'magnifyingglass',
+          },
+          android: '',
+          web: 'magnifying-glass',
+        },
+      },
+      authorHandle &&
+        !isInvalidHandle(authorHandle) && {
+          label: _(msg`See ${tag} posts by this user`),
+          onPress() {
+            navigation.navigate({
+              name: 'Search',
+              params: {
+                q: tag + (authorHandle ? ` from:${authorHandle}` : ''),
+              },
+            })
+          },
+          testID: 'tagMenuSeachByUser',
+          icon: {
+            ios: {
+              name: 'magnifyingglass',
+            },
+            android: '',
+            web: ['far', 'user'],
+          },
+        },
+      preferences && {
+        label: 'separator',
+      },
+      preferences && {
+        label: isMuted ? _(msg`Unmute ${tag}`) : _(msg`Mute ${tag}`),
+        onPress() {
+          if (isMuted) {
+            removeMutedWord({value: sanitizedTag, targets: ['tag']})
+          } else {
+            upsertMutedWord([{value: sanitizedTag, targets: ['tag']}])
+          }
+        },
+        testID: 'tagMenuMute',
+        icon: {
+          ios: {
+            name: 'speaker.slash',
+          },
+          android: 'ic_menu_sort_alphabetically',
+          web: isMuted ? 'eye' : ['far', 'eye-slash'],
+        },
+      },
+    ].filter(Boolean)
+  }, [
+    _,
+    authorHandle,
+    isMuted,
+    navigation,
+    preferences,
+    tag,
+    sanitizedTag,
+    upsertMutedWord,
+    removeMutedWord,
+  ])
+
+  return (
+    <EventStopper>
+      <NativeDropdown
+        accessibilityLabel={_(msg`Click here to open tag menu for ${tag}`)}
+        accessibilityHint=""
+        // @ts-ignore
+        items={dropdownItems}>
+        {children}
+      </NativeDropdown>
+    </EventStopper>
+  )
+}
-- 
cgit 1.4.1

