This commit is contained in:
xmflsct 2022-12-22 01:21:51 +01:00
parent fb7111d771
commit 39ab9059d9
20 changed files with 320 additions and 62 deletions

View File

@ -1,3 +1,2 @@
Enjoy toooting! This version includes following improvements and fixes:
- Fixed wrongly update notification
- Fix some random crashes
- Allowing adding more context of reports

View File

@ -1,3 +1,2 @@
toooting愉快此版本包括以下改进和修复
- 修复错误的升级通知
- 修复部分应用崩溃
- 可添加举报细节

View File

@ -1,6 +1,6 @@
{
"name": "tooot",
"version": "4.7.2",
"version": "4.7.3",
"description": "tooot for Mastodon",
"author": "xmflsct <me@xmflsct.com>",
"license": "GPL-3.0-or-later",

View File

@ -470,6 +470,11 @@ declare namespace Mastodon {
updated_at: string
}
type Rule = {
id: string
text: string
}
type Status = {
// Base
id: string

View File

@ -42,11 +42,11 @@ const ComponentAccount: React.FC<PropsWithChildren & Props> = ({ account, props,
style={{
width: StyleConstants.Avatar.S,
height: StyleConstants.Avatar.S,
borderRadius: 6,
borderRadius: 8,
marginRight: StyleConstants.Spacing.S
}}
/>
<View>
<View style={{ flex: 1 }}>
<CustomText numberOfLines={1}>
<ParseEmojis
content={account.display_name || account.username}

View File

@ -28,7 +28,7 @@ const EmojisList = () => {
const { t } = useTranslation()
const { emojisState, emojisDispatch } = useContext(EmojisContext)
const { colors, mode } = useTheme()
const { colors } = useTheme()
const addEmoji = (shortcode: string) => {
if (emojisState.targetIndex === -1) {
@ -158,7 +158,6 @@ const EmojisList = () => {
onChangeText={setSearch}
autoCapitalize='none'
clearButtonMode='always'
keyboardAppearance={mode}
autoCorrect={false}
spellCheck={false}
/>

View File

@ -18,6 +18,7 @@ export interface Props {
loading?: boolean
disabled?: boolean
destructive?: boolean
onPress: () => void
}
@ -34,6 +35,7 @@ const HeaderRight: React.FC<Props> = ({
background = false,
loading,
disabled,
destructive = false,
onPress
}) => {
const { colors, theme } = useTheme()
@ -41,10 +43,7 @@ const HeaderRight: React.FC<Props> = ({
const loadingSpinkit = useMemo(
() => (
<View style={{ position: 'absolute' }}>
<Flow
size={StyleConstants.Font.Size.M * 1.25}
color={colors.secondary}
/>
<Flow size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} />
</View>
),
[theme]
@ -59,7 +58,7 @@ const HeaderRight: React.FC<Props> = ({
name={content}
style={{ opacity: loading ? 0 : 1 }}
size={StyleConstants.Spacing.M * 1.25}
color={disabled ? colors.secondary : colors.primaryDefault}
color={disabled ? colors.secondary : destructive ? colors.red : colors.primaryDefault}
/>
{loading && loadingSpinkit}
</>
@ -69,8 +68,13 @@ const HeaderRight: React.FC<Props> = ({
<>
<CustomText
fontStyle='M'
fontWeight={destructive ? 'Bold' : 'Normal'}
style={{
color: disabled ? colors.secondary : colors.primaryDefault,
color: disabled
? colors.secondary
: destructive
? colors.red
: colors.primaryDefault,
opacity: loading ? 0 : 1
}}
children={content}
@ -94,14 +98,10 @@ const HeaderRight: React.FC<Props> = ({
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: background
? colors.backgroundOverlayDefault
: undefined,
backgroundColor: background ? colors.backgroundOverlayDefault : undefined,
minHeight: 44,
minWidth: 44,
marginRight: native
? -StyleConstants.Spacing.S
: StyleConstants.Spacing.S,
marginRight: native ? -StyleConstants.Spacing.S : StyleConstants.Spacing.S,
...(type === 'icon' && {
borderRadius: 100
}),

View File

@ -85,7 +85,6 @@ const ComponentInput = forwardRef(
multiline,
numberOfLines: Platform.OS === 'android' ? 5 : undefined
})}
keyboardAppearance={mode}
textAlignVertical='top'
{...props}
/>

View File

@ -93,7 +93,7 @@ const ParseEmojis = React.memo(
</CustomText>
)
},
(prev, next) => prev.content === next.content
(prev, next) => prev.content === next.content && prev.style?.color === next.style?.color
)
export default ParseEmojis

View File

@ -11,9 +11,15 @@ export interface Props {
multiple?: boolean
options: { selected: boolean; content: string }[]
setOptions: React.Dispatch<React.SetStateAction<{ selected: boolean; content: string }[]>>
disabled?: boolean
}
const Selections: React.FC<Props> = ({ multiple = false, options, setOptions }) => {
const Selections: React.FC<Props> = ({
multiple = false,
options,
setOptions,
disabled = false
}) => {
const { colors } = useTheme()
const isSelected = (index: number): string =>
@ -22,10 +28,11 @@ const Selections: React.FC<Props> = ({ multiple = false, options, setOptions })
: `${multiple ? 'Square' : 'Circle'}`
return (
<>
<View>
{options.map((option, index) => (
<Pressable
key={index}
disabled={disabled}
style={{ flex: 1, paddingVertical: StyleConstants.Spacing.S }}
onPress={() => {
if (multiple) {
@ -56,15 +63,18 @@ const Selections: React.FC<Props> = ({ multiple = false, options, setOptions })
}}
name={isSelected(index)}
size={StyleConstants.Font.Size.M}
color={colors.primaryDefault}
color={disabled ? colors.disabled : colors.primaryDefault}
/>
<CustomText style={{ flex: 1 }}>
<ParseEmojis content={option.content} />
<ParseEmojis
content={option.content}
style={{ color: disabled ? colors.disabled : colors.primaryDefault }}
/>
</CustomText>
</View>
</Pressable>
))}
</>
</View>
)
}

View File

@ -5,8 +5,7 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next'
import { Platform, StyleSheet, View } from 'react-native'
import { Path, Svg } from 'react-native-svg'
import { Platform } from 'react-native'
import { useSelector } from 'react-redux'
import { isRtlLang } from 'rtl-detect'
import StatusContext from './Context'
@ -45,7 +44,14 @@ const TimelineContent: React.FC<Props> = ({ notificationOwnToot = false, setSpoi
}
/>
{inThread ? (
<CustomText fontStyle='S' style={{ textAlign: 'center', color: colors.secondary, paddingVertical: StyleConstants.Spacing.XS }}>
<CustomText
fontStyle='S'
style={{
textAlign: 'center',
color: colors.secondary,
paddingVertical: StyleConstants.Spacing.XS
}}
>
{t('shared.content.expandHint')}
</CustomText>
) : null}

View File

@ -28,6 +28,7 @@ const TimelineHeaderAndroid: React.FC = () => {
type: 'status',
openChange,
account: status.account,
...(status && { status }),
queryKey
})
const mStatus = menuStatus({ status, queryKey, rootQueryKey })

View File

@ -34,6 +34,7 @@ const TimelineHeaderDefault: React.FC = () => {
type: 'status',
openChange,
account: status.account,
...(status && { status }),
queryKey
})
const mStatus = menuStatus({ status, queryKey, rootQueryKey })

View File

@ -42,6 +42,7 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
type: 'status',
openChange,
account: status?.account,
...(status && { status }),
queryKey
})
const mStatus = menuStatus({ status, queryKey })

View File

@ -24,12 +24,14 @@ const menuAccount = ({
type,
openChange,
account,
status,
queryKey,
rootQueryKey
}: {
type: 'status' | 'account' // Where the action is coming from
openChange: boolean
account?: Pick<Mastodon.Account, 'id' | 'username'>
account?: Mastodon.Account
status?: Mastodon.Status
queryKey?: QueryKeyTimeline
rootQueryKey?: QueryKeyTimeline
}): ContextMenu[][] => {
@ -214,34 +216,7 @@ const menuAccount = ({
{
key: 'account-reports',
item: {
onSelect: () =>
Alert.alert(
t('account.reports.alert.title', { username: account.username }),
undefined,
[
{
text: t('common:buttons.confirm'),
style: 'destructive',
onPress: () => {
timelineMutation.mutate({
type: 'updateAccountProperty',
queryKey,
id: account.id,
payload: { property: 'reports' }
})
timelineMutation.mutate({
type: 'updateAccountProperty',
queryKey,
id: account.id,
payload: { property: 'block', currentValue: false }
})
}
},
{
text: t('common:buttons.cancel')
}
]
),
onSelect: () => navigation.navigate('Tab-Shared-Report', { account, status }),
disabled: false,
destructive: true,
hidden: false

View File

@ -357,6 +357,25 @@
"history": {
"name": "Edit History"
},
"report": {
"name": "Report {{acct}}",
"report": "Report",
"forward": {
"heading": "Anonymously forward to remote server {{instance}}"
},
"reasons": {
"heading": "What is going on with this account?",
"spam": "It is spam",
"other": "It is something else",
"violation": "It violates server rules"
},
"comment": {
"heading": "Anything else you want to add?"
},
"violatedRules": {
"heading": "Violated server rules"
}
},
"search": {
"header": {
"prefix": "Searching",

View File

@ -0,0 +1,218 @@
import apiInstance from '@api/instance'
import ComponentAccount from '@components/Account'
import { HeaderLeft, HeaderRight } from '@components/Header'
import Selections from '@components/Selections'
import CustomText from '@components/Text'
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
import { useRulesQuery } from '@utils/queryHooks/reports'
import { getInstanceUri } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Platform, ScrollView, TextInput, View } from 'react-native'
import { Switch } from 'react-native-gesture-handler'
import { useSelector } from 'react-redux'
const TabSharedReport: React.FC<TabSharedStackScreenProps<'Tab-Shared-Report'>> = ({
navigation,
route: {
params: { account, status }
}
}) => {
const { colors } = useTheme()
const { t } = useTranslation('screenTabs')
const [categories, setCategories] = useState<
{ selected: boolean; content: string; type: 'spam' | 'other' | 'violation' }[]
>([
{ selected: true, content: t('shared.report.reasons.spam'), type: 'spam' },
{ selected: false, content: t('shared.report.reasons.other'), type: 'other' },
{ selected: false, content: t('shared.report.reasons.violation'), type: 'violation' }
])
const [rules, setRules] = useState<{ selected: boolean; content: string; id: string }[]>([])
const [forward, setForward] = useState<boolean>(true)
const [comment, setComment] = useState('')
const [isReporting, setIsReporting] = useState(false)
useEffect(() => {
navigation.setOptions({
title: t('shared.report.name', { acct: `@${account.acct}` }),
headerLeft: () => (
<HeaderLeft
type='text'
content={t('common:buttons.cancel')}
onPress={() => navigation.goBack()}
/>
),
headerRight: () => (
<HeaderRight
type='text'
content={t('shared.report.report')}
destructive
onPress={() => {
const body = new FormData()
body.append('account_id', account.id)
status && body.append('status_ids[]', status.id)
comment.length && body.append('comment', comment)
body.append('forward', forward.toString())
body.append('category', categories.find(category => category.selected)?.type || 'other')
rules.filter(rule => rule.selected).forEach(rule => body.append('rule_ids[]', rule.id))
apiInstance({ method: 'post', url: 'reports', body })
.then(() => {
setIsReporting(false)
navigation.pop(1)
})
.catch(() => {
setIsReporting(false)
})
}}
loading={isReporting}
/>
)
})
}, [isReporting, comment, forward, categories, rules])
const instanceUri = useSelector(getInstanceUri)
const localInstance = account?.acct.includes('@')
? account?.acct.includes(`@${instanceUri}`)
: true
const rulesQuery = useRulesQuery()
useEffect(() => {
if (rulesQuery.data) {
setRules(rulesQuery.data.map(rule => ({ ...rule, selected: false, content: rule.text })))
}
}, [rulesQuery.data])
return (
<ScrollView>
<View
style={{
margin: StyleConstants.Spacing.Global.PagePadding,
borderWidth: 1,
borderColor: colors.red,
borderRadius: 8
}}
>
<ComponentAccount account={account} props={{}} />
</View>
<View
style={{
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding,
marginTop: StyleConstants.Spacing.M
}}
>
{!localInstance ? (
<View
style={{
flex: 1,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: StyleConstants.Spacing.L
}}
>
<CustomText
fontStyle='M'
style={{ color: colors.primaryDefault, paddingRight: StyleConstants.Spacing.M }}
numberOfLines={2}
>
{t('shared.report.forward.heading', { instance: account.acct.match(/@(.*)/)?.[1] })}
</CustomText>
<Switch
value={forward}
onValueChange={setForward}
trackColor={{ true: colors.blue, false: colors.disabled }}
/>
</View>
) : null}
<CustomText
fontStyle='M'
style={{ color: colors.primaryDefault, marginBottom: StyleConstants.Spacing.S }}
>
{t('shared.report.reasons.heading')}
</CustomText>
<View style={{ marginLeft: StyleConstants.Spacing.M }}>
<Selections options={categories} setOptions={setCategories} />
</View>
{categories[1].selected || comment.length ? (
<>
<CustomText
fontStyle='M'
style={{
color: colors.primaryDefault,
marginTop: StyleConstants.Spacing.M,
marginBottom: StyleConstants.Spacing.XS
}}
>
{t('shared.report.comment.heading')}
</CustomText>
<View
style={{
borderWidth: 1,
marginVertical: StyleConstants.Spacing.S,
padding: StyleConstants.Spacing.S,
borderColor: colors.border,
flexDirection: 'column',
alignItems: 'stretch'
}}
>
<TextInput
style={{
flex: 1,
fontSize: StyleConstants.Font.Size.M,
color: colors.primaryDefault,
minHeight:
Platform.OS === 'ios' ? StyleConstants.Font.LineHeight.M * 5 : undefined
}}
value={comment}
onChangeText={setComment}
multiline={true}
numberOfLines={Platform.OS === 'android' ? 5 : undefined}
textAlignVertical='top'
/>
<View style={{ flexDirection: 'row', alignSelf: 'flex-end' }}>
<CustomText
fontStyle='S'
style={{ paddingLeft: StyleConstants.Spacing.XS, color: colors.secondary }}
>
{comment.length} / 1000
</CustomText>
</View>
</View>
</>
) : null}
{rules.length ? (
<>
<CustomText
fontStyle='M'
style={{
color: categories[2].selected ? colors.primaryDefault : colors.disabled,
marginTop: StyleConstants.Spacing.M,
marginBottom: StyleConstants.Spacing.S
}}
>
{t('shared.report.violatedRules.heading')}
</CustomText>
<View style={{ marginLeft: StyleConstants.Spacing.M }}>
<Selections
disabled={!categories[2].selected}
multiple
options={rules}
setOptions={setRules}
/>
</View>
</>
) : null}
</View>
</ScrollView>
)
}
export default TabSharedReport

View File

@ -1,13 +1,14 @@
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import TabSharedAccount from '@screens/Tabs/Shared/Account'
import TabSharedAccountInLists from '@screens/Tabs/Shared/AccountInLists'
import TabSharedAttachments from '@screens/Tabs/Shared/Attachments'
import TabSharedHashtag from '@screens/Tabs/Shared/Hashtag'
import TabSharedHistory from '@screens/Tabs/Shared/History'
import TabSharedReport from '@screens/Tabs/Shared/Report'
import TabSharedSearch from '@screens/Tabs/Shared/Search'
import TabSharedToot from '@screens/Tabs/Shared/Toot'
import TabSharedUsers from '@screens/Tabs/Shared/Users'
import React from 'react'
import TabSharedAccountInLists from './AccountInLists'
const TabShared = ({ Stack }: { Stack: ReturnType<typeof createNativeStackNavigator> }) => {
return (
@ -37,6 +38,12 @@ const TabShared = ({ Stack }: { Stack: ReturnType<typeof createNativeStackNaviga
name='Tab-Shared-History'
component={TabSharedHistory}
/>
<Stack.Screen
key='Tab-Shared-Report'
name='Tab-Shared-Report'
component={TabSharedReport}
options={{ presentation: 'modal' }}
/>
<Stack.Screen key='Tab-Shared-Search' name='Tab-Shared-Search' component={TabSharedSearch} />
<Stack.Screen key='Tab-Shared-Toot' name='Tab-Shared-Toot' component={TabSharedToot} />
<Stack.Screen key='Tab-Shared-Users' name='Tab-Shared-Users' component={TabSharedUsers} />

View File

@ -92,7 +92,7 @@ export type ScreenTabsScreenProps<T extends keyof ScreenTabsStackParamList> = Bo
export type TabSharedStackParamList = {
'Tab-Shared-Account': {
account: Mastodon.Account | Mastodon.Mention
account: Partial<Mastodon.Account> & Pick<Mastodon.Account, 'id'>
}
'Tab-Shared-Account-In-Lists': {
account: Pick<Mastodon.Account, 'id' | 'username'>
@ -105,6 +105,7 @@ export type TabSharedStackParamList = {
id: Mastodon.Status['id']
detectedLanguage: string
}
'Tab-Shared-Report': { account: Mastodon.Account; status?: Pick<Mastodon.Status, 'id'> }
'Tab-Shared-Search': undefined
'Tab-Shared-Toot': {
toot: Mastodon.Status

View File

@ -0,0 +1,18 @@
import apiInstance from '@api/instance'
import { AxiosError } from 'axios'
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
export type QueryKeyRules = ['Rules']
const queryFunction = () =>
apiInstance<Mastodon.Rule[]>({
method: 'get',
url: 'instance/rules'
}).then(res => res.body)
const useRulesQuery = (params?: { options?: UseQueryOptions<Mastodon.Rule[], AxiosError> }) => {
const queryKey: QueryKeyRules = ['Rules']
return useQuery(queryKey, queryFunction, params?.options)
}
export { useRulesQuery }