my fork of the bluesky client
1import React from 'react'
2import {msg} from '@lingui/macro'
3import {useLingui} from '@lingui/react'
4import {useNavigation} from '@react-navigation/native'
5
6import {NavigationProp} from '#/lib/routes/types'
7import {isInvalidHandle} from '#/lib/strings/handles'
8import {enforceLen} from '#/lib/strings/helpers'
9import {
10 usePreferencesQuery,
11 useRemoveMutedWordsMutation,
12 useUpsertMutedWordsMutation,
13} from '#/state/queries/preferences'
14import {EventStopper} from '#/view/com/util/EventStopper'
15import {NativeDropdown} from '#/view/com/util/forms/NativeDropdown'
16import {web} from '#/alf'
17import * as Dialog from '#/components/Dialog'
18
19export function useTagMenuControl(): Dialog.DialogControlProps {
20 return {
21 id: '',
22 // @ts-ignore
23 ref: null,
24 open: () => {
25 throw new Error(`TagMenu controls are only available on native platforms`)
26 },
27 close: () => {
28 throw new Error(`TagMenu controls are only available on native platforms`)
29 },
30 }
31}
32
33export function TagMenu({
34 children,
35 tag,
36 authorHandle,
37}: React.PropsWithChildren<{
38 /**
39 * This should be the sanitized tag value from the facet itself, not the
40 * "display" value with a leading `#`.
41 */
42 tag: string
43 authorHandle?: string
44}>) {
45 const {_} = useLingui()
46 const navigation = useNavigation<NavigationProp>()
47 const {data: preferences} = usePreferencesQuery()
48 const {mutateAsync: upsertMutedWord, variables: optimisticUpsert} =
49 useUpsertMutedWordsMutation()
50 const {mutateAsync: removeMutedWords, variables: optimisticRemove} =
51 useRemoveMutedWordsMutation()
52 const isMuted = Boolean(
53 (preferences?.moderationPrefs.mutedWords?.find(
54 m => m.value === tag && m.targets.includes('tag'),
55 ) ??
56 optimisticUpsert?.find(
57 m => m.value === tag && m.targets.includes('tag'),
58 )) &&
59 !optimisticRemove?.find(m => m?.value === tag),
60 )
61 const truncatedTag = '#' + enforceLen(tag, 15, true, 'middle')
62
63 /*
64 * Mute word records that exactly match the tag in question.
65 */
66 const removeableMuteWords = React.useMemo(() => {
67 return (
68 preferences?.moderationPrefs.mutedWords?.filter(word => {
69 return word.value === tag
70 }) || []
71 )
72 }, [tag, preferences?.moderationPrefs?.mutedWords])
73
74 const dropdownItems = React.useMemo(() => {
75 return [
76 {
77 label: _(msg`See ${truncatedTag} posts`),
78 onPress() {
79 navigation.push('Hashtag', {
80 tag: encodeURIComponent(tag),
81 })
82 },
83 testID: 'tagMenuSearch',
84 icon: {
85 ios: {
86 name: 'magnifyingglass',
87 },
88 android: '',
89 web: 'magnifying-glass',
90 },
91 },
92 authorHandle &&
93 !isInvalidHandle(authorHandle) && {
94 label: _(msg`See ${truncatedTag} posts by user`),
95 onPress() {
96 navigation.push('Hashtag', {
97 tag: encodeURIComponent(tag),
98 author: authorHandle,
99 })
100 },
101 testID: 'tagMenuSearchByUser',
102 icon: {
103 ios: {
104 name: 'magnifyingglass',
105 },
106 android: '',
107 web: ['far', 'user'],
108 },
109 },
110 preferences && {
111 label: 'separator',
112 },
113 preferences && {
114 label: isMuted
115 ? _(msg`Unmute ${truncatedTag}`)
116 : _(msg`Mute ${truncatedTag}`),
117 onPress() {
118 if (isMuted) {
119 removeMutedWords(removeableMuteWords)
120 } else {
121 upsertMutedWord([
122 {value: tag, targets: ['tag'], actorTarget: 'all'},
123 ])
124 }
125 },
126 testID: 'tagMenuMute',
127 icon: {
128 ios: {
129 name: 'speaker.slash',
130 },
131 android: 'ic_menu_sort_alphabetically',
132 web: isMuted ? 'eye' : ['far', 'eye-slash'],
133 },
134 },
135 ].filter(Boolean)
136 }, [
137 _,
138 authorHandle,
139 isMuted,
140 navigation,
141 preferences,
142 tag,
143 truncatedTag,
144 upsertMutedWord,
145 removeMutedWords,
146 removeableMuteWords,
147 ])
148
149 return (
150 <EventStopper>
151 <NativeDropdown
152 accessibilityLabel={_(msg`Click here to open tag menu for ${tag}`)}
153 accessibilityHint=""
154 // @ts-ignore
155 items={dropdownItems}
156 triggerStyle={web({
157 textAlign: 'left',
158 })}>
159 {children}
160 </NativeDropdown>
161 </EventStopper>
162 )
163}