Give explicit names to MobX observer components (#1413)
* Consider observer(...) as components * Add display names to MobX observers * Temporarily suppress nested components * Suppress new false positives for react/prop-types
This commit is contained in:
parent
69209c988f
commit
8a93321fb1
72 changed files with 2868 additions and 2836 deletions
|
@ -17,160 +17,162 @@ import * as Toast from '../util/Toast'
|
|||
|
||||
export const snapPoints = ['90%']
|
||||
|
||||
export const Component = observer(({}: {}) => {
|
||||
const store = useStores()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const pal = usePalette('default')
|
||||
|
||||
React.useEffect(() => {
|
||||
store.preferences.sync()
|
||||
}, [store])
|
||||
|
||||
const onToggleAdultContent = React.useCallback(async () => {
|
||||
if (isIOS) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await store.preferences.setAdultContentEnabled(
|
||||
!store.preferences.adultContentEnabled,
|
||||
)
|
||||
} catch (e) {
|
||||
Toast.show('There was an issue syncing your preferences with the server')
|
||||
store.log.error('Failed to update preferences with server', {e})
|
||||
}
|
||||
}, [store])
|
||||
|
||||
const onPressDone = React.useCallback(() => {
|
||||
store.shell.closeModal()
|
||||
}, [store])
|
||||
|
||||
return (
|
||||
<View testID="contentFilteringModal" style={[pal.view, styles.container]}>
|
||||
<Text style={[pal.text, styles.title]}>Content Filtering</Text>
|
||||
<ScrollView style={styles.scrollContainer}>
|
||||
<View style={s.mb10}>
|
||||
{isIOS ? (
|
||||
store.preferences.adultContentEnabled ? null : (
|
||||
<Text type="md" style={pal.textLight}>
|
||||
Adult content can only be enabled via the Web at{' '}
|
||||
<TextLink
|
||||
style={pal.link}
|
||||
href="https://bsky.app"
|
||||
text="bsky.app"
|
||||
/>
|
||||
.
|
||||
</Text>
|
||||
)
|
||||
) : (
|
||||
<ToggleButton
|
||||
type="default-light"
|
||||
label="Enable Adult Content"
|
||||
isSelected={store.preferences.adultContentEnabled}
|
||||
onPress={onToggleAdultContent}
|
||||
style={styles.toggleBtn}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<ContentLabelPref
|
||||
group="nsfw"
|
||||
disabled={!store.preferences.adultContentEnabled}
|
||||
/>
|
||||
<ContentLabelPref
|
||||
group="nudity"
|
||||
disabled={!store.preferences.adultContentEnabled}
|
||||
/>
|
||||
<ContentLabelPref
|
||||
group="suggestive"
|
||||
disabled={!store.preferences.adultContentEnabled}
|
||||
/>
|
||||
<ContentLabelPref
|
||||
group="gore"
|
||||
disabled={!store.preferences.adultContentEnabled}
|
||||
/>
|
||||
<ContentLabelPref group="hate" />
|
||||
<ContentLabelPref group="spam" />
|
||||
<ContentLabelPref group="impersonation" />
|
||||
<View style={{height: isMobile ? 60 : 0}} />
|
||||
</ScrollView>
|
||||
<View
|
||||
style={[
|
||||
styles.btnContainer,
|
||||
isMobile && styles.btnContainerMobile,
|
||||
pal.borderDark,
|
||||
]}>
|
||||
<Pressable
|
||||
testID="sendReportBtn"
|
||||
onPress={onPressDone}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Done"
|
||||
accessibilityHint="">
|
||||
<LinearGradient
|
||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||
start={{x: 0, y: 0}}
|
||||
end={{x: 1, y: 1}}
|
||||
style={[styles.btn]}>
|
||||
<Text style={[s.white, s.bold, s.f18]}>Done</Text>
|
||||
</LinearGradient>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
})
|
||||
|
||||
// TODO: Refactor this component to pass labels down to each tab
|
||||
const ContentLabelPref = observer(
|
||||
({
|
||||
group,
|
||||
disabled,
|
||||
}: {
|
||||
group: keyof typeof CONFIGURABLE_LABEL_GROUPS
|
||||
disabled?: boolean
|
||||
}) => {
|
||||
export const Component = observer(
|
||||
function ContentFilteringSettingsImpl({}: {}) {
|
||||
const store = useStores()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const pal = usePalette('default')
|
||||
|
||||
const onChange = React.useCallback(
|
||||
async (v: LabelPreference) => {
|
||||
try {
|
||||
await store.preferences.setContentLabelPref(group, v)
|
||||
} catch (e) {
|
||||
Toast.show(
|
||||
'There was an issue syncing your preferences with the server',
|
||||
)
|
||||
store.log.error('Failed to update preferences with server', {e})
|
||||
}
|
||||
},
|
||||
[store, group],
|
||||
)
|
||||
React.useEffect(() => {
|
||||
store.preferences.sync()
|
||||
}, [store])
|
||||
|
||||
const onToggleAdultContent = React.useCallback(async () => {
|
||||
if (isIOS) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await store.preferences.setAdultContentEnabled(
|
||||
!store.preferences.adultContentEnabled,
|
||||
)
|
||||
} catch (e) {
|
||||
Toast.show(
|
||||
'There was an issue syncing your preferences with the server',
|
||||
)
|
||||
store.log.error('Failed to update preferences with server', {e})
|
||||
}
|
||||
}, [store])
|
||||
|
||||
const onPressDone = React.useCallback(() => {
|
||||
store.shell.closeModal()
|
||||
}, [store])
|
||||
|
||||
return (
|
||||
<View style={[styles.contentLabelPref, pal.border]}>
|
||||
<View style={s.flex1}>
|
||||
<Text type="md-medium" style={[pal.text]}>
|
||||
{CONFIGURABLE_LABEL_GROUPS[group].title}
|
||||
</Text>
|
||||
{typeof CONFIGURABLE_LABEL_GROUPS[group].subtitle === 'string' && (
|
||||
<Text type="sm" style={[pal.textLight]}>
|
||||
{CONFIGURABLE_LABEL_GROUPS[group].subtitle}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
{disabled ? (
|
||||
<Text type="sm-bold" style={pal.textLight}>
|
||||
Hide
|
||||
</Text>
|
||||
) : (
|
||||
<SelectGroup
|
||||
current={store.preferences.contentLabels[group]}
|
||||
onChange={onChange}
|
||||
group={group}
|
||||
<View testID="contentFilteringModal" style={[pal.view, styles.container]}>
|
||||
<Text style={[pal.text, styles.title]}>Content Filtering</Text>
|
||||
<ScrollView style={styles.scrollContainer}>
|
||||
<View style={s.mb10}>
|
||||
{isIOS ? (
|
||||
store.preferences.adultContentEnabled ? null : (
|
||||
<Text type="md" style={pal.textLight}>
|
||||
Adult content can only be enabled via the Web at{' '}
|
||||
<TextLink
|
||||
style={pal.link}
|
||||
href="https://bsky.app"
|
||||
text="bsky.app"
|
||||
/>
|
||||
.
|
||||
</Text>
|
||||
)
|
||||
) : (
|
||||
<ToggleButton
|
||||
type="default-light"
|
||||
label="Enable Adult Content"
|
||||
isSelected={store.preferences.adultContentEnabled}
|
||||
onPress={onToggleAdultContent}
|
||||
style={styles.toggleBtn}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<ContentLabelPref
|
||||
group="nsfw"
|
||||
disabled={!store.preferences.adultContentEnabled}
|
||||
/>
|
||||
)}
|
||||
<ContentLabelPref
|
||||
group="nudity"
|
||||
disabled={!store.preferences.adultContentEnabled}
|
||||
/>
|
||||
<ContentLabelPref
|
||||
group="suggestive"
|
||||
disabled={!store.preferences.adultContentEnabled}
|
||||
/>
|
||||
<ContentLabelPref
|
||||
group="gore"
|
||||
disabled={!store.preferences.adultContentEnabled}
|
||||
/>
|
||||
<ContentLabelPref group="hate" />
|
||||
<ContentLabelPref group="spam" />
|
||||
<ContentLabelPref group="impersonation" />
|
||||
<View style={{height: isMobile ? 60 : 0}} />
|
||||
</ScrollView>
|
||||
<View
|
||||
style={[
|
||||
styles.btnContainer,
|
||||
isMobile && styles.btnContainerMobile,
|
||||
pal.borderDark,
|
||||
]}>
|
||||
<Pressable
|
||||
testID="sendReportBtn"
|
||||
onPress={onPressDone}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Done"
|
||||
accessibilityHint="">
|
||||
<LinearGradient
|
||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
||||
start={{x: 0, y: 0}}
|
||||
end={{x: 1, y: 1}}
|
||||
style={[styles.btn]}>
|
||||
<Text style={[s.white, s.bold, s.f18]}>Done</Text>
|
||||
</LinearGradient>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
// TODO: Refactor this component to pass labels down to each tab
|
||||
const ContentLabelPref = observer(function ContentLabelPrefImpl({
|
||||
group,
|
||||
disabled,
|
||||
}: {
|
||||
group: keyof typeof CONFIGURABLE_LABEL_GROUPS
|
||||
disabled?: boolean
|
||||
}) {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
|
||||
const onChange = React.useCallback(
|
||||
async (v: LabelPreference) => {
|
||||
try {
|
||||
await store.preferences.setContentLabelPref(group, v)
|
||||
} catch (e) {
|
||||
Toast.show(
|
||||
'There was an issue syncing your preferences with the server',
|
||||
)
|
||||
store.log.error('Failed to update preferences with server', {e})
|
||||
}
|
||||
},
|
||||
[store, group],
|
||||
)
|
||||
|
||||
return (
|
||||
<View style={[styles.contentLabelPref, pal.border]}>
|
||||
<View style={s.flex1}>
|
||||
<Text type="md-medium" style={[pal.text]}>
|
||||
{CONFIGURABLE_LABEL_GROUPS[group].title}
|
||||
</Text>
|
||||
{typeof CONFIGURABLE_LABEL_GROUPS[group].subtitle === 'string' && (
|
||||
<Text type="sm" style={[pal.textLight]}>
|
||||
{CONFIGURABLE_LABEL_GROUPS[group].subtitle}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
{disabled ? (
|
||||
<Text type="sm-bold" style={pal.textLight}>
|
||||
Hide
|
||||
</Text>
|
||||
) : (
|
||||
<SelectGroup
|
||||
current={store.preferences.contentLabels[group]}
|
||||
onChange={onChange}
|
||||
group={group}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
})
|
||||
|
||||
interface SelectGroupProps {
|
||||
current: LabelPreference
|
||||
onChange: (v: LabelPreference) => void
|
||||
|
|
|
@ -46,7 +46,10 @@ interface Props {
|
|||
gallery: GalleryModel
|
||||
}
|
||||
|
||||
export const Component = observer(function ({image, gallery}: Props) {
|
||||
export const Component = observer(function EditImageImpl({
|
||||
image,
|
||||
gallery,
|
||||
}: Props) {
|
||||
const pal = usePalette('default')
|
||||
const theme = useTheme()
|
||||
const store = useStores()
|
||||
|
|
|
@ -79,50 +79,56 @@ export function Component({}: {}) {
|
|||
)
|
||||
}
|
||||
|
||||
const InviteCode = observer(
|
||||
({testID, code, used}: {testID: string; code: string; used?: boolean}) => {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const {invitesAvailable} = store.me
|
||||
const InviteCode = observer(function InviteCodeImpl({
|
||||
testID,
|
||||
code,
|
||||
used,
|
||||
}: {
|
||||
testID: string
|
||||
code: string
|
||||
used?: boolean
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const {invitesAvailable} = store.me
|
||||
|
||||
const onPress = React.useCallback(() => {
|
||||
Clipboard.setString(code)
|
||||
Toast.show('Copied to clipboard')
|
||||
store.invitedUsers.setInviteCopied(code)
|
||||
}, [store, code])
|
||||
const onPress = React.useCallback(() => {
|
||||
Clipboard.setString(code)
|
||||
Toast.show('Copied to clipboard')
|
||||
store.invitedUsers.setInviteCopied(code)
|
||||
}, [store, code])
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
testID={testID}
|
||||
style={[styles.inviteCode, pal.border]}
|
||||
onPress={onPress}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={
|
||||
invitesAvailable === 1
|
||||
? 'Invite codes: 1 available'
|
||||
: `Invite codes: ${invitesAvailable} available`
|
||||
}
|
||||
accessibilityHint="Opens list of invite codes">
|
||||
<Text
|
||||
testID={`${testID}-code`}
|
||||
type={used ? 'md' : 'md-bold'}
|
||||
style={used ? [pal.textLight, styles.strikeThrough] : pal.text}>
|
||||
{code}
|
||||
</Text>
|
||||
<View style={styles.flex1} />
|
||||
{!used && store.invitedUsers.isInviteCopied(code) && (
|
||||
<Text style={[pal.textLight, styles.codeCopied]}>Copied</Text>
|
||||
)}
|
||||
{!used && (
|
||||
<FontAwesomeIcon
|
||||
icon={['far', 'clone']}
|
||||
style={pal.text as FontAwesomeIconStyle}
|
||||
/>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
)
|
||||
},
|
||||
)
|
||||
return (
|
||||
<TouchableOpacity
|
||||
testID={testID}
|
||||
style={[styles.inviteCode, pal.border]}
|
||||
onPress={onPress}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={
|
||||
invitesAvailable === 1
|
||||
? 'Invite codes: 1 available'
|
||||
: `Invite codes: ${invitesAvailable} available`
|
||||
}
|
||||
accessibilityHint="Opens list of invite codes">
|
||||
<Text
|
||||
testID={`${testID}-code`}
|
||||
type={used ? 'md' : 'md-bold'}
|
||||
style={used ? [pal.textLight, styles.strikeThrough] : pal.text}>
|
||||
{code}
|
||||
</Text>
|
||||
<View style={styles.flex1} />
|
||||
{!used && store.invitedUsers.isInviteCopied(code) && (
|
||||
<Text style={[pal.textLight, styles.codeCopied]}>Copied</Text>
|
||||
)}
|
||||
{!used && (
|
||||
<FontAwesomeIcon
|
||||
icon={['far', 'clone']}
|
||||
style={pal.text as FontAwesomeIconStyle}
|
||||
/>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
)
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
|
|
|
@ -24,210 +24,207 @@ import isEqual from 'lodash.isequal'
|
|||
|
||||
export const snapPoints = ['fullscreen']
|
||||
|
||||
export const Component = observer(
|
||||
({
|
||||
subject,
|
||||
displayName,
|
||||
onUpdate,
|
||||
}: {
|
||||
subject: string
|
||||
displayName: string
|
||||
onUpdate?: () => void
|
||||
}) => {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
const palPrimary = usePalette('primary')
|
||||
const palInverted = usePalette('inverted')
|
||||
const [originalSelections, setOriginalSelections] = React.useState<
|
||||
string[]
|
||||
>([])
|
||||
const [selected, setSelected] = React.useState<string[]>([])
|
||||
const [membershipsLoaded, setMembershipsLoaded] = React.useState(false)
|
||||
export const Component = observer(function ListAddRemoveUserImpl({
|
||||
subject,
|
||||
displayName,
|
||||
onUpdate,
|
||||
}: {
|
||||
subject: string
|
||||
displayName: string
|
||||
onUpdate?: () => void
|
||||
}) {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
const palPrimary = usePalette('primary')
|
||||
const palInverted = usePalette('inverted')
|
||||
const [originalSelections, setOriginalSelections] = React.useState<string[]>(
|
||||
[],
|
||||
)
|
||||
const [selected, setSelected] = React.useState<string[]>([])
|
||||
const [membershipsLoaded, setMembershipsLoaded] = React.useState(false)
|
||||
|
||||
const listsList: ListsListModel = React.useMemo(
|
||||
() => new ListsListModel(store, store.me.did),
|
||||
[store],
|
||||
const listsList: ListsListModel = React.useMemo(
|
||||
() => new ListsListModel(store, store.me.did),
|
||||
[store],
|
||||
)
|
||||
const memberships: ListMembershipModel = React.useMemo(
|
||||
() => new ListMembershipModel(store, subject),
|
||||
[store, subject],
|
||||
)
|
||||
React.useEffect(() => {
|
||||
listsList.refresh()
|
||||
memberships.fetch().then(
|
||||
() => {
|
||||
const ids = memberships.memberships.map(m => m.value.list)
|
||||
setOriginalSelections(ids)
|
||||
setSelected(ids)
|
||||
setMembershipsLoaded(true)
|
||||
},
|
||||
err => {
|
||||
store.log.error('Failed to fetch memberships', {err})
|
||||
},
|
||||
)
|
||||
const memberships: ListMembershipModel = React.useMemo(
|
||||
() => new ListMembershipModel(store, subject),
|
||||
[store, subject],
|
||||
)
|
||||
React.useEffect(() => {
|
||||
listsList.refresh()
|
||||
memberships.fetch().then(
|
||||
() => {
|
||||
const ids = memberships.memberships.map(m => m.value.list)
|
||||
setOriginalSelections(ids)
|
||||
setSelected(ids)
|
||||
setMembershipsLoaded(true)
|
||||
},
|
||||
err => {
|
||||
store.log.error('Failed to fetch memberships', {err})
|
||||
},
|
||||
)
|
||||
}, [memberships, listsList, store, setSelected, setMembershipsLoaded])
|
||||
}, [memberships, listsList, store, setSelected, setMembershipsLoaded])
|
||||
|
||||
const onPressCancel = useCallback(() => {
|
||||
store.shell.closeModal()
|
||||
}, [store])
|
||||
const onPressCancel = useCallback(() => {
|
||||
store.shell.closeModal()
|
||||
}, [store])
|
||||
|
||||
const onPressSave = useCallback(async () => {
|
||||
try {
|
||||
await memberships.updateTo(selected)
|
||||
} catch (err) {
|
||||
store.log.error('Failed to update memberships', {err})
|
||||
return
|
||||
const onPressSave = useCallback(async () => {
|
||||
try {
|
||||
await memberships.updateTo(selected)
|
||||
} catch (err) {
|
||||
store.log.error('Failed to update memberships', {err})
|
||||
return
|
||||
}
|
||||
Toast.show('Lists updated')
|
||||
onUpdate?.()
|
||||
store.shell.closeModal()
|
||||
}, [store, selected, memberships, onUpdate])
|
||||
|
||||
const onPressNewMuteList = useCallback(() => {
|
||||
store.shell.openModal({
|
||||
name: 'create-or-edit-mute-list',
|
||||
onSave: (_uri: string) => {
|
||||
listsList.refresh()
|
||||
},
|
||||
})
|
||||
}, [store, listsList])
|
||||
|
||||
const onToggleSelected = useCallback(
|
||||
(uri: string) => {
|
||||
if (selected.includes(uri)) {
|
||||
setSelected(selected.filter(uri2 => uri2 !== uri))
|
||||
} else {
|
||||
setSelected([...selected, uri])
|
||||
}
|
||||
Toast.show('Lists updated')
|
||||
onUpdate?.()
|
||||
store.shell.closeModal()
|
||||
}, [store, selected, memberships, onUpdate])
|
||||
},
|
||||
[selected, setSelected],
|
||||
)
|
||||
|
||||
const onPressNewMuteList = useCallback(() => {
|
||||
store.shell.openModal({
|
||||
name: 'create-or-edit-mute-list',
|
||||
onSave: (_uri: string) => {
|
||||
listsList.refresh()
|
||||
},
|
||||
})
|
||||
}, [store, listsList])
|
||||
|
||||
const onToggleSelected = useCallback(
|
||||
(uri: string) => {
|
||||
if (selected.includes(uri)) {
|
||||
setSelected(selected.filter(uri2 => uri2 !== uri))
|
||||
} else {
|
||||
setSelected([...selected, uri])
|
||||
}
|
||||
},
|
||||
[selected, setSelected],
|
||||
)
|
||||
|
||||
const renderItem = useCallback(
|
||||
(list: GraphDefs.ListView) => {
|
||||
const isSelected = selected.includes(list.uri)
|
||||
return (
|
||||
<Pressable
|
||||
testID={`toggleBtn-${list.name}`}
|
||||
style={[
|
||||
styles.listItem,
|
||||
pal.border,
|
||||
{opacity: membershipsLoaded ? 1 : 0.5},
|
||||
]}
|
||||
accessibilityLabel={`${isSelected ? 'Remove from' : 'Add to'} ${
|
||||
list.name
|
||||
}`}
|
||||
accessibilityHint=""
|
||||
disabled={!membershipsLoaded}
|
||||
onPress={() => onToggleSelected(list.uri)}>
|
||||
<View style={styles.listItemAvi}>
|
||||
<UserAvatar size={40} avatar={list.avatar} />
|
||||
</View>
|
||||
<View style={styles.listItemContent}>
|
||||
<Text
|
||||
type="lg"
|
||||
style={[s.bold, pal.text]}
|
||||
numberOfLines={1}
|
||||
lineHeight={1.2}>
|
||||
{sanitizeDisplayName(list.name)}
|
||||
</Text>
|
||||
<Text type="md" style={[pal.textLight]} numberOfLines={1}>
|
||||
{list.purpose === 'app.bsky.graph.defs#modlist' && 'Mute list'}{' '}
|
||||
by{' '}
|
||||
{list.creator.did === store.me.did
|
||||
? 'you'
|
||||
: sanitizeHandle(list.creator.handle, '@')}
|
||||
</Text>
|
||||
</View>
|
||||
{membershipsLoaded && (
|
||||
<View
|
||||
style={
|
||||
isSelected
|
||||
? [styles.checkbox, palPrimary.border, palPrimary.view]
|
||||
: [styles.checkbox, pal.borderDark]
|
||||
}>
|
||||
{isSelected && (
|
||||
<FontAwesomeIcon
|
||||
icon="check"
|
||||
style={palInverted.text as FontAwesomeIconStyle}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</Pressable>
|
||||
)
|
||||
},
|
||||
[
|
||||
pal,
|
||||
palPrimary,
|
||||
palInverted,
|
||||
onToggleSelected,
|
||||
selected,
|
||||
store.me.did,
|
||||
membershipsLoaded,
|
||||
],
|
||||
)
|
||||
|
||||
const renderEmptyState = React.useCallback(() => {
|
||||
const renderItem = useCallback(
|
||||
(list: GraphDefs.ListView) => {
|
||||
const isSelected = selected.includes(list.uri)
|
||||
return (
|
||||
<EmptyStateWithButton
|
||||
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])
|
||||
|
||||
// Only show changes button if there are some items on the list to choose from AND user has made changes in selection
|
||||
const canSaveChanges =
|
||||
!listsList.isEmpty && !isEqual(selected, originalSelections)
|
||||
|
||||
return (
|
||||
<View testID="listAddRemoveUserModal" style={s.hContentRegion}>
|
||||
<Text style={[styles.title, pal.text]}>Add {displayName} to Lists</Text>
|
||||
<ListsList
|
||||
listsList={listsList}
|
||||
showAddBtns
|
||||
onPressCreateNew={onPressNewMuteList}
|
||||
renderItem={renderItem}
|
||||
renderEmptyState={renderEmptyState}
|
||||
style={[styles.list, pal.border]}
|
||||
/>
|
||||
<View style={[styles.btns, pal.border]}>
|
||||
<Button
|
||||
testID="cancelBtn"
|
||||
type="default"
|
||||
onPress={onPressCancel}
|
||||
style={styles.footerBtn}
|
||||
accessibilityLabel="Cancel"
|
||||
accessibilityHint=""
|
||||
onAccessibilityEscape={onPressCancel}
|
||||
label="Cancel"
|
||||
/>
|
||||
{canSaveChanges && (
|
||||
<Button
|
||||
testID="saveBtn"
|
||||
type="primary"
|
||||
onPress={onPressSave}
|
||||
style={styles.footerBtn}
|
||||
accessibilityLabel="Save changes"
|
||||
accessibilityHint=""
|
||||
onAccessibilityEscape={onPressSave}
|
||||
label="Save Changes"
|
||||
/>
|
||||
)}
|
||||
|
||||
{(listsList.isLoading || !membershipsLoaded) && (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator />
|
||||
<Pressable
|
||||
testID={`toggleBtn-${list.name}`}
|
||||
style={[
|
||||
styles.listItem,
|
||||
pal.border,
|
||||
{opacity: membershipsLoaded ? 1 : 0.5},
|
||||
]}
|
||||
accessibilityLabel={`${isSelected ? 'Remove from' : 'Add to'} ${
|
||||
list.name
|
||||
}`}
|
||||
accessibilityHint=""
|
||||
disabled={!membershipsLoaded}
|
||||
onPress={() => onToggleSelected(list.uri)}>
|
||||
<View style={styles.listItemAvi}>
|
||||
<UserAvatar size={40} avatar={list.avatar} />
|
||||
</View>
|
||||
<View style={styles.listItemContent}>
|
||||
<Text
|
||||
type="lg"
|
||||
style={[s.bold, pal.text]}
|
||||
numberOfLines={1}
|
||||
lineHeight={1.2}>
|
||||
{sanitizeDisplayName(list.name)}
|
||||
</Text>
|
||||
<Text type="md" style={[pal.textLight]} numberOfLines={1}>
|
||||
{list.purpose === 'app.bsky.graph.defs#modlist' && 'Mute list'} by{' '}
|
||||
{list.creator.did === store.me.did
|
||||
? 'you'
|
||||
: sanitizeHandle(list.creator.handle, '@')}
|
||||
</Text>
|
||||
</View>
|
||||
{membershipsLoaded && (
|
||||
<View
|
||||
style={
|
||||
isSelected
|
||||
? [styles.checkbox, palPrimary.border, palPrimary.view]
|
||||
: [styles.checkbox, pal.borderDark]
|
||||
}>
|
||||
{isSelected && (
|
||||
<FontAwesomeIcon
|
||||
icon="check"
|
||||
style={palInverted.text as FontAwesomeIconStyle}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</Pressable>
|
||||
)
|
||||
},
|
||||
[
|
||||
pal,
|
||||
palPrimary,
|
||||
palInverted,
|
||||
onToggleSelected,
|
||||
selected,
|
||||
store.me.did,
|
||||
membershipsLoaded,
|
||||
],
|
||||
)
|
||||
|
||||
const renderEmptyState = React.useCallback(() => {
|
||||
return (
|
||||
<EmptyStateWithButton
|
||||
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])
|
||||
|
||||
// Only show changes button if there are some items on the list to choose from AND user has made changes in selection
|
||||
const canSaveChanges =
|
||||
!listsList.isEmpty && !isEqual(selected, originalSelections)
|
||||
|
||||
return (
|
||||
<View testID="listAddRemoveUserModal" style={s.hContentRegion}>
|
||||
<Text style={[styles.title, pal.text]}>Add {displayName} to Lists</Text>
|
||||
<ListsList
|
||||
listsList={listsList}
|
||||
showAddBtns
|
||||
onPressCreateNew={onPressNewMuteList}
|
||||
renderItem={renderItem}
|
||||
renderEmptyState={renderEmptyState}
|
||||
style={[styles.list, pal.border]}
|
||||
/>
|
||||
<View style={[styles.btns, pal.border]}>
|
||||
<Button
|
||||
testID="cancelBtn"
|
||||
type="default"
|
||||
onPress={onPressCancel}
|
||||
style={styles.footerBtn}
|
||||
accessibilityLabel="Cancel"
|
||||
accessibilityHint=""
|
||||
onAccessibilityEscape={onPressCancel}
|
||||
label="Cancel"
|
||||
/>
|
||||
{canSaveChanges && (
|
||||
<Button
|
||||
testID="saveBtn"
|
||||
type="primary"
|
||||
onPress={onPressSave}
|
||||
style={styles.footerBtn}
|
||||
accessibilityLabel="Save changes"
|
||||
accessibilityHint=""
|
||||
onAccessibilityEscape={onPressSave}
|
||||
label="Save Changes"
|
||||
/>
|
||||
)}
|
||||
|
||||
{(listsList.isLoading || !membershipsLoaded) && (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
|
|
|
@ -14,7 +14,11 @@ import {s} from 'lib/styles'
|
|||
|
||||
export const snapPoints = [520, '100%']
|
||||
|
||||
export const Component = observer(({did}: {did: string}) => {
|
||||
export const Component = observer(function ProfilePreviewImpl({
|
||||
did,
|
||||
}: {
|
||||
did: string
|
||||
}) {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
const [model] = useState(new ProfileModel(store, {actor: did}))
|
||||
|
|
|
@ -5,43 +5,41 @@ import {observer} from 'mobx-react-lite'
|
|||
import {ToggleButton} from 'view/com/util/forms/ToggleButton'
|
||||
import {useStores} from 'state/index'
|
||||
|
||||
export const LanguageToggle = observer(
|
||||
({
|
||||
code2,
|
||||
name,
|
||||
onPress,
|
||||
langType,
|
||||
}: {
|
||||
code2: string
|
||||
name: string
|
||||
onPress: () => void
|
||||
langType: 'contentLanguages' | 'postLanguages'
|
||||
}) => {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
export const LanguageToggle = observer(function LanguageToggleImpl({
|
||||
code2,
|
||||
name,
|
||||
onPress,
|
||||
langType,
|
||||
}: {
|
||||
code2: string
|
||||
name: string
|
||||
onPress: () => void
|
||||
langType: 'contentLanguages' | 'postLanguages'
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
|
||||
const isSelected = store.preferences[langType].includes(code2)
|
||||
const isSelected = store.preferences[langType].includes(code2)
|
||||
|
||||
// enforce a max of 3 selections for post languages
|
||||
let isDisabled = false
|
||||
if (
|
||||
langType === 'postLanguages' &&
|
||||
store.preferences[langType].length >= 3 &&
|
||||
!isSelected
|
||||
) {
|
||||
isDisabled = true
|
||||
}
|
||||
// enforce a max of 3 selections for post languages
|
||||
let isDisabled = false
|
||||
if (
|
||||
langType === 'postLanguages' &&
|
||||
store.preferences[langType].length >= 3 &&
|
||||
!isSelected
|
||||
) {
|
||||
isDisabled = true
|
||||
}
|
||||
|
||||
return (
|
||||
<ToggleButton
|
||||
label={name}
|
||||
isSelected={isSelected}
|
||||
onPress={isDisabled ? undefined : onPress}
|
||||
style={[pal.border, styles.languageToggle, isDisabled && styles.dimmed]}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
||||
return (
|
||||
<ToggleButton
|
||||
label={name}
|
||||
isSelected={isSelected}
|
||||
onPress={isDisabled ? undefined : onPress}
|
||||
style={[pal.border, styles.languageToggle, isDisabled && styles.dimmed]}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
languageToggle: {
|
||||
|
|
|
@ -13,7 +13,7 @@ import {ToggleButton} from 'view/com/util/forms/ToggleButton'
|
|||
|
||||
export const snapPoints = ['100%']
|
||||
|
||||
export const Component = observer(() => {
|
||||
export const Component = observer(function PostLanguagesSettingsImpl() {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue