Thread muting [APP-29] (#500)

* Implement thread muting

* Apply filtering on background fetched notifs

* Implement thread-muting tests
This commit is contained in:
Paul Frazee 2023-04-20 17:16:56 -05:00 committed by GitHub
parent 3e78c71018
commit 22884b53ad
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 470 additions and 108 deletions

View file

@ -48,11 +48,13 @@ interface PostCtrlsOpts {
likeCount?: number
isReposted: boolean
isLiked: boolean
isThreadMuted: boolean
onPressReply: () => void
onPressToggleRepost: () => Promise<void>
onPressToggleLike: () => Promise<void>
onCopyPostText: () => void
onOpenTranslate: () => void
onToggleThreadMute: () => void
onDeletePost: () => void
}
@ -255,8 +257,10 @@ export function PostCtrls(opts: PostCtrlsOpts) {
itemHref={opts.itemHref}
itemTitle={opts.itemTitle}
isAuthor={opts.isAuthor}
isThreadMuted={opts.isThreadMuted}
onCopyPostText={opts.onCopyPostText}
onOpenTranslate={opts.onOpenTranslate}
onToggleThreadMute={opts.onToggleThreadMute}
onDeletePost={opts.onDeletePost}>
<FontAwesomeIcon
icon="ellipsis-h"

View file

@ -94,7 +94,10 @@ export function Selector({
{items.map((item, i) => {
const selected = i === selectedIndex
return (
<Pressable key={item} onPress={() => onPressItem(i)}>
<Pressable
testID={`selector-${i}`}
key={item}
onPress={() => onPressItem(i)}>
<View style={styles.item} ref={itemRefs[i]}>
<Text
style={

View file

@ -22,16 +22,22 @@ import {useTheme} from 'lib/ThemeContext'
import {isAndroid, isIOS} from 'platform/detection'
import Clipboard from '@react-native-clipboard/clipboard'
import * as Toast from '../../util/Toast'
import {isWeb} from 'platform/detection'
const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10}
const ESTIMATED_MENU_ITEM_HEIGHT = 52
const ESTIMATED_BTN_HEIGHT = 50
const ESTIMATED_SEP_HEIGHT = 16
export interface DropdownItem {
export interface DropdownItemButton {
testID?: string
icon?: IconProp
label: string
onPress: () => void
}
export interface DropdownItemSeparator {
sep: true
}
export type DropdownItem = DropdownItemButton | DropdownItemSeparator
type MaybeDropdownItem = DropdownItem | false | undefined
export type DropdownButtonType = ButtonType | 'bare'
@ -59,10 +65,12 @@ export function DropdownButton({
rightOffset?: number
bottomOffset?: number
}) {
const ref = useRef<TouchableOpacity>(null)
const ref1 = useRef<TouchableOpacity>(null)
const ref2 = useRef<View>(null)
const onPress = () => {
ref.current?.measure(
const ref = ref1.current || ref2.current
ref?.measure(
(
_x: number,
_y: number,
@ -75,7 +83,14 @@ export function DropdownButton({
menuWidth = 200
}
const winHeight = Dimensions.get('window').height
const estimatedMenuHeight = items.length * ESTIMATED_MENU_ITEM_HEIGHT
let estimatedMenuHeight = 0
for (const item of items) {
if (item && isSep(item)) {
estimatedMenuHeight += ESTIMATED_SEP_HEIGHT
} else if (item && isBtn(item)) {
estimatedMenuHeight += ESTIMATED_BTN_HEIGHT
}
}
const newX = openToRight
? pageX + width + rightOffset
: pageX + width - menuWidth
@ -100,13 +115,13 @@ export function DropdownButton({
style={style}
onPress={onPress}
hitSlop={HITSLOP}
ref={ref}>
ref={ref1}>
{children}
</TouchableOpacity>
)
}
return (
<View ref={ref}>
<View ref={ref2}>
<Button testID={testID} onPress={onPress} style={style} label={label}>
{children}
</Button>
@ -122,8 +137,10 @@ export function PostDropdownBtn({
itemCid,
itemHref,
isAuthor,
isThreadMuted,
onCopyPostText,
onOpenTranslate,
onToggleThreadMute,
onDeletePost,
}: {
testID?: string
@ -134,8 +151,10 @@ export function PostDropdownBtn({
itemHref: string
itemTitle: string
isAuthor: boolean
isThreadMuted: boolean
onCopyPostText: () => void
onOpenTranslate: () => void
onToggleThreadMute: () => void
onDeletePost: () => void
}) {
const store = useStores()
@ -174,6 +193,16 @@ export function PostDropdownBtn({
}
},
},
{sep: true},
{
testID: 'postDropdownMuteThreadBtn',
icon: 'comment-slash',
label: isThreadMuted ? 'Unmute thread' : 'Mute thread',
onPress() {
onToggleThreadMute()
},
},
{sep: true},
{
testID: 'postDropdownReportBtn',
icon: 'circle-exclamation',
@ -186,21 +215,19 @@ export function PostDropdownBtn({
})
},
},
isAuthor
? {
testID: 'postDropdownDeleteBtn',
icon: ['far', 'trash-can'],
label: 'Delete post',
onPress() {
store.shell.openModal({
name: 'confirm',
title: 'Delete this post?',
message: 'Are you sure? This can not be undone.',
onPressConfirm: onDeletePost,
})
},
}
: undefined,
isAuthor && {
testID: 'postDropdownDeleteBtn',
icon: ['far', 'trash-can'],
label: 'Delete post',
onPress() {
store.shell.openModal({
name: 'confirm',
title: 'Delete this post?',
message: 'Are you sure? This can not be undone.',
onPressConfirm: onDeletePost,
})
},
},
].filter(Boolean) as DropdownItem[]
return (
@ -208,7 +235,7 @@ export function PostDropdownBtn({
testID={testID}
style={style}
items={dropdownItems}
menuWidth={200}>
menuWidth={isWeb ? 220 : 200}>
{children}
</DropdownButton>
)
@ -222,7 +249,10 @@ function createDropdownMenu(
): RootSiblings {
const onPressItem = (index: number) => {
sibling.destroy()
items[index].onPress()
const item = items[index]
if (isBtn(item)) {
item.onPress()
}
}
const onOuterPress = () => sibling.destroy()
const sibling = new RootSiblings(
@ -240,6 +270,74 @@ function createDropdownMenu(
return sibling
}
type DropDownItemProps = {
onOuterPress: () => void
x: number
y: number
width: number
items: DropdownItem[]
onPressItem: (index: number) => void
}
const DropdownItems = ({
onOuterPress,
x,
y,
width,
items,
onPressItem,
}: DropDownItemProps) => {
const pal = usePalette('default')
const theme = useTheme()
const dropDownBackgroundColor =
theme.colorScheme === 'dark' ? pal.btn : pal.view
return (
<>
<TouchableWithoutFeedback onPress={onOuterPress}>
<View style={[styles.bg]} />
</TouchableWithoutFeedback>
<View
style={[
styles.menu,
{left: x, top: y, width},
dropDownBackgroundColor,
]}>
{items.map((item, index) => {
if (isBtn(item)) {
return (
<TouchableOpacity
testID={item.testID}
key={index}
style={[styles.menuItem]}
onPress={() => onPressItem(index)}>
{item.icon && (
<FontAwesomeIcon
style={styles.icon}
icon={item.icon}
color={pal.text.color as string}
/>
)}
<Text style={[styles.label, pal.text]}>{item.label}</Text>
</TouchableOpacity>
)
} else if (isSep(item)) {
return <View key={index} style={[styles.separator, pal.border]} />
}
return null
})}
</View>
</>
)
}
function isSep(item: DropdownItem): item is DropdownItemSeparator {
return 'sep' in item && item.sep
}
function isBtn(item: DropdownItem): item is DropdownItemButton {
return !isSep(item)
}
const styles = StyleSheet.create({
bg: {
position: 'absolute',
@ -277,57 +375,8 @@ const styles = StyleSheet.create({
label: {
fontSize: 18,
},
separator: {
borderTopWidth: 1,
marginVertical: 8,
},
})
type DropDownItemProps = {
onOuterPress: () => void
x: number
y: number
width: number
items: DropdownItem[]
onPressItem: (index: number) => void
}
const DropdownItems = ({
onOuterPress,
x,
y,
width,
items,
onPressItem,
}: DropDownItemProps) => {
const pal = usePalette('default')
const theme = useTheme()
const dropDownBackgroundColor =
theme.colorScheme === 'dark' ? pal.btn : pal.view
return (
<>
<TouchableWithoutFeedback onPress={onOuterPress}>
<View style={[styles.bg]} />
</TouchableWithoutFeedback>
<View
style={[
styles.menu,
{left: x, top: y, width},
dropDownBackgroundColor,
]}>
{items.map((item, index) => (
<TouchableOpacity
testID={item.testID}
key={index}
style={[styles.menuItem]}
onPress={() => onPressItem(index)}>
{item.icon && (
<FontAwesomeIcon
style={styles.icon}
icon={item.icon}
color={pal.text.color as string}
/>
)}
<Text style={[styles.label, pal.text]}>{item.label}</Text>
</TouchableOpacity>
))}
</View>
</>
)
}