mirror of
https://github.com/tooot-app/app
synced 2025-06-05 22:19:13 +02:00
Rewrite all buttons
This commit is contained in:
@ -10,7 +10,7 @@ import {
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { ButtonRow } from '@components/Button'
|
||||
import Button from '@components/Button'
|
||||
|
||||
export interface Props {
|
||||
children: React.ReactNode
|
||||
@ -84,12 +84,12 @@ const BottomSheet: React.FC<Props> = ({ children, visible, handleDismiss }) => {
|
||||
style={[styles.handle, { backgroundColor: theme.background }]}
|
||||
/>
|
||||
{children}
|
||||
<View style={styles.button}>
|
||||
<ButtonRow
|
||||
onPress={() => closeModal.start(() => handleDismiss())}
|
||||
text='取消'
|
||||
/>
|
||||
</View>
|
||||
<Button
|
||||
type='text'
|
||||
content='取消'
|
||||
onPress={() => closeModal.start(() => handleDismiss())}
|
||||
style={styles.button}
|
||||
/>
|
||||
</Animated.View>
|
||||
</View>
|
||||
</Modal>
|
||||
@ -112,8 +112,7 @@ const styles = StyleSheet.create({
|
||||
top: -StyleConstants.Spacing.M * 2
|
||||
},
|
||||
button: {
|
||||
paddingLeft: StyleConstants.Spacing.Global.PagePadding * 2,
|
||||
paddingRight: StyleConstants.Spacing.Global.PagePadding * 2
|
||||
marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -1,4 +1,166 @@
|
||||
import ButtonRound from '@components/Button/ButtonRound'
|
||||
import ButtonRow from '@components/Button/ButtonRow'
|
||||
import { Feather } from '@expo/vector-icons'
|
||||
import React, { useLayoutEffect, useMemo } from 'react'
|
||||
import {
|
||||
Pressable,
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
ViewStyle
|
||||
} from 'react-native'
|
||||
import { Chase } from 'react-native-animated-spinkit'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import layoutAnimation from '@root/utils/styles/layoutAnimation'
|
||||
|
||||
export { ButtonRound, ButtonRow }
|
||||
export interface Props {
|
||||
style?: StyleProp<ViewStyle>
|
||||
|
||||
type: 'icon' | 'text'
|
||||
content: string
|
||||
|
||||
loading?: boolean
|
||||
destructive?: boolean
|
||||
disabled?: boolean
|
||||
|
||||
size?: 'S' | 'M' | 'L'
|
||||
spacing?: 'XS' | 'S' | 'M' | 'L'
|
||||
round?: boolean
|
||||
overlay?: boolean
|
||||
|
||||
onPress: () => void
|
||||
}
|
||||
|
||||
const Button: React.FC<Props> = ({
|
||||
style: customStyle,
|
||||
type,
|
||||
content,
|
||||
loading = false,
|
||||
destructive = false,
|
||||
disabled = false,
|
||||
size = 'M',
|
||||
spacing = 'S',
|
||||
round = false,
|
||||
overlay = false,
|
||||
onPress
|
||||
}) => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
useLayoutEffect(() => layoutAnimation(), [loading, disabled])
|
||||
|
||||
const loadingSpinkit = useMemo(
|
||||
() => (
|
||||
<View style={{ position: 'absolute' }}>
|
||||
<Chase size={StyleConstants.Font.Size[size]} color={theme.secondary} />
|
||||
</View>
|
||||
),
|
||||
[theme]
|
||||
)
|
||||
|
||||
const colorContent = useMemo(() => {
|
||||
if (overlay) {
|
||||
return theme.primaryOverlay
|
||||
} else {
|
||||
if (disabled) {
|
||||
return theme.secondary
|
||||
} else {
|
||||
if (destructive) {
|
||||
return theme.red
|
||||
} else {
|
||||
return theme.primary
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [theme, disabled])
|
||||
|
||||
const children = useMemo(() => {
|
||||
switch (type) {
|
||||
case 'icon':
|
||||
return (
|
||||
<>
|
||||
<Feather
|
||||
name={content as any}
|
||||
size={StyleConstants.Font.Size[size] * (size === 'M' ? 1 : 1.5)}
|
||||
color={colorContent}
|
||||
style={{ opacity: loading ? 0 : 1 }}
|
||||
/>
|
||||
{loading && loadingSpinkit}
|
||||
</>
|
||||
)
|
||||
case 'text':
|
||||
return (
|
||||
<>
|
||||
<Text
|
||||
style={{
|
||||
color: colorContent,
|
||||
fontSize:
|
||||
StyleConstants.Font.Size[size] * (size === 'M' ? 1 : 1.5),
|
||||
fontWeight: destructive
|
||||
? StyleConstants.Font.Weight.Bold
|
||||
: undefined,
|
||||
opacity: loading ? 0 : 1
|
||||
}}
|
||||
children={content}
|
||||
/>
|
||||
{loading && loadingSpinkit}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}, [theme, content, loading, disabled])
|
||||
|
||||
const colorBorder = useMemo(() => {
|
||||
if (disabled || loading) {
|
||||
return theme.secondary
|
||||
} else {
|
||||
if (destructive) {
|
||||
return theme.red
|
||||
} else {
|
||||
return theme.primary
|
||||
}
|
||||
}
|
||||
}, [theme, loading, disabled])
|
||||
const colorBackground = useMemo(() => {
|
||||
if (overlay) {
|
||||
return theme.backgroundOverlay
|
||||
} else {
|
||||
return theme.background
|
||||
}
|
||||
}, [theme])
|
||||
|
||||
enum spacingMapping {
|
||||
XS = 'S',
|
||||
S = 'M',
|
||||
M = 'L',
|
||||
L = 'XL'
|
||||
}
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
{...(!disabled && !loading && { onPress })}
|
||||
children={children}
|
||||
style={[
|
||||
styles.button,
|
||||
{
|
||||
borderWidth: overlay ? 0 : 1,
|
||||
borderColor: colorBorder,
|
||||
backgroundColor: colorBackground,
|
||||
paddingVertical: StyleConstants.Spacing[spacing],
|
||||
paddingHorizontal:
|
||||
StyleConstants.Spacing[round ? spacing : spacingMapping[spacing]]
|
||||
},
|
||||
customStyle
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
borderRadius: 100,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
}
|
||||
})
|
||||
|
||||
export default Button
|
||||
|
@ -1,60 +0,0 @@
|
||||
import { Feather } from '@expo/vector-icons'
|
||||
import React from 'react'
|
||||
import { Pressable, StyleSheet } from 'react-native'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
|
||||
export interface Props {
|
||||
styles: any
|
||||
onPress: () => void
|
||||
icon: any
|
||||
size?: 'S' | 'M' | 'L'
|
||||
coordinate?: 'center' | 'default'
|
||||
}
|
||||
|
||||
const ButtomRound: React.FC<Props> = ({
|
||||
styles: extraStyles,
|
||||
onPress,
|
||||
icon,
|
||||
size = 'M',
|
||||
coordinate = 'default'
|
||||
}) => {
|
||||
const { theme } = useTheme()
|
||||
const dimension =
|
||||
StyleConstants.Spacing.S * 1.5 + StyleConstants.Font.Size[size]
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={[
|
||||
styles.base,
|
||||
extraStyles,
|
||||
{
|
||||
backgroundColor: theme.backgroundOverlay,
|
||||
...(coordinate === 'center' && {
|
||||
transform: [
|
||||
{ translateX: -dimension / 2 },
|
||||
{ translateY: -dimension / 2 }
|
||||
]
|
||||
})
|
||||
}
|
||||
]}
|
||||
onPress={onPress}
|
||||
>
|
||||
<Feather
|
||||
name={icon}
|
||||
size={StyleConstants.Font.Size[size]}
|
||||
color={theme.primaryOverlay}
|
||||
/>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
position: 'absolute',
|
||||
padding: StyleConstants.Spacing.S * 1.5,
|
||||
borderRadius: StyleConstants.Spacing.XL
|
||||
}
|
||||
})
|
||||
|
||||
export default ButtomRound
|
@ -1,89 +0,0 @@
|
||||
import { Feather } from '@expo/vector-icons'
|
||||
import React from 'react'
|
||||
import { Pressable, StyleProp, StyleSheet, Text, ViewStyle } from 'react-native'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
|
||||
type PropsBase = {
|
||||
onPress: () => void
|
||||
disabled?: boolean
|
||||
buttonSize?: 'S' | 'M'
|
||||
size?: 'S' | 'M' | 'L'
|
||||
style?: StyleProp<ViewStyle>
|
||||
}
|
||||
|
||||
export interface PropsText extends PropsBase {
|
||||
text: string
|
||||
icon?: any
|
||||
}
|
||||
|
||||
export interface PropsIcon extends PropsBase {
|
||||
text?: string
|
||||
icon: any
|
||||
}
|
||||
|
||||
const ButtonRow: React.FC<PropsText | PropsIcon> = ({
|
||||
onPress,
|
||||
disabled = false,
|
||||
buttonSize = 'M',
|
||||
text,
|
||||
icon,
|
||||
size = 'M',
|
||||
style: customStyle
|
||||
}) => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
{...(!disabled && { onPress })}
|
||||
style={[
|
||||
customStyle,
|
||||
styles.button,
|
||||
{
|
||||
paddingLeft:
|
||||
StyleConstants.Spacing.M -
|
||||
(icon ? StyleConstants.Font.Size[size] / 2 : 0),
|
||||
paddingRight:
|
||||
StyleConstants.Spacing.M -
|
||||
(icon ? StyleConstants.Font.Size[size] / 2 : 0),
|
||||
borderColor: disabled ? theme.secondary : theme.primary,
|
||||
paddingTop: StyleConstants.Spacing[buttonSize === 'M' ? 'S' : 'XS'],
|
||||
paddingBottom: StyleConstants.Spacing[buttonSize === 'M' ? 'S' : 'XS']
|
||||
}
|
||||
]}
|
||||
>
|
||||
{icon ? (
|
||||
<Feather
|
||||
name={icon}
|
||||
size={StyleConstants.Font.Size[size]}
|
||||
color={disabled ? theme.secondary : theme.primary}
|
||||
/>
|
||||
) : (
|
||||
<Text
|
||||
style={[
|
||||
styles.text,
|
||||
{
|
||||
color: disabled ? theme.secondary : theme.primary,
|
||||
fontSize: StyleConstants.Font.Size[size]
|
||||
}
|
||||
]}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
)}
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
borderWidth: 1.25,
|
||||
borderRadius: 100,
|
||||
alignItems: 'center'
|
||||
},
|
||||
text: {
|
||||
textAlign: 'center'
|
||||
}
|
||||
})
|
||||
|
||||
export default ButtonRow
|
@ -1,78 +0,0 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
Alert,
|
||||
AlertButton,
|
||||
AlertOptions,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View
|
||||
} from 'react-native'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
|
||||
export interface Props {
|
||||
text: string
|
||||
destructive?: boolean
|
||||
alertOption?: {
|
||||
title: string
|
||||
message?: string | undefined
|
||||
buttons?: AlertButton[] | undefined
|
||||
options?: AlertOptions | undefined
|
||||
}
|
||||
}
|
||||
|
||||
const Core: React.FC<Props> = ({ text, destructive = false }) => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
return (
|
||||
<View style={styles.core}>
|
||||
<Text
|
||||
style={{
|
||||
color: destructive ? theme.red : theme.primary,
|
||||
fontWeight: destructive ? StyleConstants.Font.Weight.Bold : undefined
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const MenuButton: React.FC<Props> = ({ ...props }) => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={[styles.base, { borderBottomColor: theme.separator }]}
|
||||
onPress={() =>
|
||||
props.alertOption &&
|
||||
Alert.alert(
|
||||
props.alertOption.title,
|
||||
props.alertOption.message,
|
||||
props.alertOption.buttons,
|
||||
props.alertOption.options
|
||||
)
|
||||
}
|
||||
>
|
||||
<Core {...props} />
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
height: 50,
|
||||
borderBottomWidth: 1
|
||||
},
|
||||
core: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingLeft: StyleConstants.Spacing.Global.PagePadding,
|
||||
paddingRight: StyleConstants.Spacing.Global.PagePadding
|
||||
}
|
||||
})
|
||||
|
||||
export default MenuButton
|
@ -1,6 +1,5 @@
|
||||
import React, { Children } from 'react'
|
||||
import React from 'react'
|
||||
import { StyleSheet, View } from 'react-native'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
|
||||
export interface Props {
|
||||
@ -8,25 +7,7 @@ export interface Props {
|
||||
}
|
||||
|
||||
const MenuContainer: React.FC<Props> = ({ children }) => {
|
||||
const { theme } = useTheme()
|
||||
// @ts-ignore
|
||||
const firstChild = Children.toArray(children)[0].type.name
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.base,
|
||||
{
|
||||
...(firstChild !== 'MenuHeader' && {
|
||||
borderTopColor: theme.separator,
|
||||
borderTopWidth: 1
|
||||
})
|
||||
}
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
)
|
||||
return <View style={styles.base}>{children}</View>
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
@ -11,21 +11,21 @@ const MenuHeader: React.FC<Props> = ({ heading }) => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
return (
|
||||
<View style={[styles.base, { borderBottomColor: theme.separator }]}>
|
||||
<Text style={[styles.text, { color: theme.primary }]}>{heading}</Text>
|
||||
<View style={styles.base}>
|
||||
<Text style={[styles.text, { color: theme.secondary }]}>{heading}</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
borderBottomWidth: 1,
|
||||
paddingLeft: StyleConstants.Spacing.Global.PagePadding,
|
||||
paddingRight: StyleConstants.Spacing.Global.PagePadding,
|
||||
paddingBottom: StyleConstants.Spacing.S
|
||||
},
|
||||
text: {
|
||||
fontSize: StyleConstants.Font.Size.S
|
||||
fontSize: StyleConstants.Font.Size.S,
|
||||
fontWeight: StyleConstants.Font.Weight.Bold
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -71,14 +71,11 @@ const MenuRow: React.FC<Props> = ({ ...props }) => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
return props.onPress ? (
|
||||
<Pressable
|
||||
style={[styles.base, { borderBottomColor: theme.separator }]}
|
||||
onPress={props.onPress}
|
||||
>
|
||||
<Pressable style={styles.base} onPress={props.onPress}>
|
||||
<Core {...props} />
|
||||
</Pressable>
|
||||
) : (
|
||||
<View style={[styles.base, { borderBottomColor: theme.separator }]}>
|
||||
<View style={styles.base}>
|
||||
<Core {...props} />
|
||||
</View>
|
||||
)
|
||||
@ -86,8 +83,7 @@ const MenuRow: React.FC<Props> = ({ ...props }) => {
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
height: 50,
|
||||
borderBottomWidth: 1
|
||||
height: 50
|
||||
},
|
||||
core: {
|
||||
flex: 1,
|
||||
|
@ -152,83 +152,86 @@ const ParseContent: React.FC<Props> = ({
|
||||
return null
|
||||
}
|
||||
}, [])
|
||||
const rootComponent = useCallback(({ children }) => {
|
||||
const lineHeight = StyleConstants.Font.LineHeight[size]
|
||||
const rootComponent = useCallback(
|
||||
({ children }) => {
|
||||
const lineHeight = StyleConstants.Font.LineHeight[size]
|
||||
|
||||
const [heightOriginal, setHeightOriginal] = useState<number>()
|
||||
const [heightTruncated, setHeightTruncated] = useState<number>()
|
||||
const [allowExpand, setAllowExpand] = useState(false)
|
||||
const [showAllText, setShowAllText] = useState(false)
|
||||
const [heightOriginal, setHeightOriginal] = useState<number>()
|
||||
const [heightTruncated, setHeightTruncated] = useState<number>()
|
||||
const [allowExpand, setAllowExpand] = useState(false)
|
||||
const [showAllText, setShowAllText] = useState(false)
|
||||
|
||||
const calNumberOfLines = useMemo(() => {
|
||||
if (heightOriginal) {
|
||||
if (!heightTruncated) {
|
||||
return numberOfLines
|
||||
} else {
|
||||
if (allowExpand && !showAllText) {
|
||||
const calNumberOfLines = useMemo(() => {
|
||||
if (heightOriginal) {
|
||||
if (!heightTruncated) {
|
||||
return numberOfLines
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
}, [heightOriginal, heightTruncated, allowExpand, showAllText])
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Text
|
||||
style={{ lineHeight }}
|
||||
children={children}
|
||||
numberOfLines={calNumberOfLines}
|
||||
onLayout={({ nativeEvent }) => {
|
||||
if (!heightOriginal) {
|
||||
setHeightOriginal(nativeEvent.layout.height)
|
||||
if (allowExpand && !showAllText) {
|
||||
return numberOfLines
|
||||
} else {
|
||||
if (!heightTruncated) {
|
||||
setHeightTruncated(nativeEvent.layout.height)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
}, [heightOriginal, heightTruncated, allowExpand, showAllText])
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Text
|
||||
style={{ lineHeight, color: theme.primary }}
|
||||
children={children}
|
||||
numberOfLines={calNumberOfLines}
|
||||
onLayout={({ nativeEvent }) => {
|
||||
if (!heightOriginal) {
|
||||
setHeightOriginal(nativeEvent.layout.height)
|
||||
} else {
|
||||
if (heightOriginal > heightTruncated) {
|
||||
setAllowExpand(true)
|
||||
if (!heightTruncated) {
|
||||
setHeightTruncated(nativeEvent.layout.height)
|
||||
} else {
|
||||
if (heightOriginal > heightTruncated) {
|
||||
setAllowExpand(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{allowExpand && (
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
setShowAllText(!showAllText)
|
||||
}}
|
||||
style={{ marginTop: showAllText ? 0 : -lineHeight * 1.25 }}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={[
|
||||
theme.backgroundGradientStart,
|
||||
theme.backgroundGradientEnd
|
||||
]}
|
||||
locations={[0, lineHeight / (StyleConstants.Font.Size.S * 4)]}
|
||||
style={{
|
||||
paddingTop: StyleConstants.Font.Size.S * 2,
|
||||
paddingBottom: StyleConstants.Font.Size.S
|
||||
/>
|
||||
{allowExpand && (
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
setShowAllText(!showAllText)
|
||||
}}
|
||||
style={{ marginTop: showAllText ? 0 : -lineHeight * 1.25 }}
|
||||
>
|
||||
<Text
|
||||
<LinearGradient
|
||||
colors={[
|
||||
theme.backgroundGradientStart,
|
||||
theme.backgroundGradientEnd
|
||||
]}
|
||||
locations={[0, lineHeight / (StyleConstants.Font.Size.S * 4)]}
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
fontSize: StyleConstants.Font.Size.S,
|
||||
color: theme.primary
|
||||
paddingTop: StyleConstants.Font.Size.S * 2,
|
||||
paddingBottom: StyleConstants.Font.Size.S
|
||||
}}
|
||||
>
|
||||
{`${showAllText ? '折叠' : '展开'}${expandHint}`}
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}, [])
|
||||
<Text
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
fontSize: StyleConstants.Font.Size.S,
|
||||
color: theme.primary
|
||||
}}
|
||||
>
|
||||
{`${showAllText ? '折叠' : '展开'}${expandHint}`}
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
},
|
||||
[theme]
|
||||
)
|
||||
|
||||
return (
|
||||
<HTMLView
|
||||
|
@ -2,7 +2,7 @@ import { Feather } from '@expo/vector-icons'
|
||||
import React, { useMemo } from 'react'
|
||||
import { ActivityIndicator, StyleSheet, Text, View } from 'react-native'
|
||||
import { QueryStatus } from 'react-query'
|
||||
import { ButtonRow } from '@components/Button'
|
||||
import Button from '@components/Button'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
|
||||
@ -29,7 +29,7 @@ const TimelineEmpty: React.FC<Props> = ({ status, refetch }) => {
|
||||
<Text style={[styles.error, { color: theme.primary }]}>
|
||||
加载错误
|
||||
</Text>
|
||||
<ButtonRow text='重试' onPress={() => refetch()} />
|
||||
<Button type='text' content='重试' onPress={() => refetch()} />
|
||||
</>
|
||||
)
|
||||
case 'success':
|
||||
|
@ -16,7 +16,7 @@ const TimelineSeparator: React.FC<Props> = ({ highlighted = false }) => {
|
||||
style={[
|
||||
styles.base,
|
||||
{
|
||||
borderTopColor: theme.separator,
|
||||
borderTopColor: theme.border,
|
||||
marginLeft: highlighted
|
||||
? StyleConstants.Spacing.Global.PagePadding
|
||||
: StyleConstants.Spacing.Global.PagePadding +
|
||||
@ -30,7 +30,7 @@ const TimelineSeparator: React.FC<Props> = ({ highlighted = false }) => {
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
borderTopWidth: 1,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
marginRight: StyleConstants.Spacing.Global.PagePadding
|
||||
}
|
||||
})
|
||||
|
@ -78,7 +78,7 @@ const TimelineActions: React.FC<Props> = ({
|
||||
case 'reblog':
|
||||
case 'bookmark':
|
||||
if (type === 'favourite' && queryKey[0] === 'Favourites') {
|
||||
queryClient.invalidateQueries(['Favourites'])
|
||||
queryClient.invalidateQueries(['Favourites', {}])
|
||||
break
|
||||
}
|
||||
if (
|
||||
@ -91,7 +91,7 @@ const TimelineActions: React.FC<Props> = ({
|
||||
break
|
||||
}
|
||||
if (type === 'bookmark' && queryKey[0] === 'Bookmarks') {
|
||||
queryClient.invalidateQueries(['Bookmarks'])
|
||||
queryClient.invalidateQueries(['Bookmarks', {}])
|
||||
break
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { Image, Pressable, StyleSheet, View } from 'react-native'
|
||||
import { Audio } from 'expo-av'
|
||||
import { ButtonRow } from '@components/Button'
|
||||
import Button from '@components/Button'
|
||||
import { Surface } from 'gl-react-expo'
|
||||
import { Blurhash } from 'gl-react-blurhash'
|
||||
import Slider from '@react-native-community/slider'
|
||||
@ -21,7 +21,10 @@ const AttachmentAudio: React.FC<Props> = ({ sensitiveShown, audio }) => {
|
||||
const [audioPosition, setAudioPosition] = useState(0)
|
||||
const playAudio = useCallback(async () => {
|
||||
if (!audioPlayer) {
|
||||
await Audio.setAudioModeAsync({ interruptionModeIOS: 1 })
|
||||
await Audio.setAudioModeAsync({
|
||||
playsInSilentModeIOS: true,
|
||||
interruptionModeIOS: 1
|
||||
})
|
||||
const { sound } = await Audio.Sound.createAsync(
|
||||
{ uri: audio.url },
|
||||
{},
|
||||
@ -42,7 +45,7 @@ const AttachmentAudio: React.FC<Props> = ({ sensitiveShown, audio }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Pressable style={styles.overlay}>
|
||||
<View style={styles.overlay}>
|
||||
{sensitiveShown ? (
|
||||
audio.blurhash && (
|
||||
<Surface
|
||||
@ -62,16 +65,18 @@ const AttachmentAudio: React.FC<Props> = ({ sensitiveShown, audio }) => {
|
||||
source={{ uri: audio.preview_url || audio.preview_remote_url }}
|
||||
/>
|
||||
)}
|
||||
<ButtonRow
|
||||
icon={audioPlaying ? 'pause' : 'play'}
|
||||
<Button
|
||||
type='icon'
|
||||
content={audioPlaying ? 'pause' : 'play'}
|
||||
size='L'
|
||||
overlay
|
||||
{...(audioPlaying
|
||||
? { onPress: pauseAudio }
|
||||
: { onPress: playAudio })}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Pressable>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import { StyleSheet, Text, View } from 'react-native'
|
||||
import { ButtonRow } from '@components/Button'
|
||||
import Button from '@components/Button'
|
||||
import { useTheme } from '@root/utils/styles/ThemeManager'
|
||||
import { StyleConstants } from '@root/utils/styles/constants'
|
||||
import openLink from '@root/utils/openLink'
|
||||
@ -15,8 +15,9 @@ const AttachmentUnsupported: React.FC<Props> = ({ attachment }) => {
|
||||
<View style={styles.base}>
|
||||
<Text style={[styles.text, { color: theme.primary }]}>文件不支持</Text>
|
||||
{attachment.remote_url ? (
|
||||
<ButtonRow
|
||||
text='尝试远程链接'
|
||||
<Button
|
||||
type='text'
|
||||
content='尝试远程链接'
|
||||
size='S'
|
||||
onPress={async () => await openLink(attachment.remote_url!)}
|
||||
/>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useCallback, useRef, useState } from 'react'
|
||||
import { Pressable, StyleSheet } from 'react-native'
|
||||
import { Video } from 'expo-av'
|
||||
import { ButtonRow } from '@components/Button'
|
||||
import Button from '@components/Button'
|
||||
import { Surface } from 'gl-react-expo'
|
||||
import { Blurhash } from 'gl-react-blurhash'
|
||||
|
||||
@ -64,7 +64,13 @@ const AttachmentVideo: React.FC<Props> = ({
|
||||
<Blurhash hash={video.blurhash} />
|
||||
</Surface>
|
||||
) : (
|
||||
<ButtonRow icon='play' size='L' onPress={playOnPress} />
|
||||
<Button
|
||||
type='icon'
|
||||
content='play'
|
||||
size='L'
|
||||
overlay
|
||||
onPress={playOnPress}
|
||||
/>
|
||||
)}
|
||||
</Pressable>
|
||||
</>
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { Feather } from '@expo/vector-icons'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
import { useMutation, useQueryClient } from 'react-query'
|
||||
import client from '@api/client'
|
||||
import { ButtonRow } from '@components/Button'
|
||||
import Button from '@components/Button'
|
||||
import { toast } from '@components/toast'
|
||||
import relativeTime from '@utils/relativeTime'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
@ -18,14 +18,13 @@ const fireMutation = async ({
|
||||
options
|
||||
}: {
|
||||
id: string
|
||||
options?: { [key: number]: boolean }
|
||||
options?: boolean[]
|
||||
}) => {
|
||||
const formData = new FormData()
|
||||
options &&
|
||||
Object.keys(options).forEach(option => {
|
||||
// @ts-ignore
|
||||
if (options[option]) {
|
||||
formData.append('choices[]', option)
|
||||
options.forEach((o, i) => {
|
||||
if (options[i]) {
|
||||
formData.append('choices[]', i.toString())
|
||||
}
|
||||
})
|
||||
|
||||
@ -63,6 +62,11 @@ const TimelinePoll: React.FC<Props> = ({
|
||||
}) => {
|
||||
const { theme } = useTheme()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const [allOptions, setAllOptions] = useState(
|
||||
new Array(poll.options.length).fill(false)
|
||||
)
|
||||
|
||||
const mutation = useMutation(fireMutation, {
|
||||
onSuccess: (data, { id }) => {
|
||||
queryClient.cancelQueries(queryKey)
|
||||
@ -99,26 +103,26 @@ const TimelinePoll: React.FC<Props> = ({
|
||||
if (!sameAccount && !poll.voted) {
|
||||
return (
|
||||
<View style={styles.button}>
|
||||
<ButtonRow
|
||||
onPress={() => {
|
||||
if (poll.multiple) {
|
||||
mutation.mutate({ id: poll.id, options: multipleOptions })
|
||||
} else {
|
||||
mutation.mutate({ id: poll.id, options: singleOptions })
|
||||
}
|
||||
}}
|
||||
{...(mutation.isLoading ? { icon: 'loader' } : { text: '投票' })}
|
||||
disabled={mutation.isLoading}
|
||||
<Button
|
||||
onPress={() =>
|
||||
mutation.mutate({ id: poll.id, options: allOptions })
|
||||
}
|
||||
type='text'
|
||||
content='投票'
|
||||
loading={mutation.isLoading}
|
||||
disabled={allOptions.filter(o => o !== false).length === 0}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<View style={styles.button}>
|
||||
<ButtonRow
|
||||
<Button
|
||||
onPress={() => mutation.mutate({ id: poll.id })}
|
||||
{...(mutation.isLoading ? { icon: 'loader' } : { text: '刷新' })}
|
||||
disabled={mutation.isLoading}
|
||||
type='text'
|
||||
content='刷新'
|
||||
loading={mutation.isLoading}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
@ -142,19 +146,13 @@ const TimelinePoll: React.FC<Props> = ({
|
||||
}
|
||||
}, [])
|
||||
|
||||
const [singleOptions, setSingleOptions] = useState({
|
||||
...[false, false, false, false].slice(0, poll.options.length)
|
||||
})
|
||||
const [multipleOptions, setMultipleOptions] = useState({
|
||||
...[false, false, false, false].slice(0, poll.options.length)
|
||||
})
|
||||
const isSelected = (index: number) => {
|
||||
if (poll.multiple) {
|
||||
return multipleOptions[index] ? 'check-square' : 'square'
|
||||
} else {
|
||||
return singleOptions[index] ? 'check-circle' : 'circle'
|
||||
}
|
||||
}
|
||||
const isSelected = useCallback(
|
||||
(index: number): any =>
|
||||
allOptions[index]
|
||||
? `check-${poll.multiple ? 'square' : 'circle'}`
|
||||
: `${poll.multiple ? 'square' : 'circle'}`,
|
||||
[allOptions]
|
||||
)
|
||||
|
||||
return (
|
||||
<View style={styles.base}>
|
||||
@ -162,21 +160,23 @@ const TimelinePoll: React.FC<Props> = ({
|
||||
poll.voted ? (
|
||||
<View key={index} style={styles.poll}>
|
||||
<View style={styles.optionSelected}>
|
||||
<Feather
|
||||
style={styles.voted}
|
||||
name={poll.multiple ? 'check-square' : 'check-circle'}
|
||||
size={StyleConstants.Font.Size.M}
|
||||
color={
|
||||
poll.own_votes!.includes(index)
|
||||
? theme.primary
|
||||
: theme.background
|
||||
}
|
||||
/>
|
||||
<View style={styles.contentSelected}>
|
||||
<Emojis
|
||||
content={option.title}
|
||||
emojis={poll.emojis}
|
||||
size={StyleConstants.Font.Size.M}
|
||||
numberOfLines={1}
|
||||
numberOfLines={2}
|
||||
/>
|
||||
{poll.own_votes!.includes(index) && (
|
||||
<Feather
|
||||
style={styles.voted}
|
||||
name={poll.multiple ? 'check-square' : 'check-circle'}
|
||||
size={StyleConstants.Font.Size.M}
|
||||
color={theme.primary}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<Text style={[styles.percentage, { color: theme.primary }]}>
|
||||
{poll.votes_count
|
||||
@ -193,7 +193,7 @@ const TimelinePoll: React.FC<Props> = ({
|
||||
width: `${Math.round(
|
||||
(option.votes_count / poll.voters_count) * 100
|
||||
)}%`,
|
||||
backgroundColor: theme.border
|
||||
backgroundColor: theme.disabled
|
||||
}
|
||||
]}
|
||||
/>
|
||||
@ -204,19 +204,15 @@ const TimelinePoll: React.FC<Props> = ({
|
||||
style={[styles.optionUnselected]}
|
||||
onPress={() => {
|
||||
if (poll.multiple) {
|
||||
setMultipleOptions({
|
||||
...multipleOptions,
|
||||
[index]: !multipleOptions[index]
|
||||
})
|
||||
setAllOptions(
|
||||
allOptions.map((o, i) => (i === index ? !o : o))
|
||||
)
|
||||
} else {
|
||||
setSingleOptions({
|
||||
...[
|
||||
index === 0,
|
||||
index === 1,
|
||||
index === 2,
|
||||
index === 3
|
||||
].slice(0, poll.options.length)
|
||||
})
|
||||
setAllOptions(
|
||||
allOptions.map((o, i) =>
|
||||
i === index ? !allOptions[index] : allOptions[index]
|
||||
)
|
||||
)
|
||||
}
|
||||
}}
|
||||
>
|
||||
@ -231,6 +227,7 @@ const TimelinePoll: React.FC<Props> = ({
|
||||
content={option.title}
|
||||
emojis={poll.emojis}
|
||||
size={StyleConstants.Font.Size.M}
|
||||
numberOfLines={2}
|
||||
/>
|
||||
</View>
|
||||
</Pressable>
|
||||
@ -253,15 +250,15 @@ const styles = StyleSheet.create({
|
||||
marginTop: StyleConstants.Spacing.M
|
||||
},
|
||||
poll: {
|
||||
minHeight: StyleConstants.Font.LineHeight.M * 1.5,
|
||||
marginBottom: StyleConstants.Spacing.XS
|
||||
flex: 1,
|
||||
minHeight: StyleConstants.Font.LineHeight.M * 2,
|
||||
paddingVertical: StyleConstants.Spacing.XS
|
||||
},
|
||||
optionSelected: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingLeft: StyleConstants.Spacing.M,
|
||||
paddingRight: StyleConstants.Spacing.M
|
||||
},
|
||||
optionUnselected: {
|
||||
@ -269,30 +266,30 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row'
|
||||
},
|
||||
contentSelected: {
|
||||
flexBasis: '80%',
|
||||
flexShrink: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
alignItems: 'center',
|
||||
paddingRight: StyleConstants.Spacing.S
|
||||
},
|
||||
contentUnselected: {
|
||||
flexBasis: '90%'
|
||||
flexShrink: 1
|
||||
},
|
||||
voted: {
|
||||
marginLeft: StyleConstants.Spacing.S
|
||||
marginRight: StyleConstants.Spacing.S
|
||||
},
|
||||
votedNot: {
|
||||
marginRight: StyleConstants.Spacing.S
|
||||
paddingRight: StyleConstants.Spacing.S
|
||||
},
|
||||
percentage: {
|
||||
fontSize: StyleConstants.Font.Size.M
|
||||
},
|
||||
background: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
height: '100%',
|
||||
height: StyleConstants.Spacing.XS,
|
||||
minWidth: 2,
|
||||
borderTopRightRadius: 6,
|
||||
borderBottomRightRadius: 6
|
||||
borderTopRightRadius: 10,
|
||||
borderBottomRightRadius: 10,
|
||||
marginTop: StyleConstants.Spacing.XS,
|
||||
marginBottom: StyleConstants.Spacing.S
|
||||
},
|
||||
meta: {
|
||||
flex: 1,
|
||||
|
Reference in New Issue
Block a user