[APP-635] Mutelists (#601)

* Add lists and profilelist screens

* Implement lists screen and lists-list in profiles

* Add empty states to the lists screen

* Switch (mostly) from blocklists to mutelists

* Rework: create a new moderation screen and move everything related under it

* Fix moderation screen on desktop web

* Tune the empty state code

* Change content moderation modal to content filtering

* Add CreateMuteList modal

* Implement mutelist creation

* Add lists listings

* Add the ability to create new mutelists

* Add 'add to list' tool

* Satisfy the hashtag hyphen haters

* Add update/delete/subscribe/unsubscribe to lists

* Show which list caused a mute

* Add list un/subscribe

* Add the mute override when viewing a profile's posts

* Update to latest backend

* Add simulation tests and tune some behaviors

* Fix lint

* Bump deps

* Fix list refresh after creation

* Mute list subscriptions -> Mute lists
This commit is contained in:
Paul Frazee 2023-05-11 16:08:21 -05:00 committed by GitHub
parent 34d8fa5991
commit ebcd633386
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 2984 additions and 151 deletions

View file

@ -32,7 +32,7 @@ export const ProfileCard = observer(
noBorder?: boolean
followers?: AppBskyActorDefs.ProfileView[] | undefined
overrideModeration?: boolean
renderButton?: () => JSX.Element
renderButton?: (profile: AppBskyActorDefs.ProfileViewBasic) => JSX.Element
}) => {
const store = useStores()
const pal = usePalette('default')
@ -92,7 +92,7 @@ export const ProfileCard = observer(
)}
</View>
{renderButton ? (
<View style={styles.layoutButton}>{renderButton()}</View>
<View style={styles.layoutButton}>{renderButton(profile)}</View>
) : undefined}
</View>
{profile.description ? (

View file

@ -23,6 +23,7 @@ import {DropdownButton, DropdownItem} from '../util/forms/DropdownButton'
import * as Toast from '../util/Toast'
import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
import {Text} from '../util/text/Text'
import {TextLink} from '../util/Link'
import {RichText} from '../util/text/RichText'
import {UserAvatar} from '../util/UserAvatar'
import {UserBanner} from '../util/UserBanner'
@ -30,6 +31,7 @@ import {ProfileHeaderWarnings} from '../util/moderation/ProfileHeaderWarnings'
import {usePalette} from 'lib/hooks/usePalette'
import {useAnalytics} from 'lib/analytics'
import {NavigationProp} from 'lib/routes/types'
import {listUriToHref} from 'lib/strings/url-helpers'
import {isDesktopWeb, isNative} from 'platform/detection'
import {FollowState} from 'state/models/cache/my-follows'
import {shareUrl} from 'lib/sharing'
@ -146,12 +148,21 @@ const ProfileHeaderLoaded = observer(
navigation.push('ProfileFollows', {name: view.handle})
}, [track, navigation, view])
const onPressShare = React.useCallback(async () => {
const onPressShare = React.useCallback(() => {
track('ProfileHeader:ShareButtonClicked')
const url = toShareUrl(`/profile/${view.handle}`)
shareUrl(url)
}, [track, view])
const onPressAddRemoveLists = React.useCallback(() => {
track('ProfileHeader:AddToListsButtonClicked')
store.shell.openModal({
name: 'list-add-remove-user',
subject: view.did,
displayName: view.displayName || view.handle,
})
}, [track, view, store])
const onPressMuteAccount = React.useCallback(async () => {
track('ProfileHeader:MuteAccountButtonClicked')
try {
@ -233,6 +244,11 @@ const ProfileHeaderLoaded = observer(
label: 'Share',
onPress: onPressShare,
},
{
testID: 'profileHeaderDropdownListAddRemoveBtn',
label: 'Add to Lists',
onPress: onPressAddRemoveLists,
},
]
if (!isMe) {
items.push({sep: true})
@ -269,6 +285,7 @@ const ProfileHeaderLoaded = observer(
onPressUnblockAccount,
onPressBlockAccount,
onPressReportAccount,
onPressAddRemoveLists,
])
const blockHide = !isMe && (view.viewer.blocking || view.viewer.blockedBy)
@ -422,31 +439,42 @@ const ProfileHeaderLoaded = observer(
{view.viewer.blocking ? (
<View
testID="profileHeaderBlockedNotice"
style={[styles.moderationNotice, pal.view, pal.border]}>
<FontAwesomeIcon icon="ban" style={[pal.text, s.mr5]} />
<Text type="md" style={[s.mr2, pal.text]}>
style={[styles.moderationNotice, pal.viewLight]}>
<FontAwesomeIcon icon="ban" style={[pal.text]} />
<Text type="lg-medium" style={pal.text}>
Account blocked
</Text>
</View>
) : view.viewer.muted ? (
<View
testID="profileHeaderMutedNotice"
style={[styles.moderationNotice, pal.view, pal.border]}>
style={[styles.moderationNotice, pal.viewLight]}>
<FontAwesomeIcon
icon={['far', 'eye-slash']}
style={[pal.text, s.mr5]}
style={[pal.text]}
/>
<Text type="md" style={[s.mr2, pal.text]}>
Account muted
<Text type="lg-medium" style={pal.text}>
Account muted{' '}
{view.viewer.mutedByList && (
<Text type="lg-medium" style={pal.text}>
by{' '}
<TextLink
type="lg-medium"
style={pal.link}
href={listUriToHref(view.viewer.mutedByList.uri)}
text={view.viewer.mutedByList.name}
/>
</Text>
)}
</Text>
</View>
) : undefined}
{view.viewer.blockedBy && (
<View
testID="profileHeaderBlockedNotice"
style={[styles.moderationNotice, pal.view, pal.border]}>
<FontAwesomeIcon icon="ban" style={[pal.text, s.mr5]} />
<Text type="md" style={[s.mr2, pal.text]}>
style={[styles.moderationNotice, pal.viewLight]}>
<FontAwesomeIcon icon="ban" style={[pal.text]} />
<Text type="lg-medium" style={pal.text}>
This account has blocked you
</Text>
</View>
@ -595,10 +623,10 @@ const styles = StyleSheet.create({
moderationNotice: {
flexDirection: 'row',
alignItems: 'center',
borderWidth: 1,
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 10,
paddingHorizontal: 16,
paddingVertical: 14,
gap: 8,
},
br40: {borderRadius: 40},