Merge pull request #339 from tooot-app/main

Release v4.1.2
This commit is contained in:
xmflsct 2022-06-15 00:01:48 +02:00 committed by GitHub
commit 77e0f32260
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 291 additions and 314 deletions

View File

@ -4,7 +4,7 @@
"native": "220603",
"major": 4,
"minor": 1,
"patch": 1,
"patch": 2,
"expo": "45.0.0"
},
"description": "tooot app for Mastodon",

View File

@ -271,8 +271,9 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
return (
<IntlProvider locale={i18n.language}>
<StatusBar
{...(Platform.OS === 'ios' && {
backgroundColor: colors.backgroundDefault
backgroundColor={colors.backgroundDefault}
{...(Platform.OS === 'android' && {
barStyle: theme === 'light' ? 'dark-content' : 'light-content'
})}
/>
<NavigationContainer

View File

@ -1,5 +1,6 @@
import analytics from '@components/analytics'
import { displayMessage } from '@components/Message'
import { useRelationshipQuery } from '@utils/queryHooks/relationship'
import {
MutationVarsTimelineUpdateAccountProperty,
QueryKeyTimeline,
@ -8,13 +9,13 @@ import {
import { getInstanceAccount } from '@utils/slices/instancesSlice'
import { useTheme } from '@utils/styles/ThemeManager'
import { useTranslation } from 'react-i18next'
import { Platform } from 'react-native'
import { ContextMenuAction } from 'react-native-context-menu-view'
import { useQueryClient } from 'react-query'
import { useSelector } from 'react-redux'
export interface Props {
actions: ContextMenuAction[]
type: 'status' | 'account' // Do not need to fetch relationship in timeline
queryKey?: QueryKeyTimeline
rootQueryKey?: QueryKeyTimeline
id: Mastodon.Account['id']
@ -22,6 +23,7 @@ export interface Props {
const contextMenuAccount = ({
actions,
type,
queryKey,
rootQueryKey,
id: accountId
@ -32,12 +34,17 @@ const contextMenuAccount = ({
const queryClient = useQueryClient()
const mutateion = useTimelineMutation({
onSuccess: (_, params) => {
queryClient.refetchQueries(['Relationship', { id: accountId }])
const theParams = params as MutationVarsTimelineUpdateAccountProperty
displayMessage({
theme,
type: 'success',
message: t('common:message.success.message', {
function: t(`account.${theParams.payload.property}.action`)
function: t(`account.${theParams.payload.property}.action`, {
...(typeof theParams.payload.currentValue === 'boolean' && {
context: theParams.payload.currentValue.toString()
})
})
})
})
},
@ -47,7 +54,11 @@ const contextMenuAccount = ({
theme,
type: 'error',
message: t('common:message.error.message', {
function: t(`account.${theParams.payload.property}.action`)
function: t(`account.${theParams.payload.property}.action`, {
...(typeof theParams.payload.currentValue === 'boolean' && {
context: theParams.payload.currentValue.toString()
})
})
}),
...(err.status &&
typeof err.status === 'number' &&
@ -70,93 +81,70 @@ const contextMenuAccount = ({
)
const ownAccount = instanceAccount?.id === accountId
const { data: relationship } = useRelationshipQuery({
id: accountId,
options: { enabled: type === 'account' }
})
if (!ownAccount) {
switch (Platform.OS) {
case 'ios':
actions.push({
id: 'account',
title: t('account.title'),
inlineChildren: true,
actions: [
{
id: 'account-mute',
title: t('account.mute.action'),
systemIcon: 'eye.slash'
},
{
id: 'account-block',
title: t('account.block.action'),
systemIcon: 'xmark.circle',
destructive: true
},
{
id: 'account-reports',
title: t('account.reports.action'),
systemIcon: 'flag',
destructive: true
}
]
})
break
default:
actions.push(
{
id: 'account-mute',
title: t('account.mute.action'),
systemIcon: 'eye.slash'
},
{
id: 'account-block',
title: t('account.block.action'),
systemIcon: 'xmark.circle',
destructive: true
},
{
id: 'account-reports',
title: t('account.reports.action'),
systemIcon: 'flag',
destructive: true
}
)
break
}
actions.push(
{
id: 'account-mute',
title: t('account.mute.action', {
context: (relationship?.muting || false).toString()
}),
systemIcon: 'eye.slash'
},
{
id: 'account-block',
title: t('account.block.action', {
context: (relationship?.blocking || false).toString()
}),
systemIcon: 'xmark.circle',
destructive: true
},
{
id: 'account-reports',
title: t('account.reports.action'),
systemIcon: 'flag',
destructive: true
}
)
}
return (id: string) => {
switch (id) {
case 'account-mute':
analytics('timeline_shared_headeractions_account_mute_press', {
page: queryKey && queryKey[1].page
})
mutateion.mutate({
type: 'updateAccountProperty',
queryKey,
id: accountId,
payload: { property: 'mute' }
})
break
case 'account-block':
analytics('timeline_shared_headeractions_account_block_press', {
page: queryKey && queryKey[1].page
})
mutateion.mutate({
type: 'updateAccountProperty',
queryKey,
id: accountId,
payload: { property: 'block' }
})
break
case 'account-report':
analytics('timeline_shared_headeractions_account_reports_press', {
page: queryKey && queryKey[1].page
})
mutateion.mutate({
type: 'updateAccountProperty',
queryKey,
id: accountId,
payload: { property: 'reports' }
})
break
return (index: number) => {
if (actions[index].id === 'account-mute') {
analytics('timeline_shared_headeractions_account_mute_press', {
page: queryKey && queryKey[1].page
})
mutateion.mutate({
type: 'updateAccountProperty',
queryKey,
id: accountId,
payload: { property: 'mute', currentValue: relationship?.muting }
})
}
if (actions[index].id === 'account-block') {
analytics('timeline_shared_headeractions_account_block_press', {
page: queryKey && queryKey[1].page
})
mutateion.mutate({
type: 'updateAccountProperty',
queryKey,
id: accountId,
payload: { property: 'block', currentValue: relationship?.blocking }
})
}
if (actions[index].id === 'account-report') {
analytics('timeline_shared_headeractions_account_reports_press', {
page: queryKey && queryKey[1].page
})
mutateion.mutate({
type: 'updateAccountProperty',
queryKey,
id: accountId,
payload: { property: 'reports' }
})
}
}
}

View File

@ -71,36 +71,38 @@ const contextMenuInstance = ({
}
}
return (id: string) => {
switch (id) {
case 'instance-block':
analytics('timeline_shared_headeractions_domain_block_press', {
page: queryKey[1].page
})
Alert.alert(
t('instance.block.alert.title', { instance }),
t('instance.block.alert.message'),
[
{
text: t('instance.block.alert.buttons.confirm'),
style: 'destructive',
onPress: () => {
analytics(
'timeline_shared_headeractions_domain_block_confirm',
{ page: queryKey && queryKey[1].page }
)
mutation.mutate({
type: 'domainBlock',
queryKey,
domain: instance
})
}
},
{
text: t('common:buttons.cancel')
return (index: number) => {
if (
actions[index].id === 'instance-block' ||
(actions[index].id === 'instance' &&
actions[index].actions?.[0].id === 'instance-block')
) {
analytics('timeline_shared_headeractions_domain_block_press', {
page: queryKey[1].page
})
Alert.alert(
t('instance.block.alert.title', { instance }),
t('instance.block.alert.message'),
[
{
text: t('instance.block.alert.buttons.confirm'),
style: 'destructive',
onPress: () => {
analytics('timeline_shared_headeractions_domain_block_confirm', {
page: queryKey && queryKey[1].page
})
mutation.mutate({
type: 'domainBlock',
queryKey,
domain: instance
})
}
]
)
},
{
text: t('common:buttons.cancel')
}
]
)
}
}
}

View File

@ -18,19 +18,17 @@ const contextMenuShare = ({ actions, type, url }: Props) => {
systemIcon: 'square.and.arrow.up'
})
return (id: string) => {
switch (id) {
case 'share':
analytics('timeline_shared_headeractions_share_press')
switch (Platform.OS) {
case 'ios':
Share.share({ url })
break
case 'android':
Share.share({ message: url })
break
}
break
return (index: number) => {
if (actions[index].id === 'share') {
analytics('timeline_shared_headeractions_share_press')
switch (Platform.OS) {
case 'ios':
Share.share({ url })
break
case 'android':
Share.share({ message: url })
break
}
}
}
}

View File

@ -15,7 +15,7 @@ import {
} from '@utils/slices/instancesSlice'
import { useTheme } from '@utils/styles/ThemeManager'
import { useTranslation } from 'react-i18next'
import { Alert, Platform } from 'react-native'
import { Alert } from 'react-native'
import { ContextMenuAction } from 'react-native-context-menu-view'
import { useQueryClient } from 'react-query'
import { useSelector } from 'react-redux'
@ -88,7 +88,7 @@ const contextMenuStatus = ({
},
{
id: 'status-mute',
title: t('status.mute.action-muted', {
title: t('status.mute.action', {
context: status.muted.toString()
}),
systemIcon: status.muted ? 'speaker' : 'speaker.slash'
@ -107,178 +107,161 @@ const contextMenuStatus = ({
if (status.visibility === 'public' || status.visibility === 'unlisted') {
accountMenuItems.push({
id: 'status-pin',
title: t('status.pin.action-pinned', {
title: t('status.pin.action', {
context: status.pinned.toString()
}),
systemIcon: status.pinned ? 'pin.slash' : 'pin'
})
}
switch (Platform.OS) {
case 'ios':
actions.push({
id: 'status',
title: t('status.title'),
inlineChildren: true,
actions: accountMenuItems
})
break
default:
actions.push(...accountMenuItems)
break
}
actions.push(...accountMenuItems)
}
return async (id: string) => {
switch (id) {
case 'status-delete':
analytics('timeline_shared_headeractions_status_delete_press', {
page: queryKey && queryKey[1].page
})
Alert.alert(
t('status.delete.alert.title'),
t('status.delete.alert.message'),
[
{
text: t('status.delete.alert.buttons.confirm'),
style: 'destructive',
onPress: async () => {
analytics(
'timeline_shared_headeractions_status_delete_confirm',
{
page: queryKey && queryKey[1].page
}
)
mutation.mutate({
return async (index: number) => {
if (actions[index].id === 'status-delete') {
analytics('timeline_shared_headeractions_status_delete_press', {
page: queryKey && queryKey[1].page
})
Alert.alert(
t('status.delete.alert.title'),
t('status.delete.alert.message'),
[
{
text: t('status.delete.alert.buttons.confirm'),
style: 'destructive',
onPress: async () => {
analytics('timeline_shared_headeractions_status_delete_confirm', {
page: queryKey && queryKey[1].page
})
mutation.mutate({
type: 'deleteItem',
source: 'statuses',
queryKey,
rootQueryKey,
id: status.id
})
}
},
{
text: t('common:buttons.cancel')
}
]
)
}
if (actions[index].id === 'status-delete-edit') {
analytics('timeline_shared_headeractions_status_deleteedit_press', {
page: queryKey && queryKey[1].page
})
Alert.alert(
t('status.deleteEdit.alert.title'),
t('status.deleteEdit.alert.message'),
[
{
text: t('status.deleteEdit.alert.buttons.confirm'),
style: 'destructive',
onPress: async () => {
analytics(
'timeline_shared_headeractions_status_deleteedit_confirm',
{
page: queryKey && queryKey[1].page
}
)
let replyToStatus: Mastodon.Status | undefined = undefined
if (status.in_reply_to_id) {
replyToStatus = await apiInstance<Mastodon.Status>({
method: 'get',
url: `statuses/${status.in_reply_to_id}`
}).then(res => res.body)
}
mutation
.mutateAsync({
type: 'deleteItem',
source: 'statuses',
queryKey,
rootQueryKey,
id: status.id
})
}
},
{
text: t('common:buttons.cancel')
}
]
)
break
case 'status-delete-edit':
analytics('timeline_shared_headeractions_status_deleteedit_press', {
page: queryKey && queryKey[1].page
})
Alert.alert(
t('status.deleteEdit.alert.title'),
t('status.deleteEdit.alert.message'),
[
{
text: t('status.deleteEdit.alert.buttons.confirm'),
style: 'destructive',
onPress: async () => {
analytics(
'timeline_shared_headeractions_status_deleteedit_confirm',
{
page: queryKey && queryKey[1].page
}
)
let replyToStatus: Mastodon.Status | undefined = undefined
if (status.in_reply_to_id) {
replyToStatus = await apiInstance<Mastodon.Status>({
method: 'get',
url: `statuses/${status.in_reply_to_id}`
}).then(res => res.body)
}
mutation
.mutateAsync({
type: 'deleteItem',
source: 'statuses',
queryKey,
id: status.id
.then(res => {
navigation.navigate('Screen-Compose', {
type: 'deleteEdit',
incomingStatus: res.body as Mastodon.Status,
...(replyToStatus && { replyToStatus }),
queryKey
})
.then(res => {
navigation.navigate('Screen-Compose', {
type: 'deleteEdit',
incomingStatus: res.body as Mastodon.Status,
...(replyToStatus && { replyToStatus }),
queryKey
})
})
}
},
{
text: t('common:buttons.cancel')
})
}
]
)
break
case 'status-mute':
analytics('timeline_shared_headeractions_status_mute_press', {
page: queryKey && queryKey[1].page
})
mutation.mutate({
type: 'updateStatusProperty',
queryKey,
rootQueryKey,
id: status.id,
payload: {
property: 'muted',
currentValue: status.muted,
propertyCount: undefined,
countValue: undefined
},
{
text: t('common:buttons.cancel')
}
})
break
case 'status-edit':
analytics('timeline_shared_headeractions_status_edit_press', {
page: queryKey && queryKey[1].page
})
let replyToStatus: Mastodon.Status | undefined = undefined
if (status.in_reply_to_id) {
replyToStatus = await apiInstance<Mastodon.Status>({
method: 'get',
url: `statuses/${status.in_reply_to_id}`
}).then(res => res.body)
]
)
}
if (actions[index].id === 'status-mute') {
analytics('timeline_shared_headeractions_status_mute_press', {
page: queryKey && queryKey[1].page
})
mutation.mutate({
type: 'updateStatusProperty',
queryKey,
rootQueryKey,
id: status.id,
payload: {
property: 'muted',
currentValue: status.muted,
propertyCount: undefined,
countValue: undefined
}
apiInstance<{
id: Mastodon.Status['id']
text: NonNullable<Mastodon.Status['text']>
spoiler_text: Mastodon.Status['spoiler_text']
}>({
})
}
if (actions[index].id === 'status-edit') {
analytics('timeline_shared_headeractions_status_edit_press', {
page: queryKey && queryKey[1].page
})
let replyToStatus: Mastodon.Status | undefined = undefined
if (status.in_reply_to_id) {
replyToStatus = await apiInstance<Mastodon.Status>({
method: 'get',
url: `statuses/${status.id}/source`
}).then(res => {
navigation.navigate('Screen-Compose', {
type: 'edit',
incomingStatus: {
...status,
text: res.body.text,
spoiler_text: res.body.spoiler_text
},
...(replyToStatus && { replyToStatus }),
queryKey,
rootQueryKey
})
})
break
case 'status-pin':
// Also note that reblogs cannot be pinned.
analytics('timeline_shared_headeractions_status_pin_press', {
page: queryKey && queryKey[1].page
})
mutation.mutate({
type: 'updateStatusProperty',
url: `statuses/${status.in_reply_to_id}`
}).then(res => res.body)
}
apiInstance<{
id: Mastodon.Status['id']
text: NonNullable<Mastodon.Status['text']>
spoiler_text: Mastodon.Status['spoiler_text']
}>({
method: 'get',
url: `statuses/${status.id}/source`
}).then(res => {
navigation.navigate('Screen-Compose', {
type: 'edit',
incomingStatus: {
...status,
text: res.body.text,
spoiler_text: res.body.spoiler_text
},
...(replyToStatus && { replyToStatus }),
queryKey,
rootQueryKey,
id: status.id,
payload: {
property: 'pinned',
currentValue: status.pinned,
propertyCount: undefined,
countValue: undefined
}
rootQueryKey
})
break
})
}
if (actions[index].id === 'status-pin') {
// Also note that reblogs cannot be pinned.
analytics('timeline_shared_headeractions_status_pin_press', {
page: queryKey && queryKey[1].page
})
mutation.mutate({
type: 'updateStatusProperty',
queryKey,
rootQueryKey,
id: status.id,
payload: {
property: 'pinned',
currentValue: status.pinned,
propertyCount: undefined,
countValue: undefined
}
})
}
}
}

View File

@ -3,11 +3,10 @@ import { FormattedRelativeTime } from 'react-intl'
import { AppState } from 'react-native'
export interface Props {
type: 'past' | 'future'
time: string | number
}
const RelativeTime: React.FC<Props> = ({ type, time }) => {
const RelativeTime: React.FC<Props> = ({ time }) => {
const [now, setNow] = useState(new Date().getTime())
useEffect(() => {
const appStateListener = AppState.addEventListener('change', state => {
@ -21,9 +20,7 @@ const RelativeTime: React.FC<Props> = ({ type, time }) => {
return (
<FormattedRelativeTime
value={
((type === 'past' ? -1 : 1) * (now - new Date(time).getTime())) / 1000
}
value={(new Date(time).getTime() - now) / 1000}
updateIntervalInSeconds={1}
/>
)

View File

@ -47,6 +47,7 @@ const TimelineContextMenu: React.FC<Props & ContextMenuProps> = ({
})
const accountOnPress = contextMenuAccount({
actions,
type: 'status',
queryKey,
rootQueryKey,
id: status.account.id
@ -62,14 +63,14 @@ const TimelineContextMenu: React.FC<Props & ContextMenuProps> = ({
<ContextMenuContext.Provider value={actions}>
<ContextMenu
actions={actions}
onPress={({ nativeEvent: { id } }) => {
onPress={({ nativeEvent: { index } }) => {
for (const on of [
shareOnPress,
statusOnPress,
accountOnPress,
instanceOnPress
]) {
on && on(id)
on && on(index)
}
}}
children={children}

View File

@ -32,7 +32,7 @@ const HeaderSharedCreated = React.memo(
/>
</>
) : (
<RelativeTime type='past' time={actualTime} />
<RelativeTime time={actualTime} />
)}
</CustomText>
{edited_at ? (

View File

@ -269,7 +269,7 @@ const TimelinePoll: React.FC<Props> = ({
))
}, [theme, allOptions])
const pollVoteCounts = useMemo(() => {
const pollVoteCounts = () => {
if (poll.voters_count !== null) {
return (
t('shared.poll.meta.count.voters', { count: poll.voters_count }) + ' • '
@ -279,9 +279,9 @@ const TimelinePoll: React.FC<Props> = ({
t('shared.poll.meta.count.votes', { count: poll.votes_count }) + ' • '
)
}
}, [poll.voters_count, poll.votes_count])
}
const pollExpiration = useMemo(() => {
const pollExpiration = () => {
if (poll.expired) {
return t('shared.poll.meta.expiration.expired')
} else {
@ -289,12 +289,12 @@ const TimelinePoll: React.FC<Props> = ({
return (
<Trans
i18nKey='componentTimeline:shared.poll.meta.expiration.until'
components={[<RelativeTime type='future' time={poll.expires_at} />]}
components={[<RelativeTime time={poll.expires_at} />]}
/>
)
}
}
}, [theme, i18n.language, poll.expired, poll.expires_at])
}
return (
<View style={{ marginTop: StyleConstants.Spacing.M }}>
@ -312,8 +312,8 @@ const TimelinePoll: React.FC<Props> = ({
fontStyle='S'
style={{ flexShrink: 1, color: colors.secondary }}
>
{pollVoteCounts}
{pollExpiration}
{pollVoteCounts()}
{pollExpiration()}
</CustomText>
</View>
</View>

View File

@ -3,10 +3,12 @@
"account": {
"title": "User actions",
"mute": {
"action": "Mute user"
"action_false": "Mute user",
"action_true": "Unmute user"
},
"block": {
"action": "Block user"
"action_false": "Block user",
"action_true": "Unblock user"
},
"reports": {
"action": "Report user"
@ -59,12 +61,12 @@
}
},
"mute": {
"action-muted_false": "Mute toot and replies",
"action-muted_true": "Unmute toot and replies"
"action_false": "Mute toot and replies",
"action_true": "Unmute toot and replies"
},
"pin": {
"action-pinned_false": "Pin toot",
"action-pinned_true": "Unpin toot"
"action_false": "Pin toot",
"action_true": "Unpin toot"
}
}
}

View File

@ -139,7 +139,7 @@
},
"expiration": {
"expired": "Vote expired",
"until": "Expires in <0 />"
"until": "Expires <0 />"
}
}
}

View File

@ -93,7 +93,7 @@ const ScreenAnnouncements: React.FC<
<Trans
i18nKey='screenAnnouncements:content.published'
components={[
<RelativeTime type='past' time={item.published_at} />
<RelativeTime time={item.published_at} />
]}
/>
</CustomText>

View File

@ -99,7 +99,8 @@ const TabSharedAccount: React.FC<
customProps={{
renderItem,
onScroll,
ListHeaderComponent
ListHeaderComponent,
maintainVisibleContentPosition: undefined
}}
/>
</>

View File

@ -63,15 +63,16 @@ const TabSharedRoot = ({
})
const accountOnPress = contextMenuAccount({
actions,
type: 'account',
id: account.id
})
return (
<ContextMenu
actions={actions}
onPress={({ nativeEvent: { id } }) => {
shareOnPress(id)
accountOnPress(id)
onPress={({ nativeEvent: { index } }) => {
shareOnPress(index)
accountOnPress(index)
}}
dropdownMenuMode
>

View File

@ -314,6 +314,7 @@ export type MutationVarsTimelineUpdateAccountProperty = {
id: Mastodon.Account['id']
payload: {
property: 'mute' | 'block' | 'reports'
currentValue?: boolean
}
}
@ -383,7 +384,9 @@ const mutationFunction = async (params: MutationVarsTimeline) => {
case 'mute':
return apiInstance<Mastodon.Account>({
method: 'post',
url: `accounts/${params.id}/${params.payload.property}`
url: `accounts/${params.id}/${
params.payload.currentValue ? 'un' : ''
}${params.payload.property}`
})
case 'reports':
return apiInstance<Mastodon.Account>({