diff options
Diffstat (limited to 'src/view/screens')
-rw-r--r-- | src/view/screens/Home.tsx | 2 | ||||
-rw-r--r-- | src/view/screens/Moderation.tsx | 136 | ||||
-rw-r--r-- | src/view/screens/ModerationBlockedAccounts.tsx (renamed from src/view/screens/BlockedAccounts.tsx) | 7 | ||||
-rw-r--r-- | src/view/screens/ModerationMuteLists.tsx | 122 | ||||
-rw-r--r-- | src/view/screens/ModerationMutedAccounts.tsx (renamed from src/view/screens/MutedAccounts.tsx) | 7 | ||||
-rw-r--r-- | src/view/screens/Profile.tsx | 95 | ||||
-rw-r--r-- | src/view/screens/ProfileList.tsx | 175 | ||||
-rw-r--r-- | src/view/screens/Settings.tsx | 55 |
8 files changed, 510 insertions, 89 deletions
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index 18e4f2506..0ead6b65c 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -62,7 +62,7 @@ export const HomeScreen = withAuthRequired( setSelectedPage(index) store.shell.setIsDrawerSwipeDisabled(index > 0) }, - [store], + [store, setSelectedPage], ) const onPressSelected = React.useCallback(() => { diff --git a/src/view/screens/Moderation.tsx b/src/view/screens/Moderation.tsx new file mode 100644 index 000000000..29ef8b4b2 --- /dev/null +++ b/src/view/screens/Moderation.tsx @@ -0,0 +1,136 @@ +import React from 'react' +import {StyleSheet, TouchableOpacity, View} from 'react-native' +import {useFocusEffect} from '@react-navigation/native' +import { + FontAwesomeIcon, + FontAwesomeIconStyle, +} from '@fortawesome/react-native-fontawesome' +import {observer} from 'mobx-react-lite' +import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' +import {withAuthRequired} from 'view/com/auth/withAuthRequired' +import {useStores} from 'state/index' +import {s} from 'lib/styles' +import {CenteredView} from '../com/util/Views' +import {ViewHeader} from '../com/util/ViewHeader' +import {Link} from '../com/util/Link' +import {Text} from '../com/util/text/Text' +import {usePalette} from 'lib/hooks/usePalette' +import {useAnalytics} from 'lib/analytics' +import {isDesktopWeb} from 'platform/detection' + +type Props = NativeStackScreenProps<CommonNavigatorParams, 'Moderation'> +export const ModerationScreen = withAuthRequired( + observer(function Moderation({}: Props) { + const pal = usePalette('default') + const store = useStores() + const {screen, track} = useAnalytics() + + useFocusEffect( + React.useCallback(() => { + screen('Moderation') + store.shell.setMinimalShellMode(false) + }, [screen, store]), + ) + + const onPressContentFiltering = React.useCallback(() => { + track('Moderation:ContentfilteringButtonClicked') + store.shell.openModal({name: 'content-filtering-settings'}) + }, [track, store]) + + return ( + <CenteredView + style={[ + s.hContentRegion, + pal.border, + isDesktopWeb ? styles.desktopContainer : pal.viewLight, + ]} + testID="moderationScreen"> + <ViewHeader title="Moderation" showOnDesktop /> + <View style={styles.spacer} /> + <TouchableOpacity + testID="contentFilteringBtn" + style={[styles.linkCard, pal.view]} + onPress={onPressContentFiltering} + accessibilityHint="Content filtering" + accessibilityLabel="Opens configurable content filtering settings"> + <View style={[styles.iconContainer, pal.btn]}> + <FontAwesomeIcon + icon="eye" + style={pal.text as FontAwesomeIconStyle} + /> + </View> + <Text type="lg" style={pal.text}> + Content filtering + </Text> + </TouchableOpacity> + <Link + testID="mutelistsBtn" + style={[styles.linkCard, pal.view]} + href="/moderation/mute-lists"> + <View style={[styles.iconContainer, pal.btn]}> + <FontAwesomeIcon + icon="users-slash" + style={pal.text as FontAwesomeIconStyle} + /> + </View> + <Text type="lg" style={pal.text}> + Mute lists + </Text> + </Link> + <Link + testID="mutedAccountsBtn" + style={[styles.linkCard, pal.view]} + href="/moderation/muted-accounts"> + <View style={[styles.iconContainer, pal.btn]}> + <FontAwesomeIcon + icon="user-slash" + style={pal.text as FontAwesomeIconStyle} + /> + </View> + <Text type="lg" style={pal.text}> + Muted accounts + </Text> + </Link> + <Link + testID="blockedAccountsBtn" + style={[styles.linkCard, pal.view]} + href="/moderation/blocked-accounts"> + <View style={[styles.iconContainer, pal.btn]}> + <FontAwesomeIcon + icon="ban" + style={pal.text as FontAwesomeIconStyle} + /> + </View> + <Text type="lg" style={pal.text}> + Blocked accounts + </Text> + </Link> + </CenteredView> + ) + }), +) + +const styles = StyleSheet.create({ + desktopContainer: { + borderLeftWidth: 1, + borderRightWidth: 1, + }, + spacer: { + height: 6, + }, + linkCard: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 12, + paddingHorizontal: 18, + marginBottom: 1, + }, + iconContainer: { + alignItems: 'center', + justifyContent: 'center', + width: 40, + height: 40, + borderRadius: 30, + marginRight: 12, + }, +}) diff --git a/src/view/screens/BlockedAccounts.tsx b/src/view/screens/ModerationBlockedAccounts.tsx index 195068510..cd506d630 100644 --- a/src/view/screens/BlockedAccounts.tsx +++ b/src/view/screens/ModerationBlockedAccounts.tsx @@ -22,8 +22,11 @@ import {ViewHeader} from '../com/util/ViewHeader' import {CenteredView} from 'view/com/util/Views' import {ProfileCard} from 'view/com/profile/ProfileCard' -type Props = NativeStackScreenProps<CommonNavigatorParams, 'BlockedAccounts'> -export const BlockedAccounts = withAuthRequired( +type Props = NativeStackScreenProps< + CommonNavigatorParams, + 'ModerationBlockedAccounts' +> +export const ModerationBlockedAccounts = withAuthRequired( observer(({}: Props) => { const pal = usePalette('default') const store = useStores() diff --git a/src/view/screens/ModerationMuteLists.tsx b/src/view/screens/ModerationMuteLists.tsx new file mode 100644 index 000000000..0b81f432f --- /dev/null +++ b/src/view/screens/ModerationMuteLists.tsx @@ -0,0 +1,122 @@ +import React from 'react' +import {StyleSheet} from 'react-native' +import {useFocusEffect, useNavigation} from '@react-navigation/native' +import { + FontAwesomeIcon, + FontAwesomeIconStyle, +} from '@fortawesome/react-native-fontawesome' +import {AtUri} from '@atproto/api' +import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' +import {withAuthRequired} from 'view/com/auth/withAuthRequired' +import {EmptyStateWithButton} from 'view/com/util/EmptyStateWithButton' +import {useStores} from 'state/index' +import {ListsListModel} from 'state/models/lists/lists-list' +import {ListsList} from 'view/com/lists/ListsList' +import {Button} from 'view/com/util/forms/Button' +import {NavigationProp} from 'lib/routes/types' +import {usePalette} from 'lib/hooks/usePalette' +import {CenteredView} from 'view/com/util/Views' +import {ViewHeader} from 'view/com/util/ViewHeader' +import {isDesktopWeb} from 'platform/detection' + +type Props = NativeStackScreenProps< + CommonNavigatorParams, + 'ModerationMuteLists' +> +export const ModerationMuteListsScreen = withAuthRequired(({}: Props) => { + const pal = usePalette('default') + const store = useStores() + const navigation = useNavigation<NavigationProp>() + + const mutelists: ListsListModel = React.useMemo( + () => new ListsListModel(store, 'my-modlists'), + [store], + ) + + useFocusEffect( + React.useCallback(() => { + store.shell.setMinimalShellMode(false) + mutelists.refresh() + }, [store, mutelists]), + ) + + const onPressNewMuteList = React.useCallback(() => { + store.shell.openModal({ + name: 'create-or-edit-mute-list', + onSave: (uri: string) => { + try { + const urip = new AtUri(uri) + navigation.navigate('ProfileList', { + name: urip.hostname, + rkey: urip.rkey, + }) + } catch {} + }, + }) + }, [store, navigation]) + + const renderEmptyState = React.useCallback(() => { + return ( + <EmptyStateWithButton + testID="emptyMuteLists" + icon="users-slash" + message="You can subscribe to mute lists to automatically mute all of the users they include. Mute lists are public but your subscription to a mute list is private." + buttonLabel="New Mute List" + onPress={onPressNewMuteList} + /> + ) + }, [onPressNewMuteList]) + + const renderHeaderButton = React.useCallback( + () => ( + <Button + type="primary-light" + onPress={onPressNewMuteList} + style={styles.createBtn}> + <FontAwesomeIcon + icon="plus" + style={pal.link as FontAwesomeIconStyle} + size={18} + /> + </Button> + ), + [onPressNewMuteList, pal], + ) + + return ( + <CenteredView + style={[ + styles.container, + isDesktopWeb && styles.containerDesktop, + pal.view, + pal.border, + ]} + testID="moderationMutelistsScreen"> + <ViewHeader + title="Mute Lists" + showOnDesktop + renderButton={renderHeaderButton} + /> + <ListsList + listsList={mutelists} + showAddBtns={isDesktopWeb} + renderEmptyState={renderEmptyState} + onPressCreateNew={onPressNewMuteList} + /> + </CenteredView> + ) +}) + +const styles = StyleSheet.create({ + container: { + flex: 1, + paddingBottom: isDesktopWeb ? 0 : 100, + }, + containerDesktop: { + borderLeftWidth: 1, + borderRightWidth: 1, + }, + createBtn: { + width: 40, + }, +}) diff --git a/src/view/screens/MutedAccounts.tsx b/src/view/screens/ModerationMutedAccounts.tsx index f7120051f..ec732f682 100644 --- a/src/view/screens/MutedAccounts.tsx +++ b/src/view/screens/ModerationMutedAccounts.tsx @@ -22,8 +22,11 @@ import {ViewHeader} from '../com/util/ViewHeader' import {CenteredView} from 'view/com/util/Views' import {ProfileCard} from 'view/com/profile/ProfileCard' -type Props = NativeStackScreenProps<CommonNavigatorParams, 'MutedAccounts'> -export const MutedAccounts = withAuthRequired( +type Props = NativeStackScreenProps< + CommonNavigatorParams, + 'ModerationMutedAccounts' +> +export const ModerationMutedAccounts = withAuthRequired( observer(({}: Props) => { const pal = usePalette('default') const store = useStores() diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index 5fb212554..d23974859 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -7,12 +7,16 @@ import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {ViewSelector} from '../com/util/ViewSelector' import {CenteredView} from '../com/util/Views' import {ScreenHider} from 'view/com/util/moderation/ScreenHider' -import {ProfileUiModel} from 'state/models/ui/profile' +import {ProfileUiModel, Sections} from 'state/models/ui/profile' import {useStores} from 'state/index' import {PostsFeedSliceModel} from 'state/models/feeds/posts' import {ProfileHeader} from '../com/profile/ProfileHeader' import {FeedSlice} from '../com/posts/FeedSlice' -import {PostFeedLoadingPlaceholder} from '../com/util/LoadingPlaceholder' +import {ListCard} from 'view/com/lists/ListCard' +import { + PostFeedLoadingPlaceholder, + ProfileCardFeedLoadingPlaceholder, +} from '../com/util/LoadingPlaceholder' import {ErrorScreen} from '../com/util/error/ErrorScreen' import {ErrorMessage} from '../com/util/error/ErrorMessage' import {EmptyState} from '../com/util/EmptyState' @@ -111,52 +115,81 @@ export const ProfileScreen = withAuthRequired( }, [uiState.showLoadingMoreFooter]) const renderItem = React.useCallback( (item: any) => { - if (item === ProfileUiModel.END_ITEM) { - return <Text style={styles.endItem}>- end of feed -</Text> - } else if (item === ProfileUiModel.LOADING_ITEM) { - return <PostFeedLoadingPlaceholder /> - } else if (item._reactKey === '__error__') { - if (uiState.feed.isBlocking) { + if (uiState.selectedView === Sections.Lists) { + if (item === ProfileUiModel.LOADING_ITEM) { + return <ProfileCardFeedLoadingPlaceholder /> + } else if (item._reactKey === '__error__') { + return ( + <View style={s.p5}> + <ErrorMessage + message={item.error} + onPressTryAgain={onPressTryAgain} + /> + </View> + ) + } else if (item === ProfileUiModel.EMPTY_ITEM) { return ( <EmptyState - icon="ban" - message="Posts hidden" + testID="listsEmpty" + icon="list-ul" + message="No lists yet!" style={styles.emptyState} /> ) + } else { + return <ListCard testID={`list-${item.name}`} list={item} /> } - if (uiState.feed.isBlockedBy) { + } else { + if (item === ProfileUiModel.END_ITEM) { + return <Text style={styles.endItem}>- end of feed -</Text> + } else if (item === ProfileUiModel.LOADING_ITEM) { + return <PostFeedLoadingPlaceholder /> + } else if (item._reactKey === '__error__') { + if (uiState.feed.isBlocking) { + return ( + <EmptyState + icon="ban" + message="Posts hidden" + style={styles.emptyState} + /> + ) + } + if (uiState.feed.isBlockedBy) { + return ( + <EmptyState + icon="ban" + message="Posts hidden" + style={styles.emptyState} + /> + ) + } + return ( + <View style={s.p5}> + <ErrorMessage + message={item.error} + onPressTryAgain={onPressTryAgain} + /> + </View> + ) + } else if (item === ProfileUiModel.EMPTY_ITEM) { return ( <EmptyState - icon="ban" - message="Posts hidden" + icon={['far', 'message']} + message="No posts yet!" style={styles.emptyState} /> ) + } else if (item instanceof PostsFeedSliceModel) { + return ( + <FeedSlice slice={item} ignoreMuteFor={uiState.profile.did} /> + ) } - return ( - <View style={s.p5}> - <ErrorMessage - message={item.error} - onPressTryAgain={onPressTryAgain} - /> - </View> - ) - } else if (item === ProfileUiModel.EMPTY_ITEM) { - return ( - <EmptyState - icon={['far', 'message']} - message="No posts yet!" - style={styles.emptyState} - /> - ) - } else if (item instanceof PostsFeedSliceModel) { - return <FeedSlice slice={item} ignoreMuteFor={uiState.profile.did} /> } return <View /> }, [ onPressTryAgain, + uiState.selectedView, uiState.profile.did, uiState.feed.isBlocking, uiState.feed.isBlockedBy, diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx new file mode 100644 index 000000000..a78faaf62 --- /dev/null +++ b/src/view/screens/ProfileList.tsx @@ -0,0 +1,175 @@ +import React from 'react' +import {StyleSheet, View} from 'react-native' +import {useFocusEffect} from '@react-navigation/native' +import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' +import {useNavigation} from '@react-navigation/native' +import {observer} from 'mobx-react-lite' +import {withAuthRequired} from 'view/com/auth/withAuthRequired' +import {ViewHeader} from 'view/com/util/ViewHeader' +import {CenteredView} from 'view/com/util/Views' +import {ListItems} from 'view/com/lists/ListItems' +import {EmptyState} from 'view/com/util/EmptyState' +import {Button} from 'view/com/util/forms/Button' +import * as Toast from 'view/com/util/Toast' +import {ListModel} from 'state/models/content/list' +import {useStores} from 'state/index' +import {usePalette} from 'lib/hooks/usePalette' +import {NavigationProp} from 'lib/routes/types' +import {isDesktopWeb} from 'platform/detection' + +type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileList'> +export const ProfileListScreen = withAuthRequired( + observer(({route}: Props) => { + const store = useStores() + const navigation = useNavigation<NavigationProp>() + const pal = usePalette('default') + const {name, rkey} = route.params + + const list: ListModel = React.useMemo(() => { + const model = new ListModel( + store, + `at://${name}/app.bsky.graph.list/${rkey}`, + ) + return model + }, [store, name, rkey]) + + useFocusEffect( + React.useCallback(() => { + store.shell.setMinimalShellMode(false) + list.loadMore(true) + }, [store, list]), + ) + + const onToggleSubscribed = React.useCallback(async () => { + try { + if (list.list?.viewer?.muted) { + await list.unsubscribe() + } else { + await list.subscribe() + } + } catch (err) { + Toast.show( + 'There was an an issue updating your subscription, please check your internet connection and try again.', + ) + store.log.error('Failed up update subscription', {err}) + } + }, [store, list]) + + const onPressEditList = React.useCallback(() => { + store.shell.openModal({ + name: 'create-or-edit-mute-list', + list, + onSave() { + list.refresh() + }, + }) + }, [store, list]) + + const onPressDeleteList = React.useCallback(() => { + store.shell.openModal({ + name: 'confirm', + title: 'Delete List', + message: 'Are you sure?', + async onPressConfirm() { + await list.delete() + if (navigation.canGoBack()) { + navigation.goBack() + } else { + navigation.navigate('Home') + } + }, + }) + }, [store, list, navigation]) + + const renderEmptyState = React.useCallback(() => { + return <EmptyState icon="users-slash" message="This list is empty!" /> + }, []) + + const renderHeaderBtn = React.useCallback(() => { + return ( + <View style={styles.headerBtns}> + {list?.isOwner && ( + <Button + type="default" + label="Delete List" + testID="deleteListBtn" + accessibilityLabel="Delete list" + accessibilityHint="Deletes the mutelist" + onPress={onPressDeleteList} + /> + )} + {list?.isOwner && ( + <Button + type="default" + label="Edit List" + testID="editListBtn" + accessibilityLabel="Edit list" + accessibilityHint="Opens a modal to edit the mutelist" + onPress={onPressEditList} + /> + )} + {list.list?.viewer?.muted ? ( + <Button + type="inverted" + label="Unsubscribe" + testID="unsubscribeListBtn" + accessibilityLabel="Unsubscribe from this list" + accessibilityHint="Stops muting the users included in this list" + onPress={onToggleSubscribed} + /> + ) : ( + <Button + type="primary" + label="Subscribe & Mute" + testID="subscribeListBtn" + accessibilityLabel="Subscribe to this list" + accessibilityHint="Mutes the users included in this list" + onPress={onToggleSubscribed} + /> + )} + </View> + ) + }, [ + list?.isOwner, + list.list?.viewer?.muted, + onPressDeleteList, + onPressEditList, + onToggleSubscribed, + ]) + + return ( + <CenteredView + style={[ + styles.container, + isDesktopWeb && styles.containerDesktop, + pal.view, + pal.border, + ]} + testID="moderationMutelistsScreen"> + <ViewHeader title="" renderButton={renderHeaderBtn} /> + <ListItems + list={list} + renderEmptyState={renderEmptyState} + onToggleSubscribed={onToggleSubscribed} + onPressEditList={onPressEditList} + onPressDeleteList={onPressDeleteList} + /> + </CenteredView> + ) + }), +) + +const styles = StyleSheet.create({ + headerBtns: { + flexDirection: 'row', + gap: 8, + }, + container: { + flex: 1, + paddingBottom: isDesktopWeb ? 0 : 100, + }, + containerDesktop: { + borderLeftWidth: 1, + borderRightWidth: 1, + }, +}) diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx index f98cdc0c8..c73653713 100644 --- a/src/view/screens/Settings.tsx +++ b/src/view/screens/Settings.tsx @@ -127,11 +127,6 @@ export const SettingsScreen = withAuthRequired( store.shell.openModal({name: 'invite-codes'}) }, [track, store]) - const onPressContentFiltering = React.useCallback(() => { - track('Settings:ContentfilteringButtonClicked') - store.shell.openModal({name: 'content-filtering-settings'}) - }, [track, store]) - const onPressContentLanguages = React.useCallback(() => { track('Settings:ContentlanguagesButtonClicked') store.shell.openModal({name: 'content-languages-settings'}) @@ -252,7 +247,9 @@ export const SettingsScreen = withAuthRequired( Add account </Text> </TouchableOpacity> + <View style={styles.spacer20} /> + <Text type="xl-bold" style={[pal.text, styles.heading]}> Invite a friend </Text> @@ -288,54 +285,6 @@ export const SettingsScreen = withAuthRequired( <View style={styles.spacer20} /> <Text type="xl-bold" style={[pal.text, styles.heading]}> - Moderation - </Text> - <TouchableOpacity - testID="contentFilteringBtn" - style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]} - onPress={isSwitching ? undefined : onPressContentFiltering} - accessibilityHint="Content moderation" - accessibilityLabel="Opens configurable content moderation settings"> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon="eye" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - <Text type="lg" style={pal.text}> - Content moderation - </Text> - </TouchableOpacity> - <Link - testID="mutedAccountsBtn" - style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]} - href="/settings/muted-accounts"> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon={['far', 'eye-slash']} - style={pal.text as FontAwesomeIconStyle} - /> - </View> - <Text type="lg" style={pal.text}> - Muted accounts - </Text> - </Link> - <Link - testID="blockedAccountsBtn" - style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]} - href="/settings/blocked-accounts"> - <View style={[styles.iconContainer, pal.btn]}> - <FontAwesomeIcon - icon="ban" - style={pal.text as FontAwesomeIconStyle} - /> - </View> - <Text type="lg" style={pal.text}> - Blocked accounts - </Text> - </Link> - <View style={styles.spacer20} /> - <Text type="xl-bold" style={[pal.text, styles.heading]}> Advanced </Text> <Link |