diff --git a/fastlane/metadata/en-US/release_notes.txt b/fastlane/metadata/en-US/release_notes.txt index 9e267a57..568f648b 100644 --- a/fastlane/metadata/en-US/release_notes.txt +++ b/fastlane/metadata/en-US/release_notes.txt @@ -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 diff --git a/fastlane/metadata/zh-Hans/release_notes.txt b/fastlane/metadata/zh-Hans/release_notes.txt index 8ba28658..1f213bc2 100644 --- a/fastlane/metadata/zh-Hans/release_notes.txt +++ b/fastlane/metadata/zh-Hans/release_notes.txt @@ -1,3 +1,2 @@ toooting愉快!此版本包括以下改进和修复: -- 修复错误的升级通知 -- 修复部分应用崩溃 +- 可添加举报细节 diff --git a/package.json b/package.json index 1a0f20db..02186088 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tooot", - "version": "4.7.2", + "version": "4.7.3", "description": "tooot for Mastodon", "author": "xmflsct ", "license": "GPL-3.0-or-later", diff --git a/src/@types/mastodon.d.ts b/src/@types/mastodon.d.ts index 102739ca..80b3e477 100644 --- a/src/@types/mastodon.d.ts +++ b/src/@types/mastodon.d.ts @@ -470,6 +470,11 @@ declare namespace Mastodon { updated_at: string } + type Rule = { + id: string + text: string + } + type Status = { // Base id: string diff --git a/src/components/Account.tsx b/src/components/Account.tsx index b2c7a058..7445ad7e 100644 --- a/src/components/Account.tsx +++ b/src/components/Account.tsx @@ -42,11 +42,11 @@ const ComponentAccount: React.FC = ({ account, props, style={{ width: StyleConstants.Avatar.S, height: StyleConstants.Avatar.S, - borderRadius: 6, + borderRadius: 8, marginRight: StyleConstants.Spacing.S }} /> - + { 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} /> diff --git a/src/components/Header/Right.tsx b/src/components/Header/Right.tsx index 3c833c86..ade36587 100644 --- a/src/components/Header/Right.tsx +++ b/src/components/Header/Right.tsx @@ -18,6 +18,7 @@ export interface Props { loading?: boolean disabled?: boolean + destructive?: boolean onPress: () => void } @@ -34,6 +35,7 @@ const HeaderRight: React.FC = ({ background = false, loading, disabled, + destructive = false, onPress }) => { const { colors, theme } = useTheme() @@ -41,10 +43,7 @@ const HeaderRight: React.FC = ({ const loadingSpinkit = useMemo( () => ( - + ), [theme] @@ -59,7 +58,7 @@ const HeaderRight: React.FC = ({ 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 = ({ <> = ({ 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 }), diff --git a/src/components/Input.tsx b/src/components/Input.tsx index 7354dc44..c6b3de63 100644 --- a/src/components/Input.tsx +++ b/src/components/Input.tsx @@ -85,7 +85,6 @@ const ComponentInput = forwardRef( multiline, numberOfLines: Platform.OS === 'android' ? 5 : undefined })} - keyboardAppearance={mode} textAlignVertical='top' {...props} /> diff --git a/src/components/Parse/Emojis.tsx b/src/components/Parse/Emojis.tsx index 7607f555..195c9893 100644 --- a/src/components/Parse/Emojis.tsx +++ b/src/components/Parse/Emojis.tsx @@ -93,7 +93,7 @@ const ParseEmojis = React.memo( ) }, - (prev, next) => prev.content === next.content + (prev, next) => prev.content === next.content && prev.style?.color === next.style?.color ) export default ParseEmojis diff --git a/src/components/Selections.tsx b/src/components/Selections.tsx index 3e39cdfd..9fa38501 100644 --- a/src/components/Selections.tsx +++ b/src/components/Selections.tsx @@ -11,9 +11,15 @@ export interface Props { multiple?: boolean options: { selected: boolean; content: string }[] setOptions: React.Dispatch> + disabled?: boolean } -const Selections: React.FC = ({ multiple = false, options, setOptions }) => { +const Selections: React.FC = ({ + multiple = false, + options, + setOptions, + disabled = false +}) => { const { colors } = useTheme() const isSelected = (index: number): string => @@ -22,10 +28,11 @@ const Selections: React.FC = ({ multiple = false, options, setOptions }) : `${multiple ? 'Square' : 'Circle'}` return ( - <> + {options.map((option, index) => ( { if (multiple) { @@ -56,15 +63,18 @@ const Selections: React.FC = ({ multiple = false, options, setOptions }) }} name={isSelected(index)} size={StyleConstants.Font.Size.M} - color={colors.primaryDefault} + color={disabled ? colors.disabled : colors.primaryDefault} /> - + ))} - + ) } diff --git a/src/components/Timeline/Shared/Content.tsx b/src/components/Timeline/Shared/Content.tsx index 5cbcde98..b250d56a 100644 --- a/src/components/Timeline/Shared/Content.tsx +++ b/src/components/Timeline/Shared/Content.tsx @@ -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 = ({ notificationOwnToot = false, setSpoi } /> {inThread ? ( - + {t('shared.content.expandHint')} ) : null} diff --git a/src/components/Timeline/Shared/HeaderAndroid.tsx b/src/components/Timeline/Shared/HeaderAndroid.tsx index ece4ef79..31836e4c 100644 --- a/src/components/Timeline/Shared/HeaderAndroid.tsx +++ b/src/components/Timeline/Shared/HeaderAndroid.tsx @@ -28,6 +28,7 @@ const TimelineHeaderAndroid: React.FC = () => { type: 'status', openChange, account: status.account, + ...(status && { status }), queryKey }) const mStatus = menuStatus({ status, queryKey, rootQueryKey }) diff --git a/src/components/Timeline/Shared/HeaderDefault.tsx b/src/components/Timeline/Shared/HeaderDefault.tsx index 44e28479..1a5f4d98 100644 --- a/src/components/Timeline/Shared/HeaderDefault.tsx +++ b/src/components/Timeline/Shared/HeaderDefault.tsx @@ -34,6 +34,7 @@ const TimelineHeaderDefault: React.FC = () => { type: 'status', openChange, account: status.account, + ...(status && { status }), queryKey }) const mStatus = menuStatus({ status, queryKey, rootQueryKey }) diff --git a/src/components/Timeline/Shared/HeaderNotification.tsx b/src/components/Timeline/Shared/HeaderNotification.tsx index 02cdef2f..171148ff 100644 --- a/src/components/Timeline/Shared/HeaderNotification.tsx +++ b/src/components/Timeline/Shared/HeaderNotification.tsx @@ -42,6 +42,7 @@ const TimelineHeaderNotification: React.FC = ({ notification }) => { type: 'status', openChange, account: status?.account, + ...(status && { status }), queryKey }) const mStatus = menuStatus({ status, queryKey }) diff --git a/src/components/contextMenu/account.ts b/src/components/contextMenu/account.ts index 1b26735a..21a5bed4 100644 --- a/src/components/contextMenu/account.ts +++ b/src/components/contextMenu/account.ts @@ -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 + 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 diff --git a/src/i18n/en/screens/tabs.json b/src/i18n/en/screens/tabs.json index e8785d5d..75c975eb 100644 --- a/src/i18n/en/screens/tabs.json +++ b/src/i18n/en/screens/tabs.json @@ -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", diff --git a/src/screens/Tabs/Shared/Report.tsx b/src/screens/Tabs/Shared/Report.tsx new file mode 100644 index 00000000..97e92920 --- /dev/null +++ b/src/screens/Tabs/Shared/Report.tsx @@ -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> = ({ + 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(true) + const [comment, setComment] = useState('') + + const [isReporting, setIsReporting] = useState(false) + useEffect(() => { + navigation.setOptions({ + title: t('shared.report.name', { acct: `@${account.acct}` }), + headerLeft: () => ( + navigation.goBack()} + /> + ), + headerRight: () => ( + { + 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 ( + + + + + + {!localInstance ? ( + + + {t('shared.report.forward.heading', { instance: account.acct.match(/@(.*)/)?.[1] })} + + + + ) : null} + + + {t('shared.report.reasons.heading')} + + + + + + {categories[1].selected || comment.length ? ( + <> + + {t('shared.report.comment.heading')} + + + + + + + {comment.length} / 1000 + + + + + ) : null} + + {rules.length ? ( + <> + + {t('shared.report.violatedRules.heading')} + + + + + + ) : null} + + + ) +} + +export default TabSharedReport diff --git a/src/screens/Tabs/Shared/index.tsx b/src/screens/Tabs/Shared/index.tsx index dfc67d4a..7ae62d84 100644 --- a/src/screens/Tabs/Shared/index.tsx +++ b/src/screens/Tabs/Shared/index.tsx @@ -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 }) => { return ( @@ -37,6 +38,12 @@ const TabShared = ({ Stack }: { Stack: ReturnType + diff --git a/src/utils/navigation/navigators.ts b/src/utils/navigation/navigators.ts index 95f752d8..42095ac2 100644 --- a/src/utils/navigation/navigators.ts +++ b/src/utils/navigation/navigators.ts @@ -92,7 +92,7 @@ export type ScreenTabsScreenProps = Bo export type TabSharedStackParamList = { 'Tab-Shared-Account': { - account: Mastodon.Account | Mastodon.Mention + account: Partial & Pick } 'Tab-Shared-Account-In-Lists': { account: Pick @@ -105,6 +105,7 @@ export type TabSharedStackParamList = { id: Mastodon.Status['id'] detectedLanguage: string } + 'Tab-Shared-Report': { account: Mastodon.Account; status?: Pick } 'Tab-Shared-Search': undefined 'Tab-Shared-Toot': { toot: Mastodon.Status diff --git a/src/utils/queryHooks/reports.ts b/src/utils/queryHooks/reports.ts new file mode 100644 index 00000000..9b33fcf9 --- /dev/null +++ b/src/utils/queryHooks/reports.ts @@ -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({ + method: 'get', + url: 'instance/rules' + }).then(res => res.body) + +const useRulesQuery = (params?: { options?: UseQueryOptions }) => { + const queryKey: QueryKeyRules = ['Rules'] + return useQuery(queryKey, queryFunction, params?.options) +} + +export { useRulesQuery }