Add modservice screen and profile-header-card
This commit is contained in:
@@ -210,6 +210,7 @@ func serve(cctx *cli.Context) error {
|
|||||||
e.GET("/profile/:handleOrDID/lists/:rkey", server.WebGeneric)
|
e.GET("/profile/:handleOrDID/lists/:rkey", server.WebGeneric)
|
||||||
e.GET("/profile/:handleOrDID/feed/:rkey", server.WebGeneric)
|
e.GET("/profile/:handleOrDID/feed/:rkey", server.WebGeneric)
|
||||||
e.GET("/profile/:handleOrDID/feed/:rkey/liked-by", server.WebGeneric)
|
e.GET("/profile/:handleOrDID/feed/:rkey/liked-by", server.WebGeneric)
|
||||||
|
e.GET("/profile/:handleOrDID/modservice", server.WebGeneric)
|
||||||
|
|
||||||
// profile RSS feed (DID not handle)
|
// profile RSS feed (DID not handle)
|
||||||
e.GET("/profile/:ident/rss", server.WebProfileRSS)
|
e.GET("/profile/:ident/rss", server.WebProfileRSS)
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ import {ProfileFollowersScreen} from './view/screens/ProfileFollowers'
|
|||||||
import {ProfileFollowsScreen} from './view/screens/ProfileFollows'
|
import {ProfileFollowsScreen} from './view/screens/ProfileFollows'
|
||||||
import {ProfileFeedScreen} from './view/screens/ProfileFeed'
|
import {ProfileFeedScreen} from './view/screens/ProfileFeed'
|
||||||
import {ProfileFeedLikedByScreen} from './view/screens/ProfileFeedLikedBy'
|
import {ProfileFeedLikedByScreen} from './view/screens/ProfileFeedLikedBy'
|
||||||
|
import {ProfileModserviceScreen} from './view/screens/ProfileModservice'
|
||||||
import {ProfileListScreen} from './view/screens/ProfileList'
|
import {ProfileListScreen} from './view/screens/ProfileList'
|
||||||
import {PostThreadScreen} from './view/screens/PostThread'
|
import {PostThreadScreen} from './view/screens/PostThread'
|
||||||
import {PostLikedByScreen} from './view/screens/PostLikedBy'
|
import {PostLikedByScreen} from './view/screens/PostLikedBy'
|
||||||
@@ -197,6 +198,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
|||||||
getComponent={() => ProfileFeedLikedByScreen}
|
getComponent={() => ProfileFeedLikedByScreen}
|
||||||
options={{title: title(msg`Liked by`)}}
|
options={{title: title(msg`Liked by`)}}
|
||||||
/>
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name="ProfileModservice"
|
||||||
|
getComponent={() => ProfileModserviceScreen}
|
||||||
|
options={{title: title(msg`Moderation service`)}}
|
||||||
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="Debug"
|
name="Debug"
|
||||||
getComponent={() => Storybook}
|
getComponent={() => Storybook}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export type CommonNavigatorParams = {
|
|||||||
PostRepostedBy: {name: string; rkey: string}
|
PostRepostedBy: {name: string; rkey: string}
|
||||||
ProfileFeed: {name: string; rkey: string}
|
ProfileFeed: {name: string; rkey: string}
|
||||||
ProfileFeedLikedBy: {name: string; rkey: string}
|
ProfileFeedLikedBy: {name: string; rkey: string}
|
||||||
|
ProfileModservice: {name: string}
|
||||||
Debug: undefined
|
Debug: undefined
|
||||||
Log: undefined
|
Log: undefined
|
||||||
Support: undefined
|
Support: undefined
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const router = new Router({
|
|||||||
PostRepostedBy: '/profile/:name/post/:rkey/reposted-by',
|
PostRepostedBy: '/profile/:name/post/:rkey/reposted-by',
|
||||||
ProfileFeed: '/profile/:name/feed/:rkey',
|
ProfileFeed: '/profile/:name/feed/:rkey',
|
||||||
ProfileFeedLikedBy: '/profile/:name/feed/:rkey/liked-by',
|
ProfileFeedLikedBy: '/profile/:name/feed/:rkey/liked-by',
|
||||||
|
ProfileModservice: '/profile/:name/modservice',
|
||||||
Debug: '/sys/debug',
|
Debug: '/sys/debug',
|
||||||
Log: '/sys/log',
|
Log: '/sys/log',
|
||||||
AppPasswords: '/settings/app-passwords',
|
AppPasswords: '/settings/app-passwords',
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import {useQuery} from '@tanstack/react-query'
|
||||||
|
import {getAgent} from '../session'
|
||||||
|
|
||||||
|
export const RQKEY = (did: string) => ['mod-service-info', did]
|
||||||
|
|
||||||
|
export function useModServiceInfoQuery({did}: {did: string}) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: RQKEY(did),
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await getAgent().app.bsky.moderation.getService({did})
|
||||||
|
return res.data
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -312,6 +312,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
__globalAgent = agent
|
__globalAgent = agent
|
||||||
|
window.agent = agent
|
||||||
queryClient.clear()
|
queryClient.clear()
|
||||||
upsertAccount(account)
|
upsertAccount(account)
|
||||||
|
|
||||||
@@ -351,6 +352,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
{networkErrorCallback: clearCurrentAccount},
|
{networkErrorCallback: clearCurrentAccount},
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
window.agent = agent
|
||||||
|
|
||||||
let canReusePrevSession = false
|
let canReusePrevSession = false
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {StyleSheet, View} from 'react-native'
|
||||||
|
import {s} from 'lib/styles'
|
||||||
|
import {Text} from '../util/text/Text'
|
||||||
|
import {TextLink} from '../util/Link'
|
||||||
|
import {ToggleButton} from '../util/forms/ToggleButton'
|
||||||
|
import {Button} from '../util/forms/Button'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {isIOS} from 'platform/detection'
|
||||||
|
import * as Toast from '../util/Toast'
|
||||||
|
import {logger} from '#/logger'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {useModalControls} from '#/state/modals'
|
||||||
|
import {
|
||||||
|
usePreferencesQuery,
|
||||||
|
usePreferencesSetAdultContentMutation,
|
||||||
|
} from '#/state/queries/preferences'
|
||||||
|
|
||||||
|
export function AdultContentEnabledPref() {
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const {_} = useLingui()
|
||||||
|
const {data: preferences} = usePreferencesQuery()
|
||||||
|
const {mutate, variables} = usePreferencesSetAdultContentMutation()
|
||||||
|
const {openModal} = useModalControls()
|
||||||
|
|
||||||
|
const onSetAge = React.useCallback(
|
||||||
|
() => openModal({name: 'birth-date-settings'}),
|
||||||
|
[openModal],
|
||||||
|
)
|
||||||
|
|
||||||
|
const onToggleAdultContent = React.useCallback(async () => {
|
||||||
|
if (isIOS) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
mutate({
|
||||||
|
enabled: !(variables?.enabled ?? preferences?.adultContentEnabled),
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
Toast.show(
|
||||||
|
_(msg`There was an issue syncing your preferences with the server`),
|
||||||
|
)
|
||||||
|
logger.error('Failed to update preferences with server', {error: e})
|
||||||
|
}
|
||||||
|
}, [variables, preferences, mutate, _])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[pal.border, {borderTopWidth: 1, paddingHorizontal: 12}]}>
|
||||||
|
{isIOS ? (
|
||||||
|
preferences?.adultContentEnabled ? null : (
|
||||||
|
<View style={{paddingVertical: 12}}>
|
||||||
|
<Text type="md" style={pal.textLight}>
|
||||||
|
<Trans>
|
||||||
|
Adult content can only be enabled via the Web at{' '}
|
||||||
|
<TextLink
|
||||||
|
style={pal.link}
|
||||||
|
href="https://bsky.app"
|
||||||
|
text="bsky.app"
|
||||||
|
/>
|
||||||
|
.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
) : (preferences?.userAge || 0) >= 18 ? (
|
||||||
|
<View style={{paddingVertical: 4}}>
|
||||||
|
<ToggleButton
|
||||||
|
type="default-light"
|
||||||
|
label={_(msg`Enable Adult Content`)}
|
||||||
|
isSelected={
|
||||||
|
variables?.enabled ?? preferences?.adultContentEnabled ?? false
|
||||||
|
}
|
||||||
|
onPress={onToggleAdultContent}
|
||||||
|
style={styles.toggleBtn}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View style={styles.agePrompt}>
|
||||||
|
<Text type="md" style={[pal.text, {flex: 1}]}>
|
||||||
|
<Trans>You must be 18 or older to enable adult content.</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
agePrompt: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingVertical: 12,
|
||||||
|
},
|
||||||
|
toggleBtn: {
|
||||||
|
paddingHorizontal: 0,
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {LabelPreference} from '@atproto/api'
|
||||||
|
import {StyleSheet, Pressable, View} from 'react-native'
|
||||||
|
import {s} from 'lib/styles'
|
||||||
|
import {Text} from '../util/text/Text'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {
|
||||||
|
usePreferencesSetContentLabelMutation,
|
||||||
|
ConfigurableLabelGroup,
|
||||||
|
CONFIGURABLE_LABEL_GROUPS,
|
||||||
|
UsePreferencesQueryResponse,
|
||||||
|
} from '#/state/queries/preferences'
|
||||||
|
|
||||||
|
export function LabelGroupPref({
|
||||||
|
preferences,
|
||||||
|
labelGroup,
|
||||||
|
disabled,
|
||||||
|
}: {
|
||||||
|
preferences?: UsePreferencesQueryResponse
|
||||||
|
labelGroup: ConfigurableLabelGroup
|
||||||
|
disabled?: boolean
|
||||||
|
}) {
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const visibility = preferences?.contentLabels?.[labelGroup]
|
||||||
|
const {mutate, variables} = usePreferencesSetContentLabelMutation()
|
||||||
|
|
||||||
|
const onChange = React.useCallback(
|
||||||
|
(vis: LabelPreference) => {
|
||||||
|
mutate({labelGroup, visibility: vis})
|
||||||
|
},
|
||||||
|
[mutate, labelGroup],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[styles.labelGroupPref, pal.border]}>
|
||||||
|
<View style={s.flex1}>
|
||||||
|
<Text type="md-medium" style={[pal.text]}>
|
||||||
|
{CONFIGURABLE_LABEL_GROUPS[labelGroup].title}
|
||||||
|
</Text>
|
||||||
|
{typeof CONFIGURABLE_LABEL_GROUPS[labelGroup].subtitle === 'string' && (
|
||||||
|
<Text type="sm" style={[pal.textLight]}>
|
||||||
|
{CONFIGURABLE_LABEL_GROUPS[labelGroup].subtitle}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{disabled || !visibility ? (
|
||||||
|
<Text type="sm-bold" style={pal.textLight}>
|
||||||
|
<Trans context="action">Hide</Trans>
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<SelectGroup
|
||||||
|
current={variables?.visibility || visibility}
|
||||||
|
onChange={onChange}
|
||||||
|
labelGroup={labelGroup}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SelectGroupProps {
|
||||||
|
current: LabelPreference
|
||||||
|
onChange: (v: LabelPreference) => void
|
||||||
|
labelGroup: ConfigurableLabelGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectGroup({current, onChange, labelGroup}: SelectGroupProps) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.selectableBtns}>
|
||||||
|
<SelectableBtn
|
||||||
|
current={current}
|
||||||
|
value="hide"
|
||||||
|
label={_(msg`Hide`)}
|
||||||
|
left
|
||||||
|
onChange={onChange}
|
||||||
|
labelGroup={labelGroup}
|
||||||
|
/>
|
||||||
|
<SelectableBtn
|
||||||
|
current={current}
|
||||||
|
value="warn"
|
||||||
|
label={_(msg`Warn`)}
|
||||||
|
onChange={onChange}
|
||||||
|
labelGroup={labelGroup}
|
||||||
|
/>
|
||||||
|
<SelectableBtn
|
||||||
|
current={current}
|
||||||
|
value="ignore"
|
||||||
|
label={_(msg`Show`)}
|
||||||
|
right
|
||||||
|
onChange={onChange}
|
||||||
|
labelGroup={labelGroup}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SelectableBtnProps {
|
||||||
|
current: string
|
||||||
|
value: LabelPreference
|
||||||
|
label: string
|
||||||
|
left?: boolean
|
||||||
|
right?: boolean
|
||||||
|
onChange: (v: LabelPreference) => void
|
||||||
|
labelGroup: ConfigurableLabelGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectableBtn({
|
||||||
|
current,
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
onChange,
|
||||||
|
labelGroup,
|
||||||
|
}: SelectableBtnProps) {
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const palPrimary = usePalette('inverted')
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
style={[
|
||||||
|
styles.selectableBtn,
|
||||||
|
left && styles.selectableBtnLeft,
|
||||||
|
right && styles.selectableBtnRight,
|
||||||
|
pal.border,
|
||||||
|
current === value ? palPrimary.view : pal.view,
|
||||||
|
]}
|
||||||
|
onPress={() => onChange(value)}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={value}
|
||||||
|
accessibilityHint={_(
|
||||||
|
msg`Set ${value} for ${labelGroup} content moderation policy`,
|
||||||
|
)}>
|
||||||
|
<Text style={current === value ? palPrimary.text : pal.text}>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
labelGroupPref: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingVertical: 14,
|
||||||
|
paddingLeft: 14,
|
||||||
|
paddingRight: 10,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
selectableBtns: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
marginLeft: 10,
|
||||||
|
},
|
||||||
|
selectableBtn: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'center',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderLeftWidth: 0,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
paddingVertical: 10,
|
||||||
|
},
|
||||||
|
selectableBtnLeft: {
|
||||||
|
borderTopLeftRadius: 8,
|
||||||
|
borderBottomLeftRadius: 8,
|
||||||
|
borderLeftWidth: 1,
|
||||||
|
},
|
||||||
|
selectableBtnRight: {
|
||||||
|
borderTopRightRadius: 8,
|
||||||
|
borderBottomRightRadius: 8,
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {Text} from '../util/text/Text'
|
||||||
|
import {usePalette} from '#/lib/hooks/usePalette'
|
||||||
|
|
||||||
|
export function ModServiceGuidelines({}: {}) {
|
||||||
|
const pal = usePalette('default')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View testID="modServiceGuidelines">
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
pal.border,
|
||||||
|
{paddingHorizontal: 14, paddingBottom: 8, borderBottomWidth: 1},
|
||||||
|
]}>
|
||||||
|
<Text type="2xl-bold">Guidelines</Text>
|
||||||
|
</View>
|
||||||
|
<View style={{paddingHorizontal: 14, paddingVertical: 8}}>
|
||||||
|
<Text type="lg" style={pal.text} lineHeight={1.4}>
|
||||||
|
These rules will evolve over time as we continually work to cultivate
|
||||||
|
a healthy and thriving community. Do not:
|
||||||
|
</Text>
|
||||||
|
<Text type="lg" style={pal.text} lineHeight={1.4}>
|
||||||
|
1. Praise or promote material from hate groups or U.S., Canadian, and
|
||||||
|
E.U. proscribed terror groups.
|
||||||
|
</Text>
|
||||||
|
<Text type="lg" style={pal.text} lineHeight={1.4}>
|
||||||
|
2. Distribute child sexual abuse material
|
||||||
|
</Text>
|
||||||
|
<Text type="lg" style={pal.text} lineHeight={1.4}>
|
||||||
|
3. Engage in human trafficking or sexual exploitation, including any
|
||||||
|
attempt to distribute, participate or normalize child sexual abuse
|
||||||
|
</Text>
|
||||||
|
<Text type="lg" style={pal.text} lineHeight={1.4}>
|
||||||
|
4. Trade in illegal goods or substances
|
||||||
|
</Text>
|
||||||
|
<Text type="lg" style={pal.text} lineHeight={1.4}>
|
||||||
|
5. Steal or distribute others’ private personal information without
|
||||||
|
their permission
|
||||||
|
</Text>
|
||||||
|
<Text type="lg" style={pal.text} lineHeight={1.4}>
|
||||||
|
6. Hack or access systems that you aren’t authorized to access
|
||||||
|
</Text>
|
||||||
|
<Text type="lg" style={pal.text} lineHeight={1.4}>
|
||||||
|
7. Scam or cheat others for financial gain h. Spam, phish, or
|
||||||
|
otherwise use technical means to disrupt the experience of others on
|
||||||
|
Bluesky Social
|
||||||
|
</Text>
|
||||||
|
<Text type="lg" style={pal.text} lineHeight={1.4}>
|
||||||
|
8. Infringe other’s copyrights, trademarks and/or other intellectual
|
||||||
|
property
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {Pressable, StyleSheet, View} from 'react-native'
|
||||||
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
|
import {useNavigation} from '@react-navigation/native'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
|
import {Text} from '../util/text/Text'
|
||||||
|
import {TextLink} from '../util/Link'
|
||||||
|
import {CenteredView} from '../util/Views'
|
||||||
|
import {sanitizeHandle} from 'lib/strings/handles'
|
||||||
|
import {makeProfileLink} from 'lib/routes/links'
|
||||||
|
import {NavigationProp} from 'lib/routes/types'
|
||||||
|
import {BACK_HITSLOP} from 'lib/constants'
|
||||||
|
import {isNative} from 'platform/detection'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useSetDrawerOpen} from '#/state/shell'
|
||||||
|
import {emitSoftReset} from '#/state/events'
|
||||||
|
import {AppBskyModerationDefs} from '@atproto/api'
|
||||||
|
import {HandIcon} from '#/lib/icons'
|
||||||
|
import {shareUrl} from 'lib/sharing'
|
||||||
|
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||||
|
import {Button} from '../util/forms/Button'
|
||||||
|
import {NativeDropdown, DropdownItem} from 'view/com/util/forms/NativeDropdown'
|
||||||
|
import {useSession} from '#/state/session'
|
||||||
|
import {useModalControls} from '#/state/modals'
|
||||||
|
|
||||||
|
export function ModServiceHeader({
|
||||||
|
info,
|
||||||
|
}: {
|
||||||
|
info: AppBskyModerationDefs.ModServiceViewDetailed
|
||||||
|
}) {
|
||||||
|
const setDrawerOpen = useSetDrawerOpen()
|
||||||
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
const {_} = useLingui()
|
||||||
|
const {isMobile} = useWebMediaQueries()
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const canGoBack = navigation.canGoBack()
|
||||||
|
|
||||||
|
const onPressBack = React.useCallback(() => {
|
||||||
|
if (navigation.canGoBack()) {
|
||||||
|
navigation.goBack()
|
||||||
|
} else {
|
||||||
|
navigation.navigate('Home')
|
||||||
|
}
|
||||||
|
}, [navigation])
|
||||||
|
|
||||||
|
const onPressMenu = React.useCallback(() => {
|
||||||
|
setDrawerOpen(true)
|
||||||
|
}, [setDrawerOpen])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CenteredView style={pal.view}>
|
||||||
|
{isMobile && (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
paddingTop: isNative ? 0 : 8,
|
||||||
|
paddingBottom: 8,
|
||||||
|
paddingHorizontal: isMobile ? 12 : 14,
|
||||||
|
},
|
||||||
|
pal.border,
|
||||||
|
]}>
|
||||||
|
<Pressable
|
||||||
|
testID="headerDrawerBtn"
|
||||||
|
onPress={canGoBack ? onPressBack : onPressMenu}
|
||||||
|
hitSlop={BACK_HITSLOP}
|
||||||
|
style={canGoBack ? styles.backBtn : styles.backBtnWide}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={canGoBack ? 'Back' : 'Menu'}
|
||||||
|
accessibilityHint="">
|
||||||
|
{canGoBack ? (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
size={18}
|
||||||
|
icon="angle-left"
|
||||||
|
style={[styles.backIcon, pal.text]}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
size={18}
|
||||||
|
icon="bars"
|
||||||
|
style={[styles.backIcon, pal.textLight]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Pressable>
|
||||||
|
<View style={{flex: 1}} />
|
||||||
|
<CommonControls info={info} />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
gap: 10,
|
||||||
|
paddingTop: 14,
|
||||||
|
paddingBottom: 14,
|
||||||
|
paddingHorizontal: isMobile ? 12 : 14,
|
||||||
|
}}>
|
||||||
|
<View style={{alignSelf: 'center'}}>
|
||||||
|
<HandIcon style={pal.text} size={32} strokeWidth={5.5} />
|
||||||
|
</View>
|
||||||
|
<View style={{flex: 1}}>
|
||||||
|
<TextLink
|
||||||
|
testID="headerTitle"
|
||||||
|
type="title-xl"
|
||||||
|
href={makeProfileLink(info.creator, 'modservice')}
|
||||||
|
style={[pal.text, {fontWeight: 'bold'}]}
|
||||||
|
text={
|
||||||
|
info.creator.displayName
|
||||||
|
? sanitizeDisplayName(info.creator.displayName)
|
||||||
|
: sanitizeHandle(info.creator.handle, '@')
|
||||||
|
}
|
||||||
|
onPress={emitSoftReset}
|
||||||
|
numberOfLines={4}
|
||||||
|
/>
|
||||||
|
<Text type="xl" style={[pal.textLight]} numberOfLines={1}>
|
||||||
|
<Trans>Moderation service</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}>
|
||||||
|
<Button type="primary" label={_(msg`Subscribe`)} />
|
||||||
|
{!isMobile && <CommonControls info={info} />}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</CenteredView>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommonControls({
|
||||||
|
info,
|
||||||
|
}: {
|
||||||
|
info: AppBskyModerationDefs.ModServiceViewDetailed
|
||||||
|
}) {
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const {hasSession} = useSession()
|
||||||
|
const {openModal} = useModalControls()
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
|
const onPressShare = React.useCallback(() => {
|
||||||
|
const url = makeProfileLink(info.creator, 'modservice')
|
||||||
|
shareUrl(url)
|
||||||
|
// track('CustomFeed:Share') TODO
|
||||||
|
}, [info /*, track*/])
|
||||||
|
|
||||||
|
const onPressReport = React.useCallback(() => {
|
||||||
|
if (!info) return
|
||||||
|
openModal({
|
||||||
|
name: 'report',
|
||||||
|
uri: info.uri,
|
||||||
|
cid: info.cid,
|
||||||
|
})
|
||||||
|
}, [openModal, info])
|
||||||
|
|
||||||
|
const dropdownItems: DropdownItem[] = React.useMemo(() => {
|
||||||
|
return [
|
||||||
|
hasSession && {
|
||||||
|
testID: 'modHeaderDropdownReportBtn',
|
||||||
|
label: _(msg`Report mod service`),
|
||||||
|
onPress: onPressReport,
|
||||||
|
icon: {
|
||||||
|
ios: {
|
||||||
|
name: 'exclamationmark.triangle',
|
||||||
|
},
|
||||||
|
android: 'ic_menu_report_image',
|
||||||
|
web: 'circle-exclamation',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testID: 'modHeaderDropdownShareBtn',
|
||||||
|
label: _(msg`Share mod service`),
|
||||||
|
onPress: onPressShare,
|
||||||
|
icon: {
|
||||||
|
ios: {
|
||||||
|
name: 'square.and.arrow.up',
|
||||||
|
},
|
||||||
|
android: 'ic_menu_share',
|
||||||
|
web: 'share',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
].filter(Boolean) as DropdownItem[]
|
||||||
|
}, [hasSession, onPressReport, onPressShare, _])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<NativeDropdown
|
||||||
|
testID="headerDropdownBtn"
|
||||||
|
items={dropdownItems}
|
||||||
|
accessibilityLabel={_(msg`More options`)}
|
||||||
|
accessibilityHint="">
|
||||||
|
<View style={[pal.viewLight, styles.btn, {marginLeft: 6}]}>
|
||||||
|
<FontAwesomeIcon icon="ellipsis" size={20} color={pal.colors.text} />
|
||||||
|
</View>
|
||||||
|
</NativeDropdown>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
backBtn: {
|
||||||
|
width: 20,
|
||||||
|
height: 30,
|
||||||
|
},
|
||||||
|
backBtnWide: {
|
||||||
|
width: 20,
|
||||||
|
height: 30,
|
||||||
|
paddingHorizontal: 6,
|
||||||
|
},
|
||||||
|
backIcon: {
|
||||||
|
marginTop: 6,
|
||||||
|
},
|
||||||
|
btn: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 6,
|
||||||
|
paddingVertical: 7,
|
||||||
|
paddingHorizontal: 14,
|
||||||
|
borderRadius: 50,
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||||
|
import {LabelGroupPref} from './LabelGroupPref'
|
||||||
|
import {Text} from '../util/text/Text'
|
||||||
|
import {usePalette} from '#/lib/hooks/usePalette'
|
||||||
|
|
||||||
|
export function ModServicePrefs({}: {}) {
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const {data: preferences} = usePreferencesQuery()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View testID="modServicePrefs" style={[pal.border, {borderBottomWidth: 1}]}>
|
||||||
|
<View style={{paddingHorizontal: 14, paddingBottom: 8}}>
|
||||||
|
<Text type="2xl-bold">Settings</Text>
|
||||||
|
</View>
|
||||||
|
<LabelGroupPref
|
||||||
|
preferences={preferences}
|
||||||
|
labelGroup="nsfw"
|
||||||
|
disabled={!preferences?.adultContentEnabled}
|
||||||
|
/>
|
||||||
|
<LabelGroupPref
|
||||||
|
preferences={preferences}
|
||||||
|
labelGroup="nudity"
|
||||||
|
disabled={!preferences?.adultContentEnabled}
|
||||||
|
/>
|
||||||
|
<LabelGroupPref
|
||||||
|
preferences={preferences}
|
||||||
|
labelGroup="suggestive"
|
||||||
|
disabled={!preferences?.adultContentEnabled}
|
||||||
|
/>
|
||||||
|
<LabelGroupPref
|
||||||
|
preferences={preferences}
|
||||||
|
labelGroup="gore"
|
||||||
|
disabled={!preferences?.adultContentEnabled}
|
||||||
|
/>
|
||||||
|
<LabelGroupPref preferences={preferences} labelGroup="hate" />
|
||||||
|
<LabelGroupPref preferences={preferences} labelGroup="spam" />
|
||||||
|
<LabelGroupPref preferences={preferences} labelGroup="impersonation" />
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ import {RichText} from '../util/text/RichText'
|
|||||||
import {UserAvatar} from '../util/UserAvatar'
|
import {UserAvatar} from '../util/UserAvatar'
|
||||||
import {UserBanner} from '../util/UserBanner'
|
import {UserBanner} from '../util/UserBanner'
|
||||||
import {ProfileHeaderAlerts} from '../util/moderation/ProfileHeaderAlerts'
|
import {ProfileHeaderAlerts} from '../util/moderation/ProfileHeaderAlerts'
|
||||||
|
import {ProfileHeaderModCard} from './ProfileHeaderModCard'
|
||||||
import {formatCount} from '../util/numeric/format'
|
import {formatCount} from '../util/numeric/format'
|
||||||
import {NativeDropdown, DropdownItem} from '../util/forms/NativeDropdown'
|
import {NativeDropdown, DropdownItem} from '../util/forms/NativeDropdown'
|
||||||
import {Link} from '../util/Link'
|
import {Link} from '../util/Link'
|
||||||
@@ -663,6 +664,7 @@ let ProfileHeaderLoaded = ({
|
|||||||
{isMe && (
|
{isMe && (
|
||||||
<LabelInfo details={{did: profile.did}} labels={profile.labels} />
|
<LabelInfo details={{did: profile.did}} labels={profile.labels} />
|
||||||
)}
|
)}
|
||||||
|
<ProfileHeaderModCard />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{!isProfilePreview && showSuggestedFollows && (
|
{!isProfilePreview && showSuggestedFollows && (
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {StyleSheet, View} from 'react-native'
|
||||||
|
import {RichText as RichTextAPI} from '@atproto/api'
|
||||||
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
|
import {HandIcon} from '#/lib/icons'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
|
||||||
|
import {Text} from '../util/text/Text'
|
||||||
|
import {RichText} from '../util/text/RichText'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {s, colors} from 'lib/styles'
|
||||||
|
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||||
|
|
||||||
|
const richtext = new RichTextAPI({
|
||||||
|
text: "Bluesky's official moderation service",
|
||||||
|
})
|
||||||
|
|
||||||
|
export function ProfileHeaderModCard() {
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const {isMobile} = useWebMediaQueries()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
pal.view,
|
||||||
|
pal.borderDark,
|
||||||
|
s.mt5,
|
||||||
|
{
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingVertical: 10,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<View style={[s.flexRow, s.alignCenter, {gap: 8}]}>
|
||||||
|
{!isMobile && <HandIcon style={pal.text} size={24} strokeWidth={5.5} />}
|
||||||
|
<View style={{flex: 1}}>
|
||||||
|
<Text type="lg-bold" style={pal.text}>
|
||||||
|
<Trans>Moderation service</Trans>
|
||||||
|
</Text>
|
||||||
|
<RichText richText={richtext} />
|
||||||
|
</View>
|
||||||
|
{isMobile ? (
|
||||||
|
<HandIcon style={pal.text} size={24} strokeWidth={5.5} />
|
||||||
|
) : (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
pal.viewLight,
|
||||||
|
{paddingHorizontal: 12, paddingVertical: 6, borderRadius: 24},
|
||||||
|
]}>
|
||||||
|
<Text type="button" style={pal.text}>
|
||||||
|
<Trans>View</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({})
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
import React, {useMemo, useCallback} from 'react'
|
||||||
|
import {Dimensions, StyleSheet, View, ActivityIndicator} from 'react-native'
|
||||||
|
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||||
|
import {useIsFocused, useNavigation} from '@react-navigation/native'
|
||||||
|
import {AppBskyModerationDefs, RichText as RichTextAPI} from '@atproto/api'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {HeartIcon, HeartIconSolid} from 'lib/icons'
|
||||||
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
|
import {CommonNavigatorParams} from 'lib/routes/types'
|
||||||
|
import {makeRecordUri} from 'lib/strings/url-helpers'
|
||||||
|
import {s} from 'lib/styles'
|
||||||
|
import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader'
|
||||||
|
import {TextLink} from 'view/com/util/Link'
|
||||||
|
import {Button} from 'view/com/util/forms/Button'
|
||||||
|
import {Text} from 'view/com/util/text/Text'
|
||||||
|
import {RichText} from 'view/com/util/text/RichText'
|
||||||
|
import {ModServicePrefs} from '../com/moderation/ModServicePrefs'
|
||||||
|
import * as Toast from 'view/com/util/Toast'
|
||||||
|
import {useSetTitle} from 'lib/hooks/useSetTitle'
|
||||||
|
import {Haptics} from 'lib/haptics'
|
||||||
|
import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
|
import {makeCustomFeedLink} from 'lib/routes/links'
|
||||||
|
import {pluralize} from 'lib/strings/helpers'
|
||||||
|
import {CenteredView, ScrollView} from 'view/com/util/Views'
|
||||||
|
import {NavigationProp} from 'lib/routes/types'
|
||||||
|
import {makeProfileLink} from 'lib/routes/links'
|
||||||
|
import {logger} from '#/logger'
|
||||||
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {useModalControls} from '#/state/modals'
|
||||||
|
import {useModServiceInfoQuery} from '#/state/queries/modservice'
|
||||||
|
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
|
||||||
|
import {
|
||||||
|
UsePreferencesQueryResponse,
|
||||||
|
usePreferencesQuery,
|
||||||
|
} from '#/state/queries/preferences'
|
||||||
|
import {useSession} from '#/state/session'
|
||||||
|
import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like'
|
||||||
|
import {ModServiceHeader} from '../com/moderation/ModServiceHeader'
|
||||||
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
|
import {ModServiceGuidelines} from '../com/moderation/ModServiceGuidelines'
|
||||||
|
|
||||||
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileModservice'>
|
||||||
|
export function ProfileModserviceScreen(props: Props) {
|
||||||
|
const {name: handleOrDid} = props.route.params
|
||||||
|
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const {_} = useLingui()
|
||||||
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
|
||||||
|
const {error, data: resolvedDid} = useResolveDidQuery(handleOrDid)
|
||||||
|
|
||||||
|
const onPressBack = React.useCallback(() => {
|
||||||
|
if (navigation.canGoBack()) {
|
||||||
|
navigation.goBack()
|
||||||
|
} else {
|
||||||
|
navigation.navigate('Home')
|
||||||
|
}
|
||||||
|
}, [navigation])
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<CenteredView>
|
||||||
|
<View style={[pal.view, pal.border, styles.notFoundContainer]}>
|
||||||
|
<Text type="title-lg" style={[pal.text, s.mb10]}>
|
||||||
|
<Trans>Could not load moderation service</Trans>
|
||||||
|
</Text>
|
||||||
|
<Text type="md" style={[pal.text, s.mb20]}>
|
||||||
|
{error.toString()}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<View style={{flexDirection: 'row'}}>
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
accessibilityLabel={_(msg`Go Back`)}
|
||||||
|
accessibilityHint="Return to previous page"
|
||||||
|
onPress={onPressBack}
|
||||||
|
style={{flexShrink: 1}}>
|
||||||
|
<Text type="button" style={pal.text}>
|
||||||
|
<Trans>Go Back</Trans>
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</CenteredView>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolvedDid ? (
|
||||||
|
<ProfileModservicecreenIntermediate modDid={resolvedDid} />
|
||||||
|
) : (
|
||||||
|
<CenteredView>
|
||||||
|
<View style={s.p20}>
|
||||||
|
<ActivityIndicator size="large" />
|
||||||
|
</View>
|
||||||
|
</CenteredView>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProfileModservicecreenIntermediate({modDid}: {modDid: string}) {
|
||||||
|
const {data: preferences} = usePreferencesQuery()
|
||||||
|
const {data: info} = useModServiceInfoQuery({did: modDid})
|
||||||
|
|
||||||
|
if (!preferences || !info) {
|
||||||
|
return (
|
||||||
|
<CenteredView>
|
||||||
|
<View style={s.p20}>
|
||||||
|
<ActivityIndicator size="large" />
|
||||||
|
</View>
|
||||||
|
</CenteredView>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProfileModserviceScreenInner preferences={preferences} modInfo={info} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProfileModserviceScreenInner({
|
||||||
|
preferences,
|
||||||
|
modInfo,
|
||||||
|
}: {
|
||||||
|
preferences: UsePreferencesQueryResponse
|
||||||
|
modInfo: AppBskyModerationDefs.ModServiceViewDetailed
|
||||||
|
}) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const {hasSession} = useSession()
|
||||||
|
const {track} = useAnalytics()
|
||||||
|
const {mutateAsync: likeMod, isPending: isLikePending} = useLikeMutation()
|
||||||
|
const {mutateAsync: unlikeMod, isPending: isUnlikePending} =
|
||||||
|
useUnlikeMutation()
|
||||||
|
const [likeUri, setLikeUri] = React.useState<string>(
|
||||||
|
modInfo.viewer?.like || '',
|
||||||
|
)
|
||||||
|
|
||||||
|
const isLiked = !!likeUri
|
||||||
|
const isSaved = false // TODO
|
||||||
|
// !removedFeed &&
|
||||||
|
// (!!savedFeed || preferences.feeds.saved.includes(feedInfo.uri))
|
||||||
|
const isEnabled = false // TODO
|
||||||
|
// !unpinnedFeed &&
|
||||||
|
// (!!pinnedFeed || preferences.feeds.pinned.includes(feedInfo.uri))
|
||||||
|
|
||||||
|
const descriptionRT = useMemo(
|
||||||
|
() =>
|
||||||
|
modInfo.description
|
||||||
|
? new RichTextAPI({
|
||||||
|
text: modInfo.description,
|
||||||
|
facets: modInfo.descriptionFacets,
|
||||||
|
})
|
||||||
|
: undefined,
|
||||||
|
[modInfo],
|
||||||
|
)
|
||||||
|
|
||||||
|
useSetTitle(modInfo.creator.displayName || modInfo.creator.handle)
|
||||||
|
|
||||||
|
// event handlers
|
||||||
|
//
|
||||||
|
const onToggleLiked = React.useCallback(async () => {
|
||||||
|
try {
|
||||||
|
Haptics.default()
|
||||||
|
|
||||||
|
if (isLiked && likeUri) {
|
||||||
|
await unlikeMod({uri: likeUri})
|
||||||
|
track('CustomFeed:Unlike')
|
||||||
|
setLikeUri('')
|
||||||
|
} else {
|
||||||
|
const res = await likeMod({uri: modInfo.uri, cid: modInfo.cid})
|
||||||
|
track('CustomFeed:Like')
|
||||||
|
setLikeUri(res.uri)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Toast.show(
|
||||||
|
_(
|
||||||
|
msg`There was an an issue contacting the server, please check your internet connection and try again.`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
logger.error('Failed up toggle like', {error: err})
|
||||||
|
}
|
||||||
|
}, [likeUri, isLiked, modInfo, likeMod, unlikeMod, track, _])
|
||||||
|
|
||||||
|
// render
|
||||||
|
// =
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={s.hContentRegion}>
|
||||||
|
<ScrollView
|
||||||
|
scrollEventThrottle={1}
|
||||||
|
contentContainerStyle={{
|
||||||
|
minHeight: Dimensions.get('window').height * 1.5,
|
||||||
|
}}>
|
||||||
|
<ModServiceHeader info={modInfo} />
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
borderTopWidth: 1,
|
||||||
|
paddingVertical: 20,
|
||||||
|
paddingHorizontal: 14,
|
||||||
|
gap: 12,
|
||||||
|
},
|
||||||
|
pal.border,
|
||||||
|
]}>
|
||||||
|
{descriptionRT ? (
|
||||||
|
<RichText
|
||||||
|
testID="modinfoDescription"
|
||||||
|
type="lg"
|
||||||
|
style={pal.text}
|
||||||
|
richText={descriptionRT}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text type="lg" style={[{fontStyle: 'italic'}, pal.textLight]}>
|
||||||
|
<Trans>No description</Trans>
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<Text type="lg" style={pal.textLight}>
|
||||||
|
<Trans>
|
||||||
|
Operated by{' '}
|
||||||
|
<TextLink
|
||||||
|
href={makeProfileLink(modInfo.creator)}
|
||||||
|
text={sanitizeHandle(modInfo.creator.handle, '@')}
|
||||||
|
style={pal.link}
|
||||||
|
/>
|
||||||
|
. Handles reports of anti-social behavior, illegal content,
|
||||||
|
unwanted sexual content, and misinformation.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<View style={{flexDirection: 'row', alignItems: 'center', gap: 10}}>
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
testID="toggleLikeBtn"
|
||||||
|
accessibilityLabel={_(msg`Like this feed`)}
|
||||||
|
accessibilityHint=""
|
||||||
|
disabled={!hasSession || isLikePending || isUnlikePending}
|
||||||
|
onPress={onToggleLiked}
|
||||||
|
style={{paddingHorizontal: 10}}>
|
||||||
|
{isLiked ? (
|
||||||
|
<HeartIconSolid size={19} style={s.likeColor} />
|
||||||
|
) : (
|
||||||
|
<HeartIcon strokeWidth={3} size={19} style={pal.textLight} />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
{typeof modInfo.likeCount === 'number' && (
|
||||||
|
<TextLink
|
||||||
|
href={'#todo'}
|
||||||
|
text={_(
|
||||||
|
msg`Liked by ${modInfo.likeCount} ${pluralize(
|
||||||
|
modInfo.likeCount,
|
||||||
|
'user',
|
||||||
|
)}`,
|
||||||
|
)}
|
||||||
|
style={[pal.textLight, s.semiBold]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<ModServicePrefs />
|
||||||
|
<View style={{height: 20}} />
|
||||||
|
<ModServiceGuidelines />
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
btn: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 6,
|
||||||
|
paddingVertical: 7,
|
||||||
|
paddingHorizontal: 14,
|
||||||
|
borderRadius: 50,
|
||||||
|
marginLeft: 6,
|
||||||
|
},
|
||||||
|
notFoundContainer: {
|
||||||
|
margin: 10,
|
||||||
|
paddingHorizontal: 18,
|
||||||
|
paddingVertical: 14,
|
||||||
|
borderRadius: 6,
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user