mirror of https://github.com/tooot-app/app
commit
772459d4ae
|
@ -108,6 +108,29 @@ private_lane :build_android do
|
|||
end
|
||||
end
|
||||
|
||||
desc "Build Android apk"
|
||||
private_lane :build_android_apk do
|
||||
sh("echo #{ENV["ANDROID_KEYSTORE"]} | base64 -d | tee #{File.expand_path('..', Dir.pwd)}/android/tooot.jks >/dev/null", log: false)
|
||||
|
||||
prepare_playstore_android
|
||||
|
||||
build_android_app(
|
||||
task: 'assemble',
|
||||
build_type: 'release',
|
||||
project_dir: "./android",
|
||||
print_command: true,
|
||||
print_command_output: true,
|
||||
properties: {
|
||||
"android.injected.signing.store.file" => "#{File.expand_path('..', Dir.pwd)}/android/tooot.jks",
|
||||
"android.injected.signing.store.password" => ENV["ANDROID_KEYSTORE_PASSWORD"],
|
||||
"android.injected.signing.key.alias" => ENV["ANDROID_KEYSTORE_ALIAS"],
|
||||
"android.injected.signing.key.password" => ENV["ANDROID_KEYSTORE_KEY_PASSWORD"],
|
||||
}
|
||||
)
|
||||
|
||||
sh "mv #{lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH]} #{File.expand_path('..', Dir.pwd)}/tooot-#{GITHUB_RELEASE}.apk"
|
||||
end
|
||||
|
||||
lane :ios do
|
||||
cocoapods(clean_install: true, podfile: "./ios/Podfile")
|
||||
build_ios
|
||||
|
@ -121,6 +144,7 @@ end
|
|||
|
||||
lane :release do
|
||||
if ENVIRONMENT == 'release'
|
||||
build_android_apk
|
||||
set_github_release(
|
||||
repository_name: GITHUB_REPO,
|
||||
name: GITHUB_RELEASE,
|
||||
|
@ -128,6 +152,7 @@ lane :release do
|
|||
description: "No changelog provided",
|
||||
commitish: git_branch,
|
||||
is_prerelease: false
|
||||
upload_assets: ["#{File.expand_path('..', Dir.pwd)}/tooot-#{GITHUB_RELEASE}.apk"]
|
||||
)
|
||||
end
|
||||
rocket
|
||||
|
|
|
@ -1 +1,5 @@
|
|||
Enjoy using tooot
|
||||
Enjoy toooting! This version includes following improvements and fixes:
|
||||
- Automatic setting detected language when tooting
|
||||
- Added notification for admins
|
||||
- Fix whole word filter matching
|
||||
- Fix tablet cannot delete toot drafts
|
|
@ -1 +1,5 @@
|
|||
tooot使用愉快
|
||||
toooting愉快!此版本包括以下改进和修复:
|
||||
- 自动识别发嘟语言
|
||||
- 新增管理员推送通知
|
||||
- 修复过滤整词功能
|
||||
- 修复平板不能删除草稿
|
|
@ -301,7 +301,7 @@ PODS:
|
|||
- React-Core
|
||||
- react-native-ios-context-menu (1.15.1):
|
||||
- React-Core
|
||||
- react-native-language-detection (0.1.0):
|
||||
- react-native-language-detection (0.2.2):
|
||||
- React
|
||||
- react-native-live-text-image-view (0.4.0):
|
||||
- React-Core
|
||||
|
@ -739,7 +739,7 @@ SPEC CHECKSUMS:
|
|||
react-native-cameraroll: a40b082318eb1ecd0336a2f29d9f74b7f2c8cae8
|
||||
react-native-image-picker: bf34f3f516d139ed3e24c5f5a381a91819e349ea
|
||||
react-native-ios-context-menu: b170594b4448c0cd10c79e13432216bac99de1ac
|
||||
react-native-language-detection: 0e43195ad014974f1b7a31b64820eff34a243f2d
|
||||
react-native-language-detection: f414937fa715108ab50a6269a3de0bcb95e4ceb0
|
||||
react-native-live-text-image-view: 483bacfdba464162b8cf176bba555364f18b584c
|
||||
react-native-menu: 8e172cfcf0e42e92f028e7781eddf84d430cae24
|
||||
react-native-netinfo: 2517ad504b3d303e90d7a431b0fcaef76d207983
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "tooot",
|
||||
"version": "4.6.6",
|
||||
"version": "4.7.0",
|
||||
"description": "tooot for Mastodon",
|
||||
"author": "xmflsct <me@xmflsct.com>",
|
||||
"license": "GPL-3.0-or-later",
|
||||
|
@ -12,7 +12,7 @@
|
|||
"start": "react-native start",
|
||||
"android": "react-native run-android",
|
||||
"iphone": "react-native run-ios --simulator 'iPhone 14 Pro'",
|
||||
"ipad": "react-native run-ios --simulator 'iPad Pro (11-inch) (3rd generation)'",
|
||||
"ipad": "react-native run-ios --simulator 'iPad Pro (11-inch) (4th generation)'",
|
||||
"app:build": "bundle exec fastlane",
|
||||
"clean": "react-native-clean-project",
|
||||
"postinstall": "patch-package"
|
||||
|
@ -78,7 +78,7 @@
|
|||
"react-native-htmlview": "^0.16.0",
|
||||
"react-native-image-picker": "^4.10.2",
|
||||
"react-native-ios-context-menu": "^1.15.1",
|
||||
"react-native-language-detection": "^0.1.0",
|
||||
"react-native-language-detection": "^0.2.2",
|
||||
"react-native-live-text-image-view": "^0.4.0",
|
||||
"react-native-pager-view": "^6.1.2",
|
||||
"react-native-reanimated": "^2.13.0",
|
||||
|
|
|
@ -30,6 +30,7 @@ declare namespace Mastodon {
|
|||
bot: boolean
|
||||
source?: Source
|
||||
suspended?: boolean
|
||||
role?: Role
|
||||
}
|
||||
|
||||
type Announcement = {
|
||||
|
@ -384,6 +385,8 @@ declare namespace Mastodon {
|
|||
mention: boolean
|
||||
poll: boolean
|
||||
status: boolean
|
||||
'admin.sign_up': boolean
|
||||
'admin.report': boolean
|
||||
}
|
||||
server_key: string
|
||||
}
|
||||
|
@ -409,6 +412,18 @@ declare namespace Mastodon {
|
|||
hashtags?: Tag[]
|
||||
}
|
||||
|
||||
type Role = {
|
||||
// Added since 4.0
|
||||
id: string
|
||||
name: string
|
||||
color: string
|
||||
position: number
|
||||
permissions: string
|
||||
highlighted: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
type Status = {
|
||||
// Base
|
||||
id: string
|
||||
|
@ -479,25 +494,4 @@ declare namespace Mastodon {
|
|||
history: { day: string; accounts: string; uses: string }[]
|
||||
following: boolean // Since v4.0
|
||||
}
|
||||
|
||||
type WebSocketStream =
|
||||
| 'user'
|
||||
| 'public'
|
||||
| 'public:local'
|
||||
| 'hashtag'
|
||||
| 'hashtag:local'
|
||||
| 'list'
|
||||
| 'direct'
|
||||
type WebSocket =
|
||||
| {
|
||||
stream: WebSocketStream[]
|
||||
event: 'update'
|
||||
payload: string // Status
|
||||
}
|
||||
| { stream: WebSocketStream[]; event: 'delete'; payload: Status['id'] }
|
||||
| {
|
||||
stream: WebSocketStream[]
|
||||
event: 'notification'
|
||||
payload: string // Notification
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ export interface Props {
|
|||
}
|
||||
|
||||
const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
||||
const { i18n, t } = useTranslation('screens')
|
||||
const { t } = useTranslation('screens')
|
||||
const dispatch = useAppDispatch()
|
||||
const instanceActive = useSelector(getInstanceActive)
|
||||
const { colors, theme } = useTheme()
|
||||
|
@ -70,8 +70,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
|||
displayMessage({
|
||||
message: t('localCorrupt.message'),
|
||||
description: localCorrupt.length ? localCorrupt : undefined,
|
||||
type: 'error',
|
||||
theme
|
||||
type: 'danger'
|
||||
})
|
||||
// @ts-ignore
|
||||
navigationRef.navigate('Screen-Tabs', {
|
||||
|
@ -136,10 +135,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
|||
instance => paths[0] === `@${instance.account.acct}@${instance.uri}`
|
||||
)
|
||||
if (instanceIndex !== -1 && instanceActive !== instanceIndex) {
|
||||
initQuery({
|
||||
instance: instances[instanceIndex],
|
||||
prefetch: { enabled: true }
|
||||
})
|
||||
initQuery({ instance: instances[instanceIndex] })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -183,8 +179,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
|||
message: t('shareError.imageNotSupported', {
|
||||
type: mime.split('/')[1]
|
||||
}),
|
||||
type: 'error',
|
||||
theme
|
||||
type: 'danger'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
@ -196,8 +191,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
|||
message: t('shareError.videoNotSupported', {
|
||||
type: mime.split('/')[1]
|
||||
}),
|
||||
type: 'error',
|
||||
theme
|
||||
type: 'danger'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
|
|
@ -22,13 +22,13 @@ const apiGeneral = async <T = unknown>({
|
|||
}: Params): Promise<{ body: T }> => {
|
||||
console.log(
|
||||
ctx.bgGreen.bold(' API general ') +
|
||||
' ' +
|
||||
domain +
|
||||
' ' +
|
||||
method +
|
||||
ctx.green(' -> ') +
|
||||
`/${url}` +
|
||||
(params ? ctx.green(' -> ') : ''),
|
||||
' ' +
|
||||
domain +
|
||||
' ' +
|
||||
method +
|
||||
ctx.green(' -> ') +
|
||||
`/${url}` +
|
||||
(params ? ctx.green(' -> ') : ''),
|
||||
params ? params : ''
|
||||
)
|
||||
|
||||
|
@ -39,10 +39,7 @@ const apiGeneral = async <T = unknown>({
|
|||
url,
|
||||
params,
|
||||
headers: {
|
||||
'Content-Type':
|
||||
body && body instanceof FormData
|
||||
? 'multipart/form-data'
|
||||
: 'application/json',
|
||||
'Content-Type': body && body instanceof FormData ? 'multipart/form-data' : 'application/json',
|
||||
Accept: '*/*',
|
||||
...userAgent,
|
||||
...headers
|
||||
|
@ -54,7 +51,7 @@ const apiGeneral = async <T = unknown>({
|
|||
body: response.data
|
||||
})
|
||||
})
|
||||
.catch(handleError)
|
||||
.catch(handleError())
|
||||
}
|
||||
|
||||
export default apiGeneral
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import * as Sentry from '@sentry/react-native'
|
||||
import chalk from 'chalk'
|
||||
import Constants from 'expo-constants'
|
||||
import { Platform } from 'react-native'
|
||||
|
@ -7,30 +8,59 @@ const userAgent = {
|
|||
}
|
||||
|
||||
const ctx = new chalk.Instance({ level: 3 })
|
||||
const handleError = (error: any) => {
|
||||
if (error?.response) {
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
console.error(
|
||||
ctx.bold(' API '),
|
||||
ctx.bold('response'),
|
||||
error.response.status,
|
||||
error?.response.data?.error || error?.response.message || 'Unknown error'
|
||||
)
|
||||
return Promise.reject({
|
||||
status: error?.response.status,
|
||||
message: error?.response.data?.error || error?.response.message || 'Unknown error'
|
||||
})
|
||||
} else if (error?.request) {
|
||||
// The request was made but no response was received
|
||||
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
|
||||
// http.ClientRequest in node.js
|
||||
console.error(ctx.bold(' API '), ctx.bold('request'), error)
|
||||
return Promise.reject()
|
||||
} else {
|
||||
console.error(ctx.bold(' API '), ctx.bold('internal'), error?.message)
|
||||
return Promise.reject()
|
||||
const handleError =
|
||||
(
|
||||
config: {
|
||||
message: string
|
||||
captureRequest?: { url: string; params: any; body: any }
|
||||
captureResponse?: boolean
|
||||
} | void
|
||||
) =>
|
||||
(error: any) => {
|
||||
const shouldReportToSentry = config && (config.captureRequest || config.captureResponse)
|
||||
shouldReportToSentry && Sentry.setContext('Error object', error)
|
||||
|
||||
if (config?.captureRequest) {
|
||||
Sentry.setContext('Error request', config.captureRequest)
|
||||
}
|
||||
|
||||
if (error?.response) {
|
||||
if (config?.captureResponse) {
|
||||
Sentry.setContext('Error response', {
|
||||
data: error.response.data,
|
||||
status: error.response.status,
|
||||
headers: error.response.headers
|
||||
})
|
||||
}
|
||||
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
console.error(
|
||||
ctx.bold(' API '),
|
||||
ctx.bold('response'),
|
||||
error.response.status,
|
||||
error?.response.data?.error || error?.response.message || 'Unknown error'
|
||||
)
|
||||
|
||||
shouldReportToSentry && Sentry.captureMessage(config.message)
|
||||
return Promise.reject({
|
||||
status: error?.response.status,
|
||||
message: error?.response.data?.error || error?.response.message || 'Unknown error'
|
||||
})
|
||||
} else if (error?.request) {
|
||||
// The request was made but no response was received
|
||||
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
|
||||
// http.ClientRequest in node.js
|
||||
console.error(ctx.bold(' API '), ctx.bold('request'), error)
|
||||
|
||||
shouldReportToSentry && Sentry.captureMessage(config.message)
|
||||
return Promise.reject()
|
||||
} else {
|
||||
console.error(ctx.bold(' API '), ctx.bold('internal'), error?.message)
|
||||
|
||||
shouldReportToSentry && Sentry.captureMessage(config.message)
|
||||
return Promise.reject()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { ctx, handleError, userAgent }
|
||||
|
|
|
@ -86,7 +86,7 @@ const apiInstance = async <T = unknown>({
|
|||
links: { prev, next }
|
||||
})
|
||||
})
|
||||
.catch(handleError)
|
||||
.catch(handleError())
|
||||
}
|
||||
|
||||
export default apiInstance
|
||||
|
|
|
@ -55,16 +55,13 @@ const apiTooot = async <T = unknown>({
|
|||
body: response.data
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
Sentry.setContext('API request', { url, params, body })
|
||||
Sentry.setContext('Error response', {
|
||||
...(error?.response && { response: error.response?._response })
|
||||
.catch(
|
||||
handleError({
|
||||
message: 'API error',
|
||||
captureRequest: { url, params, body },
|
||||
captureResponse: true
|
||||
})
|
||||
Sentry.setContext('Error object', { error })
|
||||
Sentry.captureMessage('API error')
|
||||
|
||||
return handleError(error)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
export default apiTooot
|
||||
|
|
|
@ -12,11 +12,7 @@ interface Props {
|
|||
additionalActions?: () => void
|
||||
}
|
||||
|
||||
const AccountButton: React.FC<Props> = ({
|
||||
instance,
|
||||
selected = false,
|
||||
additionalActions
|
||||
}) => {
|
||||
const AccountButton: React.FC<Props> = ({ instance, selected = false, additionalActions }) => {
|
||||
const navigation = useNavigation()
|
||||
|
||||
return (
|
||||
|
@ -27,12 +23,10 @@ const AccountButton: React.FC<Props> = ({
|
|||
marginBottom: StyleConstants.Spacing.M,
|
||||
marginRight: StyleConstants.Spacing.M
|
||||
}}
|
||||
content={`@${instance.account.acct}@${instance.uri}${
|
||||
selected ? ' ✓' : ''
|
||||
}`}
|
||||
content={`@${instance.account.acct}@${instance.uri}${selected ? ' ✓' : ''}`}
|
||||
onPress={() => {
|
||||
haptics('Light')
|
||||
initQuery({ instance, prefetch: { enabled: true } })
|
||||
initQuery({ instance })
|
||||
navigation.goBack()
|
||||
if (additionalActions) {
|
||||
additionalActions()
|
||||
|
|
|
@ -1,90 +0,0 @@
|
|||
import browserPackage from '@helpers/browserPackage'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { useAppDispatch } from '@root/store'
|
||||
import { InstanceLatest } from '@utils/migrations/instances/migration'
|
||||
import { TabMeStackNavigationProp } from '@utils/navigation/navigators'
|
||||
import addInstance from '@utils/slices/instances/add'
|
||||
import { checkInstanceFeature } from '@utils/slices/instancesSlice'
|
||||
import * as AuthSession from 'expo-auth-session'
|
||||
import React, { useEffect } from 'react'
|
||||
import { useQueryClient } from 'react-query'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
export interface Props {
|
||||
instanceDomain: string
|
||||
// Domain can be different than uri
|
||||
instance: Mastodon.Instance
|
||||
appData: InstanceLatest['appData']
|
||||
goBack?: boolean
|
||||
}
|
||||
|
||||
const InstanceAuth = React.memo(
|
||||
({ instanceDomain, instance, appData, goBack }: Props) => {
|
||||
const redirectUri = AuthSession.makeRedirectUri({
|
||||
native: 'tooot://instance-auth',
|
||||
useProxy: false
|
||||
})
|
||||
|
||||
const navigation = useNavigation<TabMeStackNavigationProp<'Tab-Me-Root' | 'Tab-Me-Switch'>>()
|
||||
const queryClient = useQueryClient()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const deprecateAuthFollow = useSelector(checkInstanceFeature('deprecate_auth_follow'))
|
||||
const [request, response, promptAsync] = AuthSession.useAuthRequest(
|
||||
{
|
||||
clientId: appData.clientId,
|
||||
clientSecret: appData.clientSecret,
|
||||
scopes: deprecateAuthFollow
|
||||
? ['read', 'write', 'push']
|
||||
: ['read', 'write', 'follow', 'push'],
|
||||
redirectUri
|
||||
},
|
||||
{
|
||||
authorizationEndpoint: `https://${instanceDomain}/oauth/authorize`
|
||||
}
|
||||
)
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
if (request?.clientId) {
|
||||
await promptAsync({ browserPackage: await browserPackage() }).catch(e => console.log(e))
|
||||
}
|
||||
})()
|
||||
}, [request])
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
if (response?.type === 'success') {
|
||||
const { accessToken } = await AuthSession.exchangeCodeAsync(
|
||||
{
|
||||
clientId: appData.clientId,
|
||||
clientSecret: appData.clientSecret,
|
||||
scopes: ['read', 'write', 'follow', 'push'],
|
||||
redirectUri,
|
||||
code: response.params.code,
|
||||
extraParams: {
|
||||
grant_type: 'authorization_code'
|
||||
}
|
||||
},
|
||||
{
|
||||
tokenEndpoint: `https://${instanceDomain}/oauth/token`
|
||||
}
|
||||
)
|
||||
queryClient.clear()
|
||||
dispatch(
|
||||
addInstance({
|
||||
domain: instanceDomain,
|
||||
token: accessToken,
|
||||
instance,
|
||||
appData
|
||||
})
|
||||
)
|
||||
goBack && navigation.goBack()
|
||||
}
|
||||
})()
|
||||
}, [response])
|
||||
|
||||
return <></>
|
||||
},
|
||||
() => true
|
||||
)
|
||||
|
||||
export default InstanceAuth
|
|
@ -1,22 +1,27 @@
|
|||
import Button from '@components/Button'
|
||||
import Icon from '@components/Icon'
|
||||
import browserPackage from '@helpers/browserPackage'
|
||||
import { useAppsQuery } from '@utils/queryHooks/apps'
|
||||
import { redirectUri, useAppsMutation } from '@utils/queryHooks/apps'
|
||||
import { useInstanceQuery } from '@utils/queryHooks/instance'
|
||||
import { getInstances } from '@utils/slices/instancesSlice'
|
||||
import { checkInstanceFeature, getInstances } from '@utils/slices/instancesSlice'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import * as AuthSession from 'expo-auth-session'
|
||||
import * as WebBrowser from 'expo-web-browser'
|
||||
import { debounce } from 'lodash'
|
||||
import React, { RefObject, useCallback, useMemo, useState } from 'react'
|
||||
import React, { RefObject, useCallback, useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { Alert, Image, KeyboardAvoidingView, Platform, TextInput, View } from 'react-native'
|
||||
import { ScrollView } from 'react-native-gesture-handler'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { Placeholder } from 'rn-placeholder'
|
||||
import InstanceAuth from './Instance/Auth'
|
||||
import InstanceInfo from './Instance/Info'
|
||||
import CustomText from './Text'
|
||||
import InstanceInfo from './Info'
|
||||
import CustomText from '../Text'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { TabMeStackNavigationProp } from '@utils/navigation/navigators'
|
||||
import queryClient from '@helpers/queryClient'
|
||||
import { useAppDispatch } from '@root/store'
|
||||
import addInstance from '@utils/slices/instances/add'
|
||||
|
||||
export interface Props {
|
||||
scrollViewRef?: RefObject<ScrollView>
|
||||
|
@ -31,30 +36,64 @@ const ComponentInstance: React.FC<Props> = ({
|
|||
}) => {
|
||||
const { t } = useTranslation('componentInstance')
|
||||
const { colors, mode } = useTheme()
|
||||
const navigation = useNavigation<TabMeStackNavigationProp<'Tab-Me-Root' | 'Tab-Me-Switch'>>()
|
||||
|
||||
const [domain, setDomain] = useState<string>('')
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
const instances = useSelector(getInstances, () => true)
|
||||
const [domain, setDomain] = useState<string>()
|
||||
|
||||
const instanceQuery = useInstanceQuery({
|
||||
domain,
|
||||
options: { enabled: !!domain, retry: false }
|
||||
})
|
||||
const appsQuery = useAppsQuery({
|
||||
domain,
|
||||
options: { enabled: false, retry: false }
|
||||
})
|
||||
|
||||
const onChangeText = useCallback(
|
||||
debounce(
|
||||
text => {
|
||||
setDomain(text.replace(/^http(s)?\:\/\//i, ''))
|
||||
appsQuery.remove()
|
||||
},
|
||||
1000,
|
||||
{ trailing: true }
|
||||
),
|
||||
[]
|
||||
)
|
||||
const deprecateAuthFollow = useSelector(checkInstanceFeature('deprecate_auth_follow'))
|
||||
|
||||
const appsMutation = useAppsMutation({
|
||||
retry: false,
|
||||
onSuccess: async (data, variables) => {
|
||||
const clientId = data.client_id
|
||||
const clientSecret = data.client_secret
|
||||
|
||||
const discovery = { authorizationEndpoint: `https://${domain}/oauth/authorize` }
|
||||
|
||||
const request = new AuthSession.AuthRequest({
|
||||
clientId,
|
||||
clientSecret,
|
||||
scopes: deprecateAuthFollow
|
||||
? ['read', 'write', 'push']
|
||||
: ['read', 'write', 'follow', 'push'],
|
||||
redirectUri
|
||||
})
|
||||
await request.makeAuthUrlAsync(discovery)
|
||||
|
||||
const promptResult = await request.promptAsync(discovery)
|
||||
|
||||
if (promptResult?.type === 'success') {
|
||||
const { accessToken } = await AuthSession.exchangeCodeAsync(
|
||||
{
|
||||
clientId,
|
||||
clientSecret,
|
||||
scopes: ['read', 'write', 'follow', 'push'],
|
||||
redirectUri,
|
||||
code: promptResult.params.code,
|
||||
extraParams: { grant_type: 'authorization_code' }
|
||||
},
|
||||
{ tokenEndpoint: `https://${variables.domain}/oauth/token` }
|
||||
)
|
||||
queryClient.clear()
|
||||
dispatch(
|
||||
addInstance({
|
||||
domain,
|
||||
token: accessToken,
|
||||
instance: instanceQuery.data!,
|
||||
appData: { clientId, clientSecret }
|
||||
})
|
||||
)
|
||||
goBack && navigation.goBack()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const processUpdate = useCallback(() => {
|
||||
if (domain) {
|
||||
|
@ -66,39 +105,15 @@ const ComponentInstance: React.FC<Props> = ({
|
|||
},
|
||||
{
|
||||
text: t('common:buttons.continue'),
|
||||
onPress: () => {
|
||||
appsQuery.refetch()
|
||||
}
|
||||
onPress: () => appsMutation.mutate({ domain })
|
||||
}
|
||||
])
|
||||
} else {
|
||||
appsQuery.refetch()
|
||||
appsMutation.mutate({ domain })
|
||||
}
|
||||
}
|
||||
}, [domain])
|
||||
|
||||
const requestAuth = useMemo(() => {
|
||||
if (
|
||||
domain &&
|
||||
instanceQuery.data?.uri &&
|
||||
appsQuery.data?.client_id &&
|
||||
appsQuery.data.client_secret
|
||||
) {
|
||||
return (
|
||||
<InstanceAuth
|
||||
key={Math.random()}
|
||||
instanceDomain={domain}
|
||||
instance={instanceQuery.data}
|
||||
appData={{
|
||||
clientId: appsQuery.data.client_id,
|
||||
clientSecret: appsQuery.data.client_secret
|
||||
}}
|
||||
goBack={goBack}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}, [domain, instanceQuery.data, appsQuery.data])
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
style={{ flex: 1 }}
|
||||
|
@ -145,7 +160,9 @@ const ComponentInstance: React.FC<Props> = ({
|
|||
color: colors.primaryDefault,
|
||||
borderBottomColor: instanceQuery.isError ? colors.red : colors.border
|
||||
}}
|
||||
onChangeText={onChangeText}
|
||||
onChangeText={debounce(text => setDomain(text.replace(/^http(s)?\:\/\//i, '')), 1000, {
|
||||
trailing: true
|
||||
})}
|
||||
autoCapitalize='none'
|
||||
clearButtonMode='never'
|
||||
keyboardType='url'
|
||||
|
@ -176,7 +193,7 @@ const ComponentInstance: React.FC<Props> = ({
|
|||
content={t('server.button')}
|
||||
onPress={processUpdate}
|
||||
disabled={!instanceQuery.data?.uri}
|
||||
loading={instanceQuery.isFetching || appsQuery.isFetching}
|
||||
loading={instanceQuery.isFetching || appsMutation.isLoading}
|
||||
/>
|
||||
</View>
|
||||
|
||||
|
@ -276,8 +293,6 @@ const ComponentInstance: React.FC<Props> = ({
|
|||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{requestAuth}
|
||||
</KeyboardAvoidingView>
|
||||
)
|
||||
}
|
|
@ -1,10 +1,9 @@
|
|||
import Icon from '@components/Icon'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { getColors, Theme } from '@utils/styles/themes'
|
||||
import React, { RefObject } from 'react'
|
||||
import { AccessibilityInfo } from 'react-native'
|
||||
import FlashMessage, { hideMessage, showMessage } from 'react-native-flash-message'
|
||||
import FlashMessage, { MessageType, showMessage } from 'react-native-flash-message'
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import haptics from './haptics'
|
||||
|
||||
|
@ -15,107 +14,80 @@ const displayMessage = ({
|
|||
message,
|
||||
description,
|
||||
onPress,
|
||||
theme,
|
||||
type
|
||||
}:
|
||||
| {
|
||||
ref?: RefObject<FlashMessage>
|
||||
duration?: 'short' | 'long'
|
||||
autoHide?: boolean
|
||||
message: string
|
||||
description?: string
|
||||
onPress?: () => void
|
||||
theme?: undefined
|
||||
type?: undefined
|
||||
}
|
||||
| {
|
||||
ref?: RefObject<FlashMessage>
|
||||
duration?: 'short' | 'long'
|
||||
autoHide?: boolean
|
||||
message: string
|
||||
description?: string
|
||||
onPress?: () => void
|
||||
theme: Theme
|
||||
type: 'success' | 'error' | 'warning'
|
||||
}) => {
|
||||
}: {
|
||||
ref?: RefObject<FlashMessage>
|
||||
duration?: 'short' | 'long'
|
||||
autoHide?: boolean
|
||||
message: string
|
||||
description?: string
|
||||
onPress?: () => void
|
||||
type?: MessageType
|
||||
}) => {
|
||||
AccessibilityInfo.announceForAccessibility(message + '.' + description)
|
||||
|
||||
enum iconMapping {
|
||||
success = 'CheckCircle',
|
||||
error = 'XCircle',
|
||||
warning = 'AlertCircle'
|
||||
}
|
||||
enum colorMapping {
|
||||
success = 'blue',
|
||||
error = 'red',
|
||||
warning = 'secondary'
|
||||
}
|
||||
|
||||
if (type && type === 'error') {
|
||||
if (type && type === 'danger') {
|
||||
haptics('Error')
|
||||
}
|
||||
|
||||
if (ref) {
|
||||
ref.current?.showMessage({
|
||||
duration: type === 'error' ? 8000 : duration === 'short' ? 3000 : 5000,
|
||||
duration: type === 'danger' ? 8000 : duration === 'short' ? 3000 : 5000,
|
||||
autoHide,
|
||||
message,
|
||||
description,
|
||||
onPress,
|
||||
...(theme &&
|
||||
type && {
|
||||
renderFlashMessageIcon: () => {
|
||||
return (
|
||||
<Icon
|
||||
name={iconMapping[type]}
|
||||
size={StyleConstants.Font.LineHeight.M}
|
||||
color={getColors(theme)[colorMapping[type]]}
|
||||
style={{ marginRight: StyleConstants.Spacing.S }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
})
|
||||
type
|
||||
})
|
||||
} else {
|
||||
showMessage({
|
||||
duration: type === 'error' ? 8000 : duration === 'short' ? 3000 : 5000,
|
||||
duration: type === 'danger' ? 8000 : duration === 'short' ? 3000 : 5000,
|
||||
autoHide,
|
||||
message,
|
||||
description,
|
||||
onPress,
|
||||
...(theme &&
|
||||
type && {
|
||||
renderFlashMessageIcon: () => {
|
||||
return (
|
||||
<Icon
|
||||
name={iconMapping[type]}
|
||||
size={StyleConstants.Font.LineHeight.M}
|
||||
color={getColors(theme)[colorMapping[type]]}
|
||||
style={{ marginRight: StyleConstants.Spacing.S }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
})
|
||||
type
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const removeMessage = () => {
|
||||
// if (ref) {
|
||||
// ref.current?.hideMessage()
|
||||
// } else {
|
||||
hideMessage()
|
||||
// }
|
||||
}
|
||||
|
||||
const Message = React.forwardRef<FlashMessage>((_, ref) => {
|
||||
const { colors, theme } = useTheme()
|
||||
const insets = useSafeAreaInsets()
|
||||
|
||||
enum iconMapping {
|
||||
success = 'CheckCircle',
|
||||
danger = 'XCircle',
|
||||
warning = 'AlertCircle',
|
||||
none = '',
|
||||
default = '',
|
||||
info = '',
|
||||
auto = ''
|
||||
}
|
||||
enum colorMapping {
|
||||
success = 'blue',
|
||||
danger = 'red',
|
||||
warning = 'secondary',
|
||||
none = 'secondary',
|
||||
default = 'secondary',
|
||||
info = 'secondary',
|
||||
auto = 'secondary'
|
||||
}
|
||||
|
||||
return (
|
||||
<FlashMessage
|
||||
ref={ref}
|
||||
icon='auto'
|
||||
renderFlashMessageIcon={type => {
|
||||
return typeof type === 'string' && ['success', 'danger', 'warning'].includes(type) ? (
|
||||
<Icon
|
||||
name={iconMapping[type]}
|
||||
size={StyleConstants.Font.LineHeight.M}
|
||||
color={colors[colorMapping[type]]}
|
||||
style={{ marginRight: StyleConstants.Spacing.S }}
|
||||
/>
|
||||
) : null
|
||||
}}
|
||||
position='top'
|
||||
floating
|
||||
style={{
|
||||
|
@ -142,4 +114,4 @@ const Message = React.forwardRef<FlashMessage>((_, ref) => {
|
|||
)
|
||||
})
|
||||
|
||||
export { Message, displayMessage, removeMessage }
|
||||
export { Message, displayMessage }
|
||||
|
|
|
@ -19,7 +19,7 @@ export interface Props {
|
|||
const TimelineActioned: React.FC<Props> = ({ action, isNotification, ...rest }) => {
|
||||
const { status, reblogStatus } = useContext(StatusContext)
|
||||
const account = rest.account || (reblogStatus ? reblogStatus.account : status?.account)
|
||||
if (!status || !account) return null
|
||||
if (!account) return null
|
||||
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
const { colors } = useTheme()
|
||||
|
|
|
@ -7,6 +7,7 @@ import { Blurhash } from 'react-native-blurhash'
|
|||
import attachmentAspectRatio from './aspectRatio'
|
||||
import AttachmentAltText from './AltText'
|
||||
import { Platform } from 'expo-modules-core'
|
||||
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
||||
|
||||
export interface Props {
|
||||
total: number
|
||||
|
@ -23,6 +24,8 @@ const AttachmentVideo: React.FC<Props> = ({
|
|||
video,
|
||||
gifv = false
|
||||
}) => {
|
||||
const { reduceMotionEnabled } = useAccessibility()
|
||||
|
||||
const videoPlayer = useRef<Video>(null)
|
||||
const [videoLoading, setVideoLoading] = useState(false)
|
||||
const [videoLoaded, setVideoLoaded] = useState(false)
|
||||
|
@ -57,7 +60,7 @@ const AttachmentVideo: React.FC<Props> = ({
|
|||
resizeMode={videoResizeMode}
|
||||
{...(gifv
|
||||
? {
|
||||
shouldPlay: true,
|
||||
shouldPlay: reduceMotionEnabled ? false : true,
|
||||
isMuted: true,
|
||||
isLooping: true,
|
||||
source: { uri: video.url }
|
||||
|
@ -70,10 +73,10 @@ const AttachmentVideo: React.FC<Props> = ({
|
|||
onFullscreenUpdate={event => {
|
||||
if (event.fullscreenUpdate === VideoFullscreenUpdate.PLAYER_DID_DISMISS) {
|
||||
Platform.OS === 'android' && setVideoResizeMode(ResizeMode.COVER)
|
||||
if (!gifv) {
|
||||
videoPlayer.current?.pauseAsync()
|
||||
} else {
|
||||
if (gifv && !reduceMotionEnabled) {
|
||||
videoPlayer.current?.playAsync()
|
||||
} else {
|
||||
videoPlayer.current?.pauseAsync()
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
@ -103,7 +106,7 @@ const AttachmentVideo: React.FC<Props> = ({
|
|||
video.blurhash ? (
|
||||
<Blurhash blurhash={video.blurhash} style={{ width: '100%', height: '100%' }} />
|
||||
) : null
|
||||
) : !gifv ? (
|
||||
) : !gifv || (gifv && reduceMotionEnabled) ? (
|
||||
<Button
|
||||
round
|
||||
overlay
|
||||
|
|
|
@ -72,12 +72,12 @@ export const shouldFilter = ({
|
|||
const escapedPhrase = filter.phrase.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
|
||||
switch (filter.whole_word) {
|
||||
case true:
|
||||
if (new RegExp(`\\B${escapedPhrase}\\b`).test(rawContent)) {
|
||||
if (new RegExp(`\\b${escapedPhrase}\\b`, 'i').test(rawContent)) {
|
||||
shouldFilter = filter.phrase
|
||||
}
|
||||
break
|
||||
case false:
|
||||
if (new RegExp(escapedPhrase).test(rawContent)) {
|
||||
if (new RegExp(escapedPhrase, 'i').test(rawContent)) {
|
||||
shouldFilter = filter.phrase
|
||||
}
|
||||
break
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { ParseHTML } from '@components/Parse'
|
||||
import CustomText from '@components/Text'
|
||||
import detectLanguage from '@helpers/detectLanguage'
|
||||
import getLanguage from '@helpers/getLanguage'
|
||||
import { useTranslateQuery } from '@utils/queryHooks/translate'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
|
@ -7,38 +8,44 @@ import { useTheme } from '@utils/styles/ThemeManager'
|
|||
import * as Localization from 'expo-localization'
|
||||
import React, { useContext, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Pressable } from 'react-native'
|
||||
import { Platform, Pressable } from 'react-native'
|
||||
import { Circle } from 'react-native-animated-spinkit'
|
||||
import detectLanguage from 'react-native-language-detection'
|
||||
import StatusContext from './Context'
|
||||
|
||||
const TimelineTranslate = () => {
|
||||
const { status, highlighted } = useContext(StatusContext)
|
||||
const { status, highlighted, copiableContent } = useContext(StatusContext)
|
||||
if (!status || !highlighted) return null
|
||||
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
const { colors } = useTheme()
|
||||
|
||||
const text = status.spoiler_text ? [status.spoiler_text, status.content] : [status.content]
|
||||
const backupTextProcessing = (): string[] => {
|
||||
const text = status.spoiler_text ? [status.spoiler_text, status.content] : [status.content]
|
||||
|
||||
for (const i in text) {
|
||||
for (const emoji of status.emojis) {
|
||||
text[i] = text[i].replaceAll(`:${emoji.shortcode}:`, ' ')
|
||||
for (const i in text) {
|
||||
for (const emoji of status.emojis) {
|
||||
text[i] = text[i].replaceAll(`:${emoji.shortcode}:`, ' ')
|
||||
}
|
||||
text[i] = text[i]
|
||||
.replace(/(<([^>]+)>)/gi, ' ')
|
||||
.replace(/@.*? /gi, ' ')
|
||||
.replace(/#.*? /gi, ' ')
|
||||
.replace(/http(s):\/\/.*? /gi, ' ')
|
||||
}
|
||||
text[i] = text[i]
|
||||
.replace(/(<([^>]+)>)/gi, ' ')
|
||||
.replace(/@.*? /gi, ' ')
|
||||
.replace(/#.*? /gi, ' ')
|
||||
.replace(/http(s):\/\/.*? /gi, ' ')
|
||||
return text
|
||||
}
|
||||
const text = copiableContent?.current.content
|
||||
? [copiableContent?.current.content]
|
||||
: backupTextProcessing()
|
||||
|
||||
const [detectedLanguage, setDetectedLanguage] = useState<string>('')
|
||||
const [detectedLanguage, setDetectedLanguage] = useState<{
|
||||
language: string
|
||||
confidence: number
|
||||
}>({ language: status.language || '', confidence: 0 })
|
||||
useEffect(() => {
|
||||
const detect = async () => {
|
||||
const result = await detectLanguage(text.join(`\n\n`)).catch(() => {
|
||||
// No need to log language detection failure
|
||||
})
|
||||
result?.detected && setDetectedLanguage(result.detected.slice(0, 2))
|
||||
const result = await detectLanguage(text.join('\n\n'))
|
||||
result && setDetectedLanguage(result)
|
||||
}
|
||||
detect()
|
||||
}, [])
|
||||
|
@ -50,20 +57,36 @@ const TimelineTranslate = () => {
|
|||
|
||||
const [enabled, setEnabled] = useState(false)
|
||||
const { refetch, data, isLoading, isSuccess, isError } = useTranslateQuery({
|
||||
source: detectedLanguage,
|
||||
source: detectedLanguage.language,
|
||||
target: targetLanguage,
|
||||
text,
|
||||
options: { enabled }
|
||||
})
|
||||
|
||||
const devView = () => {
|
||||
return __DEV__ ? (
|
||||
<CustomText fontStyle='S' style={{ color: colors.secondary }}>{` Source: ${
|
||||
detectedLanguage?.language
|
||||
}; Confidence: ${
|
||||
detectedLanguage?.confidence.toString().slice(0, 5) || 'null'
|
||||
}; Target: ${targetLanguage}`}</CustomText>
|
||||
) : null
|
||||
}
|
||||
|
||||
if (!detectedLanguage) {
|
||||
return null
|
||||
return devView()
|
||||
}
|
||||
if (Localization.locale.slice(0, 2).includes(detectedLanguage)) {
|
||||
return null
|
||||
if (
|
||||
Platform.OS === 'ios' &&
|
||||
Localization.locale.slice(0, 2).includes(detectedLanguage.language.slice(0, 2))
|
||||
) {
|
||||
return devView()
|
||||
}
|
||||
if (settingsLanguage?.slice(0, 2).includes(detectedLanguage)) {
|
||||
return null
|
||||
if (
|
||||
Platform.OS === 'android' &&
|
||||
settingsLanguage?.slice(0, 2).includes(detectedLanguage.language.slice(0, 2))
|
||||
) {
|
||||
return devView()
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -102,9 +125,6 @@ const TimelineTranslate = () => {
|
|||
})
|
||||
: t('shared.translate.default')}
|
||||
</CustomText>
|
||||
<CustomText>
|
||||
{__DEV__ ? ` Source: ${detectedLanguage}; Target: ${targetLanguage}` : undefined}
|
||||
</CustomText>
|
||||
{isLoading ? (
|
||||
<Circle
|
||||
size={StyleConstants.Font.Size.M}
|
||||
|
@ -113,6 +133,7 @@ const TimelineTranslate = () => {
|
|||
/>
|
||||
) : null}
|
||||
</Pressable>
|
||||
{devView()}
|
||||
{data && data.error === undefined
|
||||
? data.text.map((d, i) => <ParseHTML key={i} content={d} size={'M'} numberOfLines={999} />)
|
||||
: null}
|
||||
|
|
|
@ -14,7 +14,6 @@ import {
|
|||
useTimelineMutation
|
||||
} from '@utils/queryHooks/timeline'
|
||||
import { getInstanceAccount } from '@utils/slices/instancesSlice'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Platform } from 'react-native'
|
||||
|
@ -38,7 +37,6 @@ const menuAccount = ({
|
|||
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<TabSharedStackParamList, any, undefined>>()
|
||||
const { theme } = useTheme()
|
||||
const { t } = useTranslation('componentContextMenu')
|
||||
|
||||
const menus: ContextMenu[][] = [[]]
|
||||
|
@ -60,7 +58,6 @@ const menuAccount = ({
|
|||
queryClient.refetchQueries(['Relationship', { id: account.id }])
|
||||
const theParams = params as MutationVarsTimelineUpdateAccountProperty
|
||||
displayMessage({
|
||||
theme,
|
||||
type: 'success',
|
||||
message: t('common:message.success.message', {
|
||||
function: t(`account.${theParams.payload.property}.action`, {
|
||||
|
@ -74,8 +71,7 @@ const menuAccount = ({
|
|||
onError: (err: any, params) => {
|
||||
const theParams = params as MutationVarsTimelineUpdateAccountProperty
|
||||
displayMessage({
|
||||
theme,
|
||||
type: 'error',
|
||||
type: 'danger',
|
||||
message: t('common:message.error.message', {
|
||||
function: t(`account.${theParams.payload.property}.action`, {
|
||||
...(theParams.payload.property !== 'reports' && {
|
||||
|
@ -109,8 +105,7 @@ const menuAccount = ({
|
|||
},
|
||||
onError: (err: any, { payload: { action } }) => {
|
||||
displayMessage({
|
||||
theme,
|
||||
type: 'error',
|
||||
type: 'danger',
|
||||
message: t('common:message.error.message', {
|
||||
function: t(`${action}.function`)
|
||||
}),
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { displayMessage } from '@components/Message'
|
||||
import { QueryKeyTimeline, useTimelineMutation } from '@utils/queryHooks/timeline'
|
||||
import { getInstanceUrl } from '@utils/slices/instancesSlice'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Alert } from 'react-native'
|
||||
import { useQueryClient } from 'react-query'
|
||||
|
@ -18,14 +17,12 @@ const menuInstance = ({
|
|||
}): ContextMenu[][] => {
|
||||
if (!status || !queryKey) return []
|
||||
|
||||
const { theme } = useTheme()
|
||||
const { t } = useTranslation('componentContextMenu')
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const mutation = useTimelineMutation({
|
||||
onSettled: () => {
|
||||
displayMessage({
|
||||
theme,
|
||||
type: 'success',
|
||||
message: t('common:message.success.message', {
|
||||
function: t(`instance.block.action`, { instance })
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { displayMessage } from '@components/Message'
|
||||
import Clipboard from '@react-native-clipboard/clipboard'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Platform, Share } from 'react-native'
|
||||
|
||||
|
@ -22,7 +21,6 @@ const menuShare = (
|
|||
): ContextMenu[][] => {
|
||||
if (params.type === 'status' && params.visibility === 'direct') return []
|
||||
|
||||
const { theme } = useTheme()
|
||||
const { t } = useTranslation('componentContextMenu')
|
||||
|
||||
const menus: ContextMenu[][] = [[]]
|
||||
|
@ -56,11 +54,7 @@ const menuShare = (
|
|||
item: {
|
||||
onSelect: () => {
|
||||
Clipboard.setString(params.copiableContent?.current.content || '')
|
||||
displayMessage({
|
||||
theme,
|
||||
type: 'success',
|
||||
message: t(`copy.succeed`)
|
||||
})
|
||||
displayMessage({ type: 'success', message: t(`copy.succeed`) })
|
||||
},
|
||||
disabled: false,
|
||||
destructive: false,
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import detect from 'react-native-language-detection'
|
||||
|
||||
const detectLanguage = async (
|
||||
text: string
|
||||
): Promise<{ language: string; confidence: number } | null> => {
|
||||
const possibleLanguages = await detect(text).catch(() => {})
|
||||
return possibleLanguages ? possibleLanguages.filter(lang => lang.confidence > 0.5)?.[0] : null
|
||||
}
|
||||
|
||||
export default detectLanguage
|
|
@ -19,6 +19,11 @@
|
|||
"version": 3.5,
|
||||
"reference": "https://github.com/mastodon/mastodon/releases/tag/v3.5.0"
|
||||
},
|
||||
{
|
||||
"feature": "notification_type_admin_signup",
|
||||
"version": 3.5,
|
||||
"reference": "https://github.com/mastodon/mastodon/releases/tag/v3.5.0"
|
||||
},
|
||||
{
|
||||
"feature": "notification_types_positive_filter",
|
||||
"version": 3.5,
|
||||
|
@ -33,5 +38,10 @@
|
|||
"feature": "follow_tags",
|
||||
"version": 4.0,
|
||||
"reference": "https://github.com/mastodon/mastodon/releases/tag/v4.0.0"
|
||||
},
|
||||
{
|
||||
"feature": "notification_type_admin_report",
|
||||
"version": 4.0,
|
||||
"reference": "https://github.com/mastodon/mastodon/releases/tag/v3.5.0"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,2 @@
|
|||
export const PERMISSION_MANAGE_REPORTS = 0x0000000000000010
|
||||
export const PERMISSION_MANAGE_USERS = 0x0000000000000400
|
|
@ -1,5 +1,5 @@
|
|||
import { QueryClient } from 'react-query'
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 1000 * 60 * 5 } } })
|
||||
|
||||
export default queryClient
|
||||
|
|
|
@ -178,6 +178,10 @@
|
|||
"direct": "Habilita les notificacions push",
|
||||
"settings": "Activa'ls a la configuració"
|
||||
},
|
||||
"missingServerKey": {
|
||||
"message": "",
|
||||
"description": ""
|
||||
},
|
||||
"global": {
|
||||
"heading": "Activa per {{acct}}",
|
||||
"description": "Els missatges s'envien a través del servidor del tooot"
|
||||
|
|
|
@ -178,6 +178,10 @@
|
|||
"direct": "",
|
||||
"settings": ""
|
||||
},
|
||||
"missingServerKey": {
|
||||
"message": "",
|
||||
"description": ""
|
||||
},
|
||||
"global": {
|
||||
"heading": "",
|
||||
"description": ""
|
||||
|
|
|
@ -178,6 +178,10 @@
|
|||
"direct": "Push-Benachrichtigungen aktivieren",
|
||||
"settings": "In den Einstellungen aktivieren"
|
||||
},
|
||||
"missingServerKey": {
|
||||
"message": "",
|
||||
"description": ""
|
||||
},
|
||||
"global": {
|
||||
"heading": "Aktivieren für {{acct}}",
|
||||
"description": "Nachrichten werden über den Tooot-Server geleitet"
|
||||
|
|
|
@ -178,6 +178,10 @@
|
|||
"direct": "Enable push notification",
|
||||
"settings": "Enable in settings"
|
||||
},
|
||||
"missingServerKey": {
|
||||
"message": "Server misconfigured for push",
|
||||
"description": "Please contact your server admin to configure push support"
|
||||
},
|
||||
"global": {
|
||||
"heading": "Enable for {{acct}}",
|
||||
"description": "Messages are routed through tooot's server"
|
||||
|
@ -210,6 +214,12 @@
|
|||
"status": {
|
||||
"heading": "Toot from subscribed users"
|
||||
},
|
||||
"admin.sign_up": {
|
||||
"heading": "Admin: sign up"
|
||||
},
|
||||
"admin.report": {
|
||||
"heading": "Admin: reports"
|
||||
},
|
||||
"howitworks": "Learn how routing works"
|
||||
},
|
||||
"root": {
|
||||
|
@ -221,10 +231,8 @@
|
|||
}
|
||||
},
|
||||
"push": {
|
||||
"content": {
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled"
|
||||
}
|
||||
"content_true": "Enabled",
|
||||
"content_false": "Disabled"
|
||||
},
|
||||
"update": {
|
||||
"title": "Update to latest version"
|
||||
|
@ -286,9 +294,6 @@
|
|||
"support": {
|
||||
"heading": "Support tooot"
|
||||
},
|
||||
"review": {
|
||||
"heading": "Review tooot"
|
||||
},
|
||||
"contact": {
|
||||
"heading": "Contact tooot"
|
||||
},
|
||||
|
|
|
@ -178,6 +178,10 @@
|
|||
"direct": "Habilitar notificaciones push",
|
||||
"settings": "Activar en Ajustes"
|
||||
},
|
||||
"missingServerKey": {
|
||||
"message": "",
|
||||
"description": ""
|
||||
},
|
||||
"global": {
|
||||
"heading": "Habilitar para {{acct}}",
|
||||
"description": "Los mensajes se envían a través del servidor de tooot"
|
||||
|
|
|
@ -178,6 +178,10 @@
|
|||
"direct": "Activer les notifications push",
|
||||
"settings": "Activer dans les paramètres"
|
||||
},
|
||||
"missingServerKey": {
|
||||
"message": "",
|
||||
"description": ""
|
||||
},
|
||||
"global": {
|
||||
"heading": "Activer pour {{acct}}",
|
||||
"description": "Les messages sont acheminés via le serveur de tooot"
|
||||
|
|
|
@ -178,6 +178,10 @@
|
|||
"direct": "Abilita notifiche push",
|
||||
"settings": "Abilita nelle impostazioni"
|
||||
},
|
||||
"missingServerKey": {
|
||||
"message": "",
|
||||
"description": ""
|
||||
},
|
||||
"global": {
|
||||
"heading": "Abilita per {{acct}}",
|
||||
"description": "I messaggi dovranno attraversare i server di tooot"
|
||||
|
|
|
@ -178,6 +178,10 @@
|
|||
"direct": "プッシュ通知を有効にする",
|
||||
"settings": "設定で有効にする"
|
||||
},
|
||||
"missingServerKey": {
|
||||
"message": "",
|
||||
"description": ""
|
||||
},
|
||||
"global": {
|
||||
"heading": "{{acct}} の通知を有効にする",
|
||||
"description": "メッセージは tooot のサーバー経由で到達します"
|
||||
|
|
|
@ -178,6 +178,10 @@
|
|||
"direct": "푸시 알림 활성화",
|
||||
"settings": "설정에서 활성화"
|
||||
},
|
||||
"missingServerKey": {
|
||||
"message": "",
|
||||
"description": ""
|
||||
},
|
||||
"global": {
|
||||
"heading": "{{acct}} 활성화",
|
||||
"description": "메시지는 tooot의 서버를 거쳐 전달돼요"
|
||||
|
|
|
@ -178,6 +178,10 @@
|
|||
"direct": "Push meldingen inschakelen",
|
||||
"settings": "Inschakelen in instellingen"
|
||||
},
|
||||
"missingServerKey": {
|
||||
"message": "",
|
||||
"description": ""
|
||||
},
|
||||
"global": {
|
||||
"heading": "Inschakelen voor {{acct}}",
|
||||
"description": "Berichten worden doorgestuurd via tooot's server"
|
||||
|
|
|
@ -178,6 +178,10 @@
|
|||
"direct": "",
|
||||
"settings": ""
|
||||
},
|
||||
"missingServerKey": {
|
||||
"message": "",
|
||||
"description": ""
|
||||
},
|
||||
"global": {
|
||||
"heading": "",
|
||||
"description": ""
|
||||
|
|
|
@ -178,6 +178,10 @@
|
|||
"direct": "Habilitar notificações via push",
|
||||
"settings": "Ativar em configurações"
|
||||
},
|
||||
"missingServerKey": {
|
||||
"message": "",
|
||||
"description": ""
|
||||
},
|
||||
"global": {
|
||||
"heading": "Habilitar para {{acct}}",
|
||||
"description": "Mensagens são encaminhadas pelo servidor do tooot"
|
||||
|
|
|
@ -178,6 +178,10 @@
|
|||
"direct": "Aktivera pushnotiser",
|
||||
"settings": "Aktivera i inställningar"
|
||||
},
|
||||
"missingServerKey": {
|
||||
"message": "",
|
||||
"description": ""
|
||||
},
|
||||
"global": {
|
||||
"heading": "Aktivera för {{acct}}",
|
||||
"description": "Meddelanden dirigeras via tooots server"
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
"discard": "Bỏ qua",
|
||||
"continue": "Tiếp tục",
|
||||
"delete": "Xóa",
|
||||
"done": ""
|
||||
"done": "Xong"
|
||||
},
|
||||
"customEmoji": {
|
||||
"accessibilityLabel": "Tùy chỉnh emoji {{emoji}}"
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
"action_false": "Theo dõi người này",
|
||||
"action_true": "Ngưng theo dõi người này"
|
||||
},
|
||||
"inLists": "",
|
||||
"inLists": "Quản lý người trong danh sách",
|
||||
"mute": {
|
||||
"action_false": "Ẩn người này",
|
||||
"action_true": "Bỏ ẩn người dùng"
|
||||
|
@ -20,8 +20,8 @@
|
|||
}
|
||||
},
|
||||
"at": {
|
||||
"direct": "",
|
||||
"public": ""
|
||||
"direct": "Nhắn riêng",
|
||||
"public": "Công khai"
|
||||
},
|
||||
"copy": {
|
||||
"action": "Sao chép tút",
|
||||
|
|
|
@ -178,6 +178,10 @@
|
|||
"direct": "Bật thông báo đẩy",
|
||||
"settings": "Bật trong cài đặt"
|
||||
},
|
||||
"missingServerKey": {
|
||||
"message": "",
|
||||
"description": ""
|
||||
},
|
||||
"global": {
|
||||
"heading": "Bật cho {{acct}}",
|
||||
"description": "Thông báo được truyền qua máy chủ tooot"
|
||||
|
@ -321,9 +325,9 @@
|
|||
"suspended": "Người này đã bị vô hiệu hóa"
|
||||
},
|
||||
"accountInLists": {
|
||||
"name": "",
|
||||
"inLists": "",
|
||||
"notInLists": ""
|
||||
"name": "Danh sách của @{{username}}",
|
||||
"inLists": "Trong danh sách",
|
||||
"notInLists": "Danh sách khác"
|
||||
},
|
||||
"attachments": {
|
||||
"name": "<0 /><1>'s media</1>"
|
||||
|
@ -352,7 +356,7 @@
|
|||
}
|
||||
},
|
||||
"trending": {
|
||||
"tags": ""
|
||||
"tags": "Hashtag xu hướng"
|
||||
}
|
||||
},
|
||||
"sections": {
|
||||
|
|
|
@ -178,6 +178,10 @@
|
|||
"direct": "启用推送通知",
|
||||
"settings": "在系统设置中启用"
|
||||
},
|
||||
"missingServerKey": {
|
||||
"message": "服务器推送配置不正确",
|
||||
"description": "请联系您的服务器管理员设置推送支持"
|
||||
},
|
||||
"global": {
|
||||
"heading": "启用 {{acct}}",
|
||||
"description": "通知消息将经由tooot服务器转发"
|
||||
|
|
|
@ -178,6 +178,10 @@
|
|||
"direct": "啟用推播通知",
|
||||
"settings": "在設定中啟用"
|
||||
},
|
||||
"missingServerKey": {
|
||||
"message": "",
|
||||
"description": ""
|
||||
},
|
||||
"global": {
|
||||
"heading": "啟用 {{acct}}",
|
||||
"description": "通知訊息將經由 tooot 伺服器轉發"
|
||||
|
|
|
@ -1,123 +0,0 @@
|
|||
import { MenuContainer, MenuHeader, MenuRow } from '@components/Menu'
|
||||
import { displayMessage } from '@components/Message'
|
||||
import {
|
||||
MutationVarsTimelineUpdateAccountProperty,
|
||||
QueryKeyTimeline,
|
||||
useTimelineMutation
|
||||
} from '@utils/queryHooks/timeline'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useQueryClient } from 'react-query'
|
||||
|
||||
export interface Props {
|
||||
queryKey?: QueryKeyTimeline
|
||||
rootQueryKey?: QueryKeyTimeline
|
||||
account: Mastodon.Account
|
||||
dismiss: () => void
|
||||
}
|
||||
|
||||
const ActionsAccount: React.FC<Props> = ({
|
||||
queryKey,
|
||||
rootQueryKey,
|
||||
account,
|
||||
dismiss
|
||||
}) => {
|
||||
const { theme } = useTheme()
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const mutation = useTimelineMutation({
|
||||
onSuccess: (_, params) => {
|
||||
const theParams = params as MutationVarsTimelineUpdateAccountProperty
|
||||
displayMessage({
|
||||
theme,
|
||||
type: 'success',
|
||||
message: t('common:message.success.message', {
|
||||
function: t(
|
||||
`shared.header.actions.account.${theParams.payload.property}.function`,
|
||||
{
|
||||
acct: account.acct
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
},
|
||||
onError: (err: any, params) => {
|
||||
const theParams = params as MutationVarsTimelineUpdateAccountProperty
|
||||
displayMessage({
|
||||
theme,
|
||||
type: 'error',
|
||||
message: t('common:message.error.message', {
|
||||
function: t(
|
||||
`shared.header.actions.account.${theParams.payload.property}.function`
|
||||
)
|
||||
}),
|
||||
...(err.status &&
|
||||
typeof err.status === 'number' &&
|
||||
err.data &&
|
||||
err.data.error &&
|
||||
typeof err.data.error === 'string' && {
|
||||
description: err.data.error
|
||||
})
|
||||
})
|
||||
},
|
||||
onSettled: () => {
|
||||
queryKey && queryClient.invalidateQueries(queryKey)
|
||||
rootQueryKey && queryClient.invalidateQueries(rootQueryKey)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<MenuContainer>
|
||||
<MenuHeader heading={t('shared.header.actions.account.heading')} />
|
||||
<MenuRow
|
||||
onPress={() => {
|
||||
dismiss()
|
||||
mutation.mutate({
|
||||
type: 'updateAccountProperty',
|
||||
queryKey,
|
||||
id: account.id,
|
||||
payload: { property: 'mute' }
|
||||
})
|
||||
}}
|
||||
iconFront='EyeOff'
|
||||
title={t('shared.header.actions.account.mute.button', {
|
||||
acct: account.acct
|
||||
})}
|
||||
/>
|
||||
<MenuRow
|
||||
onPress={() => {
|
||||
dismiss()
|
||||
mutation.mutate({
|
||||
type: 'updateAccountProperty',
|
||||
queryKey,
|
||||
id: account.id,
|
||||
payload: { property: 'block' }
|
||||
})
|
||||
}}
|
||||
iconFront='XCircle'
|
||||
title={t('shared.header.actions.account.block.button', {
|
||||
acct: account.acct
|
||||
})}
|
||||
/>
|
||||
<MenuRow
|
||||
onPress={() => {
|
||||
dismiss()
|
||||
mutation.mutate({
|
||||
type: 'updateAccountProperty',
|
||||
queryKey,
|
||||
id: account.id,
|
||||
payload: { property: 'reports' }
|
||||
})
|
||||
}}
|
||||
iconFront='Flag'
|
||||
title={t('shared.header.actions.account.reports.button', {
|
||||
acct: account.acct
|
||||
})}
|
||||
/>
|
||||
</MenuContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default ActionsAccount
|
|
@ -1,74 +0,0 @@
|
|||
import MenuContainer from '@components/Menu/Container'
|
||||
import MenuHeader from '@components/Menu/Header'
|
||||
import MenuRow from '@components/Menu/Row'
|
||||
import { displayMessage } from '@components/Message'
|
||||
import { QueryKeyTimeline, useTimelineMutation } from '@utils/queryHooks/timeline'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Alert } from 'react-native'
|
||||
import { useQueryClient } from 'react-query'
|
||||
|
||||
export interface Props {
|
||||
queryKey: QueryKeyTimeline
|
||||
rootQueryKey?: QueryKeyTimeline
|
||||
domain: string
|
||||
dismiss: () => void
|
||||
}
|
||||
|
||||
const ActionsDomain: React.FC<Props> = ({ queryKey, rootQueryKey, domain, dismiss }) => {
|
||||
const { theme } = useTheme()
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
const queryClient = useQueryClient()
|
||||
const mutation = useTimelineMutation({
|
||||
onSettled: () => {
|
||||
displayMessage({
|
||||
theme,
|
||||
type: 'success',
|
||||
message: t('common:message.success.message', {
|
||||
function: t(`shared.header.actions.domain.block.function`)
|
||||
})
|
||||
})
|
||||
queryClient.invalidateQueries(queryKey)
|
||||
rootQueryKey && queryClient.invalidateQueries(rootQueryKey)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<MenuContainer>
|
||||
<MenuHeader heading={t(`shared.header.actions.domain.heading`)} />
|
||||
<MenuRow
|
||||
onPress={() =>
|
||||
Alert.alert(
|
||||
t('shared.header.actions.domain.alert.title', { domain }),
|
||||
t('shared.header.actions.domain.alert.message'),
|
||||
[
|
||||
{
|
||||
text: t('shared.header.actions.domain.alert.buttons.confirm'),
|
||||
style: 'destructive',
|
||||
onPress: () => {
|
||||
dismiss()
|
||||
mutation.mutate({
|
||||
type: 'domainBlock',
|
||||
queryKey,
|
||||
domain: domain
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
text: t('shared.header.actions.domain.alert.buttons.cancel'),
|
||||
style: 'default'
|
||||
}
|
||||
]
|
||||
)
|
||||
}
|
||||
iconFront='CloudOff'
|
||||
title={t(`shared.header.actions.domain.block.button`, {
|
||||
domain
|
||||
})}
|
||||
/>
|
||||
</MenuContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default ActionsDomain
|
|
@ -1,43 +0,0 @@
|
|||
import MenuContainer from '@components/Menu/Container'
|
||||
import MenuHeader from '@components/Menu/Header'
|
||||
import MenuRow from '@components/Menu/Row'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Platform, Share } from 'react-native'
|
||||
|
||||
export interface Props {
|
||||
type: 'status' | 'account'
|
||||
url: string
|
||||
dismiss: () => void
|
||||
}
|
||||
|
||||
const ActionsShare: React.FC<Props> = ({ type, url, dismiss }) => {
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
|
||||
return (
|
||||
<MenuContainer>
|
||||
<MenuHeader heading={t(`shared.header.actions.share.${type}.heading`)} />
|
||||
<MenuRow
|
||||
iconFront='Share2'
|
||||
title={t(`shared.header.actions.share.${type}.button`)}
|
||||
onPress={async () => {
|
||||
switch (Platform.OS) {
|
||||
case 'ios':
|
||||
await Share.share({
|
||||
url
|
||||
})
|
||||
break
|
||||
case 'android':
|
||||
await Share.share({
|
||||
message: url
|
||||
})
|
||||
break
|
||||
}
|
||||
dismiss()
|
||||
}}
|
||||
/>
|
||||
</MenuContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default ActionsShare
|
|
@ -1,231 +0,0 @@
|
|||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Alert } from 'react-native'
|
||||
import { useQueryClient } from 'react-query'
|
||||
import { MenuContainer, MenuHeader, MenuRow } from '@components/Menu'
|
||||
import {
|
||||
MutationVarsTimelineUpdateStatusProperty,
|
||||
QueryKeyTimeline,
|
||||
useTimelineMutation
|
||||
} from '@utils/queryHooks/timeline'
|
||||
import { displayMessage } from '@components/Message'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import apiInstance from '@api/instance'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { RootStackParamList } from '@utils/navigation/navigators'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { checkInstanceFeature } from '@utils/slices/instancesSlice'
|
||||
|
||||
export interface Props {
|
||||
navigation: NativeStackNavigationProp<RootStackParamList, 'Screen-Actions'>
|
||||
queryKey: QueryKeyTimeline
|
||||
rootQueryKey?: QueryKeyTimeline
|
||||
status: Mastodon.Status
|
||||
dismiss: () => void
|
||||
}
|
||||
|
||||
const ActionsStatus: React.FC<Props> = ({
|
||||
navigation,
|
||||
queryKey,
|
||||
rootQueryKey,
|
||||
status,
|
||||
dismiss
|
||||
}) => {
|
||||
const { theme } = useTheme()
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const mutation = useTimelineMutation({
|
||||
onMutate: true,
|
||||
onError: (err: any, params, oldData) => {
|
||||
const theFunction = (params as MutationVarsTimelineUpdateStatusProperty).payload
|
||||
? (params as MutationVarsTimelineUpdateStatusProperty).payload.property
|
||||
: 'delete'
|
||||
displayMessage({
|
||||
theme,
|
||||
type: 'error',
|
||||
message: t('common:message.error.message', {
|
||||
function: t(`shared.header.actions.status.${theFunction}.function`)
|
||||
}),
|
||||
...(err?.status &&
|
||||
typeof err.status === 'number' &&
|
||||
err.data &&
|
||||
err.data.error &&
|
||||
typeof err.data.error === 'string' && {
|
||||
description: err.data.error
|
||||
})
|
||||
})
|
||||
queryClient.setQueryData(queryKey, oldData)
|
||||
}
|
||||
})
|
||||
|
||||
const canEditPost = useSelector(checkInstanceFeature('edit_post'))
|
||||
|
||||
return (
|
||||
<MenuContainer>
|
||||
<MenuHeader heading={t('shared.header.actions.status.heading')} />
|
||||
{canEditPost ? (
|
||||
<MenuRow
|
||||
onPress={async () => {
|
||||
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)
|
||||
}
|
||||
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 => {
|
||||
dismiss()
|
||||
navigation.navigate('Screen-Compose', {
|
||||
type: 'edit',
|
||||
incomingStatus: {
|
||||
...status,
|
||||
text: res.body.text,
|
||||
spoiler_text: res.body.spoiler_text
|
||||
},
|
||||
...(replyToStatus && { replyToStatus }),
|
||||
queryKey,
|
||||
rootQueryKey
|
||||
})
|
||||
})
|
||||
}}
|
||||
iconFront='Edit3'
|
||||
title={t('shared.header.actions.status.edit.button')}
|
||||
/>
|
||||
) : null}
|
||||
<MenuRow
|
||||
onPress={() => {
|
||||
Alert.alert(
|
||||
t('shared.header.actions.status.delete.alert.title'),
|
||||
t('shared.header.actions.status.delete.alert.message'),
|
||||
[
|
||||
{
|
||||
text: t('shared.header.actions.status.delete.alert.buttons.confirm'),
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
dismiss()
|
||||
mutation.mutate({
|
||||
type: 'deleteItem',
|
||||
source: 'statuses',
|
||||
queryKey,
|
||||
rootQueryKey,
|
||||
id: status.id
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
text: t('shared.header.actions.status.delete.alert.buttons.cancel'),
|
||||
style: 'default'
|
||||
}
|
||||
]
|
||||
)
|
||||
}}
|
||||
iconFront='Trash'
|
||||
title={t('shared.header.actions.status.delete.button')}
|
||||
/>
|
||||
<MenuRow
|
||||
onPress={() => {
|
||||
Alert.alert(
|
||||
t('shared.header.actions.status.deleteEdit.alert.title'),
|
||||
t('shared.header.actions.status.deleteEdit.alert.message'),
|
||||
[
|
||||
{
|
||||
text: t('shared.header.actions.status.deleteEdit.alert.buttons.confirm'),
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
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 => {
|
||||
dismiss()
|
||||
navigation.navigate('Screen-Compose', {
|
||||
type: 'deleteEdit',
|
||||
incomingStatus: res.body as Mastodon.Status,
|
||||
...(replyToStatus && { replyToStatus }),
|
||||
queryKey
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
text: t('shared.header.actions.status.deleteEdit.alert.buttons.cancel'),
|
||||
style: 'default'
|
||||
}
|
||||
]
|
||||
)
|
||||
}}
|
||||
iconFront='Edit'
|
||||
title={t('shared.header.actions.status.deleteEdit.button')}
|
||||
/>
|
||||
<MenuRow
|
||||
onPress={() => {
|
||||
dismiss()
|
||||
mutation.mutate({
|
||||
type: 'updateStatusProperty',
|
||||
queryKey,
|
||||
rootQueryKey,
|
||||
id: status.id,
|
||||
payload: {
|
||||
property: 'muted',
|
||||
currentValue: status.muted,
|
||||
propertyCount: undefined,
|
||||
countValue: undefined
|
||||
}
|
||||
})
|
||||
}}
|
||||
iconFront='VolumeX'
|
||||
title={
|
||||
status.muted
|
||||
? t('shared.header.actions.status.mute.button.negative')
|
||||
: t('shared.header.actions.status.mute.button.positive')
|
||||
}
|
||||
/>
|
||||
{/* Also note that reblogs cannot be pinned. */}
|
||||
{(status.visibility === 'public' || status.visibility === 'unlisted') && (
|
||||
<MenuRow
|
||||
onPress={() => {
|
||||
dismiss()
|
||||
mutation.mutate({
|
||||
type: 'updateStatusProperty',
|
||||
queryKey,
|
||||
rootQueryKey,
|
||||
id: status.id,
|
||||
payload: {
|
||||
property: 'pinned',
|
||||
currentValue: status.pinned,
|
||||
propertyCount: undefined,
|
||||
countValue: undefined
|
||||
}
|
||||
})
|
||||
}}
|
||||
iconFront='Anchor'
|
||||
title={
|
||||
status.pinned
|
||||
? t('shared.header.actions.status.pin.button.negative')
|
||||
: t('shared.header.actions.status.pin.button.positive')
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</MenuContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default ActionsStatus
|
|
@ -1,3 +1,4 @@
|
|||
import { handleError } from '@api/helpers'
|
||||
import { ComponentEmojis } from '@components/Emojis'
|
||||
import { EmojisState } from '@components/Emojis/helpers/EmojisContext'
|
||||
import { HeaderLeft, HeaderRight } from '@components/Header'
|
||||
|
@ -5,8 +6,7 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack'
|
|||
import haptics from '@root/components/haptics'
|
||||
import { useAppDispatch } from '@root/store'
|
||||
import ComposeRoot from '@screens/Compose/Root'
|
||||
import formatText from '@screens/Compose/utils/formatText'
|
||||
import * as Sentry from '@sentry/react-native'
|
||||
import { formatText } from '@screens/Compose/utils/processText'
|
||||
import { RootStackScreenProps } from '@utils/navigation/navigators'
|
||||
import { useTimelineMutation } from '@utils/queryHooks/timeline'
|
||||
import { updateStoreReview } from '@utils/slices/contextsSlice'
|
||||
|
@ -257,13 +257,17 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
|
|||
() => (
|
||||
<HeaderRight
|
||||
type='text'
|
||||
content={
|
||||
params?.type
|
||||
? params.type === 'conversation' && params.visibility === 'direct'
|
||||
? t(`heading.right.button.${params.type}`)
|
||||
: t('heading.right.button.default')
|
||||
: t('heading.right.button.default')
|
||||
}
|
||||
content={t(
|
||||
`heading.right.button.${
|
||||
(params?.type &&
|
||||
(params.type === 'conversation'
|
||||
? params.visibility === 'direct'
|
||||
? params.type
|
||||
: 'default'
|
||||
: params.type)) ||
|
||||
'default'
|
||||
}`
|
||||
)}
|
||||
onPress={() => {
|
||||
composeDispatch({ type: 'posting', payload: true })
|
||||
|
||||
|
@ -319,9 +323,8 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
|
|||
]
|
||||
)
|
||||
} else {
|
||||
Sentry.setContext('Error object', { error })
|
||||
Sentry.captureMessage('Posting error')
|
||||
haptics('Error')
|
||||
handleError({ message: 'Posting error', captureResponse: true })
|
||||
composeDispatch({ type: 'posting', payload: false })
|
||||
Alert.alert(t('heading.right.alert.default.title'), undefined, [
|
||||
{
|
||||
|
|
|
@ -15,7 +15,7 @@ import { PanGestureHandler } from 'react-native-gesture-handler'
|
|||
import { SwipeListView } from 'react-native-swipe-list-view'
|
||||
import { useSelector } from 'react-redux'
|
||||
import ComposeContext from '../utils/createContext'
|
||||
import formatText from '../utils/formatText'
|
||||
import { formatText } from '../utils/processText'
|
||||
import { ComposeStateDraft, ExtendedAttachment } from '../utils/types'
|
||||
|
||||
export interface Props {
|
||||
|
@ -159,22 +159,23 @@ const ComposeDraftsListRoot: React.FC<Props> = ({ timestamp }) => {
|
|||
data={instanceDrafts}
|
||||
renderItem={renderItem}
|
||||
renderHiddenItem={({ item }) => (
|
||||
<View
|
||||
<Pressable
|
||||
style={{
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
backgroundColor: colors.red
|
||||
}}
|
||||
onPress={() => dispatch(removeInstanceDraft(item.timestamp))}
|
||||
children={
|
||||
<Pressable
|
||||
<View
|
||||
style={{
|
||||
flexBasis:
|
||||
StyleConstants.Font.Size.L + StyleConstants.Spacing.Global.PagePadding * 4,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(0, 255, 0, 0.2)'
|
||||
}}
|
||||
onPress={() => dispatch(removeInstanceDraft(item.timestamp))}
|
||||
children={
|
||||
<Icon
|
||||
name='Trash'
|
||||
|
@ -188,12 +189,6 @@ const ComposeDraftsListRoot: React.FC<Props> = ({ timestamp }) => {
|
|||
)}
|
||||
disableRightSwipe={true}
|
||||
rightOpenValue={-actionWidth}
|
||||
// previewRowKey={
|
||||
// instanceDrafts?.length
|
||||
// ? instanceDrafts[0].timestamp.toString()
|
||||
// : undefined
|
||||
// }
|
||||
// previewDuration={350}
|
||||
previewOpenValue={-actionWidth / 2}
|
||||
ItemSeparatorComponent={ComponentSeparator}
|
||||
keyExtractor={item => item.timestamp.toString()}
|
||||
|
|
|
@ -13,13 +13,13 @@ const ComposeRootFooter: React.FC<Props> = ({ accessibleRefAttachments }) => {
|
|||
const { composeState } = useContext(ComposeContext)
|
||||
|
||||
return (
|
||||
<>
|
||||
<View>
|
||||
{composeState.attachments.uploads.length ? (
|
||||
<ComposeAttachments accessibleRefAttachments={accessibleRefAttachments} />
|
||||
) : null}
|
||||
{composeState.poll.active ? <ComposePoll /> : null}
|
||||
{composeState.replyToStatus ? <ComposeReply /> : null}
|
||||
</>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import TimelineDefault from '@components/Timeline/Default'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useContext } from 'react'
|
||||
import { StyleSheet, View } from 'react-native'
|
||||
|
@ -11,16 +12,24 @@ const ComposeReply: React.FC = () => {
|
|||
const { colors } = useTheme()
|
||||
|
||||
return (
|
||||
<View style={[styles.base, { borderTopColor: colors.border }]}>
|
||||
<TimelineDefault item={replyToStatus!} disableDetails disableOnPress />
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
minHeight: StyleConstants.Font.LineHeight.M * 5,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderRadius: StyleConstants.Spacing.S,
|
||||
overflow: 'hidden',
|
||||
borderColor: colors.border,
|
||||
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
|
||||
marginTop: StyleConstants.Spacing.M
|
||||
}}
|
||||
>
|
||||
{replyToStatus ? (
|
||||
<TimelineDefault item={replyToStatus} disableDetails disableOnPress />
|
||||
) : null}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
borderTopWidth: StyleSheet.hairlineWidth
|
||||
}
|
||||
})
|
||||
|
||||
export default React.memo(ComposeReply, () => true)
|
||||
|
|
|
@ -11,13 +11,10 @@ import ComposeTextInput from './Header/TextInput'
|
|||
const ComposeRootHeader: React.FC = () => {
|
||||
const { composeState } = useContext(ComposeContext)
|
||||
const instanceActive = useSelector(getInstanceActive)
|
||||
const localInstances = useSelector(
|
||||
getInstances,
|
||||
(prev, next) => prev.length === next.length
|
||||
)
|
||||
const localInstances = useSelector(getInstances, (prev, next) => prev.length === next.length)
|
||||
|
||||
return (
|
||||
<>
|
||||
<View>
|
||||
{instanceActive !== -1 && localInstances.length > 1 ? (
|
||||
<View style={styles.postingAs}>
|
||||
<ComposePostingAs />
|
||||
|
@ -25,7 +22,7 @@ const ComposeRootHeader: React.FC = () => {
|
|||
) : null}
|
||||
{composeState.spoiler.active ? <ComposeSpoilerInput /> : null}
|
||||
<ComposeTextInput />
|
||||
</>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next'
|
|||
import { TextInput } from 'react-native'
|
||||
import { useSelector } from 'react-redux'
|
||||
import ComposeContext from '../../utils/createContext'
|
||||
import formatText from '../../utils/formatText'
|
||||
import { formatText } from '../../utils/processText'
|
||||
|
||||
const ComposeSpoilerInput: React.FC = () => {
|
||||
const { composeState, composeDispatch } = useContext(ComposeContext)
|
||||
|
|
|
@ -10,7 +10,7 @@ import { useTranslation } from 'react-i18next'
|
|||
import { Alert } from 'react-native'
|
||||
import { useSelector } from 'react-redux'
|
||||
import ComposeContext from '../../utils/createContext'
|
||||
import formatText from '../../utils/formatText'
|
||||
import { formatText } from '../../utils/processText'
|
||||
import { uploadAttachment } from '../Footer/addAttachment'
|
||||
|
||||
const ComposeTextInput: React.FC = () => {
|
||||
|
@ -27,12 +27,11 @@ const ComposeTextInput: React.FC = () => {
|
|||
return (
|
||||
<PasteInput
|
||||
keyboardAppearance={mode}
|
||||
keyboardType='twitter'
|
||||
style={{
|
||||
...StyleConstants.FontStyle.M,
|
||||
marginTop: StyleConstants.Spacing.S,
|
||||
paddingBottom: StyleConstants.Spacing.M,
|
||||
marginLeft: StyleConstants.Spacing.Global.PagePadding,
|
||||
marginRight: StyleConstants.Spacing.Global.PagePadding,
|
||||
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
|
||||
color: colors.primaryDefault,
|
||||
borderBottomColor: colors.border,
|
||||
fontSize: adaptedFontsize,
|
||||
|
|
|
@ -3,7 +3,7 @@ import haptics from '@components/haptics'
|
|||
import ComponentHashtag from '@components/Hashtag'
|
||||
import React, { useContext, useEffect } from 'react'
|
||||
import ComposeContext from '../utils/createContext'
|
||||
import formatText from '../utils/formatText'
|
||||
import { formatText } from '../utils/processText'
|
||||
|
||||
type Props = { item: Mastodon.Account & Mastodon.Tag }
|
||||
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import apiInstance, { InstanceResponse } from '@api/instance'
|
||||
import detectLanguage from '@helpers/detectLanguage'
|
||||
import { ComposeState } from '@screens/Compose/utils/types'
|
||||
import { RootStackParamList } from '@utils/navigation/navigators'
|
||||
import * as Crypto from 'expo-crypto'
|
||||
import { getPureContent } from './processText'
|
||||
|
||||
const composePost = async (
|
||||
params: RootStackParamList['Screen-Compose'],
|
||||
|
@ -9,6 +11,13 @@ const composePost = async (
|
|||
): Promise<InstanceResponse<Mastodon.Status>> => {
|
||||
const formData = new FormData()
|
||||
|
||||
const detectedLanguage = await detectLanguage(
|
||||
getPureContent([composeState.spoiler.raw, composeState.text.raw].join('\n\n'))
|
||||
)
|
||||
if (detectedLanguage) {
|
||||
formData.append('language', detectedLanguage.language)
|
||||
}
|
||||
|
||||
if (composeState.replyToStatus) {
|
||||
try {
|
||||
await apiInstance<Mastodon.Status>({
|
||||
|
|
|
@ -150,7 +150,7 @@ const formatText = ({ textInput, composeDispatch, content, disableDebounce = fal
|
|||
})
|
||||
children.push(_content)
|
||||
contentLength = contentLength + _content.length
|
||||
|
||||
getPureContent(content)
|
||||
composeDispatch({
|
||||
type: textInput,
|
||||
payload: {
|
||||
|
@ -161,4 +161,18 @@ const formatText = ({ textInput, composeDispatch, content, disableDebounce = fal
|
|||
})
|
||||
}
|
||||
|
||||
export default formatText
|
||||
const getPureContent = (content: string): string => {
|
||||
const tags = linkify.match(content)
|
||||
if (!tags) {
|
||||
return content
|
||||
}
|
||||
|
||||
let _content = content
|
||||
for (const tag of tags) {
|
||||
_content = _content.replace(tag.raw, '')
|
||||
}
|
||||
|
||||
return _content.replace(/\s\s+/g, ' ')
|
||||
}
|
||||
|
||||
export { formatText, getPureContent }
|
|
@ -2,22 +2,19 @@ import haptics from '@components/haptics'
|
|||
import { displayMessage } from '@components/Message'
|
||||
import { CameraRoll } from '@react-native-camera-roll/camera-roll'
|
||||
import { RootStackParamList } from '@utils/navigation/navigators'
|
||||
import { Theme } from '@utils/styles/themes'
|
||||
import * as FileSystem from 'expo-file-system'
|
||||
import i18next from 'i18next'
|
||||
import { PermissionsAndroid, Platform } from 'react-native'
|
||||
|
||||
type CommonProps = {
|
||||
theme: Theme
|
||||
image: RootStackParamList['Screen-ImagesViewer']['imageUrls'][0]
|
||||
}
|
||||
|
||||
const saveIos = async ({ theme, image }: CommonProps) => {
|
||||
const saveIos = async ({ image }: CommonProps) => {
|
||||
CameraRoll.save(image.url)
|
||||
.then(() => {
|
||||
haptics('Success')
|
||||
displayMessage({
|
||||
theme,
|
||||
type: 'success',
|
||||
message: i18next.t('screenImageViewer:content.save.succeed')
|
||||
})
|
||||
|
@ -28,7 +25,6 @@ const saveIos = async ({ theme, image }: CommonProps) => {
|
|||
.then(() => {
|
||||
haptics('Success')
|
||||
displayMessage({
|
||||
theme,
|
||||
type: 'success',
|
||||
message: i18next.t('screenImageViewer:content.save.succeed')
|
||||
})
|
||||
|
@ -36,32 +32,31 @@ const saveIos = async ({ theme, image }: CommonProps) => {
|
|||
.catch(() => {
|
||||
haptics('Error')
|
||||
displayMessage({
|
||||
theme,
|
||||
type: 'error',
|
||||
type: 'danger',
|
||||
message: i18next.t('screenImageViewer:content.save.failed')
|
||||
})
|
||||
})
|
||||
} else {
|
||||
haptics('Error')
|
||||
displayMessage({
|
||||
theme,
|
||||
type: 'error',
|
||||
type: 'danger',
|
||||
message: i18next.t('screenImageViewer:content.save.failed')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const saveAndroid = async ({ theme, image }: CommonProps) => {
|
||||
const saveAndroid = async ({ image }: CommonProps) => {
|
||||
const fileUri: string = `${FileSystem.documentDirectory}${image.id}.jpg`
|
||||
const downloadedFile: FileSystem.FileSystemDownloadResult =
|
||||
await FileSystem.downloadAsync(image.url, fileUri)
|
||||
const downloadedFile: FileSystem.FileSystemDownloadResult = await FileSystem.downloadAsync(
|
||||
image.url,
|
||||
fileUri
|
||||
)
|
||||
|
||||
if (downloadedFile.status != 200) {
|
||||
haptics('Error')
|
||||
displayMessage({
|
||||
theme,
|
||||
type: 'error',
|
||||
type: 'danger',
|
||||
message: i18next.t('screenImageViewer:content.save.failed')
|
||||
})
|
||||
return
|
||||
|
@ -75,8 +70,7 @@ const saveAndroid = async ({ theme, image }: CommonProps) => {
|
|||
if (status !== 'granted') {
|
||||
haptics('Error')
|
||||
displayMessage({
|
||||
theme,
|
||||
type: 'error',
|
||||
type: 'danger',
|
||||
message: i18next.t('screenImageViewer:content.save.failed')
|
||||
})
|
||||
return
|
||||
|
@ -87,7 +81,6 @@ const saveAndroid = async ({ theme, image }: CommonProps) => {
|
|||
.then(() => {
|
||||
haptics('Success')
|
||||
displayMessage({
|
||||
theme,
|
||||
type: 'success',
|
||||
message: i18next.t('screenImageViewer:content.save.succeed')
|
||||
})
|
||||
|
@ -95,8 +88,7 @@ const saveAndroid = async ({ theme, image }: CommonProps) => {
|
|||
.catch(() => {
|
||||
haptics('Error')
|
||||
displayMessage({
|
||||
theme,
|
||||
type: 'error',
|
||||
type: 'danger',
|
||||
message: i18next.t('screenImageViewer:content.save.failed')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -40,7 +40,7 @@ const ScreenImagesViewer = ({
|
|||
|
||||
const insets = useSafeAreaInsets()
|
||||
|
||||
const { mode, theme } = useTheme()
|
||||
const { mode } = useTheme()
|
||||
const { t } = useTranslation('screenImageViewer')
|
||||
|
||||
const initialIndex = imageUrls.findIndex(image => image.id === id)
|
||||
|
@ -61,7 +61,7 @@ const ScreenImagesViewer = ({
|
|||
async buttonIndex => {
|
||||
switch (buttonIndex) {
|
||||
case 0:
|
||||
saveImage({ theme, image: imageUrls[currentIndex] })
|
||||
saveImage({ image: imageUrls[currentIndex] })
|
||||
break
|
||||
case 1:
|
||||
switch (Platform.OS) {
|
||||
|
@ -188,7 +188,7 @@ const ScreenImagesViewer = ({
|
|||
async buttonIndex => {
|
||||
switch (buttonIndex) {
|
||||
case 0:
|
||||
saveImage({ theme, image: imageUrls[currentIndex] })
|
||||
saveImage({ image: imageUrls[currentIndex] })
|
||||
break
|
||||
case 1:
|
||||
switch (Platform.OS) {
|
||||
|
|
|
@ -6,8 +6,7 @@ import { ScreenTabsScreenProps, TabLocalStackParamList } from '@utils/navigation
|
|||
import usePopToTop from '@utils/navigation/usePopToTop'
|
||||
import { useListsQuery } from '@utils/queryHooks/lists'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import layoutAnimation from '@utils/styles/layoutAnimation'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import * as DropdownMenu from 'zeego/dropdown-menu'
|
||||
import TabShared from './Shared'
|
||||
|
@ -19,9 +18,6 @@ const TabLocal = React.memo(
|
|||
const { t } = useTranslation('screenTabs')
|
||||
|
||||
const { data: lists } = useListsQuery({})
|
||||
useEffect(() => {
|
||||
layoutAnimation()
|
||||
}, [lists?.length])
|
||||
|
||||
const [queryKey, setQueryKey] = useState<QueryKeyTimeline>(['Timeline', { page: 'Following' }])
|
||||
|
||||
|
|
|
@ -129,15 +129,11 @@ const TabMe = React.memo(
|
|||
name='Tab-Me-Push'
|
||||
component={TabMePush}
|
||||
options={({ navigation }) => ({
|
||||
presentation: 'modal',
|
||||
headerShown: true,
|
||||
title: t('me.stacks.push.name'),
|
||||
...(Platform.OS === 'android' && {
|
||||
headerCenter: () => <HeaderCenter content={t('me.stacks.push.name')} />
|
||||
}),
|
||||
headerLeft: () => (
|
||||
<HeaderLeft content='ChevronDown' onPress={() => navigation.goBack()} />
|
||||
)
|
||||
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />
|
||||
})}
|
||||
/>
|
||||
<Stack.Screen
|
||||
|
|
|
@ -41,8 +41,7 @@ const TabMeListAccounts: React.FC<TabMeStackScreenProps<'Tab-Me-List-Accounts'>>
|
|||
},
|
||||
onError: () => {
|
||||
displayMessage({
|
||||
theme,
|
||||
type: 'error',
|
||||
type: 'danger',
|
||||
message: t('common:message.error.message', {
|
||||
function: t('me.listAccounts.error')
|
||||
})
|
||||
|
|
|
@ -41,8 +41,7 @@ const TabMeListEdit: React.FC<TabMeStackScreenProps<'Tab-Me-List-Edit'>> = ({
|
|||
onError: () => {
|
||||
displayMessage({
|
||||
ref: messageRef,
|
||||
theme,
|
||||
type: 'error',
|
||||
type: 'danger',
|
||||
message: t('common:message.error.message', {
|
||||
function:
|
||||
params.type === 'add' ? t('me.stacks.listAdd.name') : t('me.stacks.listEdit.name')
|
||||
|
|
|
@ -17,7 +17,7 @@ const TabMeList: React.FC<TabMeStackScreenProps<'Tab-Me-List'>> = ({
|
|||
navigation,
|
||||
route: { key, params }
|
||||
}) => {
|
||||
const { colors, theme } = useTheme()
|
||||
const { colors } = useTheme()
|
||||
const { t } = useTranslation('screenTabs')
|
||||
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'List', list: params.id }]
|
||||
|
||||
|
@ -30,8 +30,7 @@ const TabMeList: React.FC<TabMeStackScreenProps<'Tab-Me-List'>> = ({
|
|||
},
|
||||
onError: () => {
|
||||
displayMessage({
|
||||
theme,
|
||||
type: 'error',
|
||||
type: 'danger',
|
||||
message: t('common:message.error.message', {
|
||||
function: t('me.listDelete.heading')
|
||||
})
|
||||
|
|
|
@ -121,7 +121,6 @@ const TabMeProfileFields: React.FC<
|
|||
content='Save'
|
||||
onPress={async () => {
|
||||
mutateAsync({
|
||||
theme,
|
||||
messageRef,
|
||||
message: {
|
||||
text: 'me.profile.root.note.title',
|
||||
|
|
|
@ -75,7 +75,6 @@ const TabMeProfileName: React.FC<
|
|||
content='Save'
|
||||
onPress={async () => {
|
||||
mutateAsync({
|
||||
theme,
|
||||
messageRef,
|
||||
message: {
|
||||
text: 'me.profile.root.name.title',
|
||||
|
|
|
@ -75,7 +75,6 @@ const TabMeProfileNote: React.FC<
|
|||
content='Save'
|
||||
onPress={async () => {
|
||||
mutateAsync({
|
||||
theme,
|
||||
messageRef,
|
||||
message: {
|
||||
text: 'me.profile.root.note.title',
|
||||
|
|
|
@ -5,7 +5,7 @@ import { TabMeProfileStackScreenProps } from '@utils/navigation/navigators'
|
|||
import { useProfileMutation, useProfileQuery } from '@utils/queryHooks/profile'
|
||||
import { updateAccountPreferences } from '@utils/slices/instances/updateAccountPreferences'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { RefObject, useCallback } from 'react'
|
||||
import React, { RefObject } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import FlashMessage from 'react-native-flash-message'
|
||||
import { ScrollView } from 'react-native-gesture-handler'
|
||||
|
@ -16,99 +16,15 @@ const TabMeProfileRoot: React.FC<
|
|||
messageRef: RefObject<FlashMessage>
|
||||
}
|
||||
> = ({ messageRef, navigation }) => {
|
||||
const { mode, theme } = useTheme()
|
||||
const { mode } = useTheme()
|
||||
const { t } = useTranslation('screenTabs')
|
||||
|
||||
const { showActionSheetWithOptions } = useActionSheet()
|
||||
|
||||
const { data, isLoading } = useProfileQuery({})
|
||||
const { data, isLoading } = useProfileQuery()
|
||||
const { mutateAsync } = useProfileMutation()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const onPressVisibility = useCallback(() => {
|
||||
showActionSheetWithOptions(
|
||||
{
|
||||
title: t('me.profile.root.visibility.title'),
|
||||
options: [
|
||||
t('me.profile.root.visibility.options.public'),
|
||||
t('me.profile.root.visibility.options.unlisted'),
|
||||
t('me.profile.root.visibility.options.private'),
|
||||
t('common:buttons.cancel')
|
||||
],
|
||||
cancelButtonIndex: 3,
|
||||
userInterfaceStyle: mode
|
||||
},
|
||||
async buttonIndex => {
|
||||
switch (buttonIndex) {
|
||||
case 0:
|
||||
case 1:
|
||||
case 2:
|
||||
const indexVisibilityMapping = ['public', 'unlisted', 'private'] as [
|
||||
'public',
|
||||
'unlisted',
|
||||
'private'
|
||||
]
|
||||
if (data?.source.privacy !== indexVisibilityMapping[buttonIndex]) {
|
||||
mutateAsync({
|
||||
theme,
|
||||
messageRef,
|
||||
message: {
|
||||
text: 'me.profile.root.visibility.title',
|
||||
succeed: false,
|
||||
failed: true
|
||||
},
|
||||
type: 'source[privacy]',
|
||||
data: indexVisibilityMapping[buttonIndex]
|
||||
}).then(() => dispatch(updateAccountPreferences()))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
)
|
||||
}, [theme, data?.source?.privacy])
|
||||
|
||||
const onPressSensitive = useCallback(() => {
|
||||
mutateAsync({
|
||||
theme,
|
||||
messageRef,
|
||||
message: {
|
||||
text: 'me.profile.root.sensitive.title',
|
||||
succeed: false,
|
||||
failed: true
|
||||
},
|
||||
type: 'source[sensitive]',
|
||||
data: data?.source.sensitive === undefined ? true : !data.source.sensitive
|
||||
}).then(() => dispatch(updateAccountPreferences()))
|
||||
}, [data?.source.sensitive])
|
||||
|
||||
const onPressLock = useCallback(() => {
|
||||
mutateAsync({
|
||||
theme,
|
||||
messageRef,
|
||||
message: {
|
||||
text: 'me.profile.root.lock.title',
|
||||
succeed: false,
|
||||
failed: true
|
||||
},
|
||||
type: 'locked',
|
||||
data: data?.locked === undefined ? true : !data.locked
|
||||
})
|
||||
}, [theme, data?.locked])
|
||||
|
||||
const onPressBot = useCallback(() => {
|
||||
mutateAsync({
|
||||
theme,
|
||||
messageRef,
|
||||
message: {
|
||||
text: 'me.profile.root.bot.title',
|
||||
succeed: false,
|
||||
failed: true
|
||||
},
|
||||
type: 'bot',
|
||||
data: data?.bot === undefined ? true : !data.bot
|
||||
})
|
||||
}, [theme, data?.bot])
|
||||
|
||||
return (
|
||||
<ScrollView>
|
||||
<MenuContainer>
|
||||
|
@ -166,12 +82,62 @@ const TabMeProfileRoot: React.FC<
|
|||
}
|
||||
loading={isLoading}
|
||||
iconBack='ChevronRight'
|
||||
onPress={onPressVisibility}
|
||||
onPress={() =>
|
||||
showActionSheetWithOptions(
|
||||
{
|
||||
title: t('me.profile.root.visibility.title'),
|
||||
options: [
|
||||
t('me.profile.root.visibility.options.public'),
|
||||
t('me.profile.root.visibility.options.unlisted'),
|
||||
t('me.profile.root.visibility.options.private'),
|
||||
t('common:buttons.cancel')
|
||||
],
|
||||
cancelButtonIndex: 3,
|
||||
userInterfaceStyle: mode
|
||||
},
|
||||
async buttonIndex => {
|
||||
switch (buttonIndex) {
|
||||
case 0:
|
||||
case 1:
|
||||
case 2:
|
||||
const indexVisibilityMapping = ['public', 'unlisted', 'private'] as [
|
||||
'public',
|
||||
'unlisted',
|
||||
'private'
|
||||
]
|
||||
if (data?.source.privacy !== indexVisibilityMapping[buttonIndex]) {
|
||||
mutateAsync({
|
||||
messageRef,
|
||||
message: {
|
||||
text: 'me.profile.root.visibility.title',
|
||||
succeed: false,
|
||||
failed: true
|
||||
},
|
||||
type: 'source[privacy]',
|
||||
data: indexVisibilityMapping[buttonIndex]
|
||||
}).then(() => dispatch(updateAccountPreferences()))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
/>
|
||||
<MenuRow
|
||||
title={t('me.profile.root.sensitive.title')}
|
||||
switchValue={data?.source.sensitive}
|
||||
switchOnValueChange={onPressSensitive}
|
||||
switchOnValueChange={() =>
|
||||
mutateAsync({
|
||||
messageRef,
|
||||
message: {
|
||||
text: 'me.profile.root.sensitive.title',
|
||||
succeed: false,
|
||||
failed: true
|
||||
},
|
||||
type: 'source[sensitive]',
|
||||
data: data?.source.sensitive === undefined ? true : !data.source.sensitive
|
||||
}).then(() => dispatch(updateAccountPreferences()))
|
||||
}
|
||||
loading={isLoading}
|
||||
/>
|
||||
</MenuContainer>
|
||||
|
@ -180,14 +146,36 @@ const TabMeProfileRoot: React.FC<
|
|||
title={t('me.profile.root.lock.title')}
|
||||
description={t('me.profile.root.lock.description')}
|
||||
switchValue={data?.locked}
|
||||
switchOnValueChange={onPressLock}
|
||||
switchOnValueChange={() =>
|
||||
mutateAsync({
|
||||
messageRef,
|
||||
message: {
|
||||
text: 'me.profile.root.lock.title',
|
||||
succeed: false,
|
||||
failed: true
|
||||
},
|
||||
type: 'locked',
|
||||
data: data?.locked === undefined ? true : !data.locked
|
||||
})
|
||||
}
|
||||
loading={isLoading}
|
||||
/>
|
||||
<MenuRow
|
||||
title={t('me.profile.root.bot.title')}
|
||||
description={t('me.profile.root.bot.description')}
|
||||
switchValue={data?.bot}
|
||||
switchOnValueChange={onPressBot}
|
||||
switchOnValueChange={() =>
|
||||
mutateAsync({
|
||||
messageRef,
|
||||
message: {
|
||||
text: 'me.profile.root.bot.title',
|
||||
succeed: false,
|
||||
failed: true
|
||||
},
|
||||
type: 'bot',
|
||||
data: data?.bot === undefined ? true : !data.bot
|
||||
})
|
||||
}
|
||||
loading={isLoading}
|
||||
/>
|
||||
</MenuContainer>
|
||||
|
|
|
@ -3,7 +3,6 @@ import { MenuRow } from '@components/Menu'
|
|||
import { displayMessage } from '@components/Message'
|
||||
import { useActionSheet } from '@expo/react-native-action-sheet'
|
||||
import { useProfileMutation, useProfileQuery } from '@utils/queryHooks/profile'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { RefObject } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import FlashMessage from 'react-native-flash-message'
|
||||
|
@ -14,12 +13,11 @@ export interface Props {
|
|||
}
|
||||
|
||||
const ProfileAvatarHeader: React.FC<Props> = ({ type, messageRef }) => {
|
||||
const { theme } = useTheme()
|
||||
const { t } = useTranslation('screenTabs')
|
||||
|
||||
const { showActionSheetWithOptions } = useActionSheet()
|
||||
|
||||
const query = useProfileQuery({})
|
||||
const query = useProfileQuery()
|
||||
const mutation = useProfileMutation()
|
||||
|
||||
return (
|
||||
|
@ -40,7 +38,6 @@ const ProfileAvatarHeader: React.FC<Props> = ({ type, messageRef }) => {
|
|||
})
|
||||
if (image[0].uri) {
|
||||
mutation.mutate({
|
||||
theme,
|
||||
messageRef,
|
||||
message: {
|
||||
text: `me.profile.root.${type}.title`,
|
||||
|
@ -54,8 +51,7 @@ const ProfileAvatarHeader: React.FC<Props> = ({ type, messageRef }) => {
|
|||
displayMessage({
|
||||
ref: messageRef,
|
||||
message: t('screenTabs:me.profile.mediaSelectionFailed'),
|
||||
theme: theme,
|
||||
type: 'error'
|
||||
type: 'danger'
|
||||
})
|
||||
}
|
||||
}}
|
||||
|
|
|
@ -5,31 +5,42 @@ import CustomText from '@components/Text'
|
|||
import browserPackage from '@helpers/browserPackage'
|
||||
import { useAppDispatch } from '@root/store'
|
||||
import { isDevelopment } from '@utils/checkEnvironment'
|
||||
import { getExpoToken } from '@utils/slices/appSlice'
|
||||
import { useAppsQuery } from '@utils/queryHooks/apps'
|
||||
import { useProfileQuery } from '@utils/queryHooks/profile'
|
||||
import { getExpoToken, retrieveExpoToken } from '@utils/slices/appSlice'
|
||||
import {
|
||||
checkPushAdminPermission,
|
||||
PUSH_ADMIN,
|
||||
PUSH_DEFAULT,
|
||||
setChannels
|
||||
} from '@utils/slices/instances/push/utils'
|
||||
import { updateInstancePush } from '@utils/slices/instances/updatePush'
|
||||
import { updateInstancePushAlert } from '@utils/slices/instances/updatePushAlert'
|
||||
import { updateInstancePushDecode } from '@utils/slices/instances/updatePushDecode'
|
||||
import {
|
||||
clearPushLoading,
|
||||
getInstanceAccount,
|
||||
getInstancePush,
|
||||
getInstanceUri
|
||||
} from '@utils/slices/instancesSlice'
|
||||
import { getInstance, getInstancePush } from '@utils/slices/instancesSlice'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import layoutAnimation from '@utils/styles/layoutAnimation'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import * as Notifications from 'expo-notifications'
|
||||
import * as WebBrowser from 'expo-web-browser'
|
||||
import React, { useState, useEffect, useMemo } from 'react'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { AppState, Linking, ScrollView, View } from 'react-native'
|
||||
import { AppState, Linking, Platform, ScrollView, View } from 'react-native'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
const TabMePush: React.FC = () => {
|
||||
const { colors } = useTheme()
|
||||
const { t } = useTranslation('screenTabs')
|
||||
const instanceAccount = useSelector(getInstanceAccount, (prev, next) => prev?.acct === next?.acct)
|
||||
const instanceUri = useSelector(getInstanceUri)
|
||||
|
||||
const instance = useSelector(getInstance)
|
||||
const expoToken = useSelector(getExpoToken)
|
||||
|
||||
const [serverKeyAvailable, setServerKeyAvailable] = useState<boolean>()
|
||||
useAppsQuery({
|
||||
options: {
|
||||
onSuccess: data => setServerKeyAvailable(!!data.vapid_key)
|
||||
}
|
||||
})
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
const instancePush = useSelector(getInstancePush)
|
||||
|
@ -37,61 +48,55 @@ const TabMePush: React.FC = () => {
|
|||
const [pushAvailable, setPushAvailable] = useState<boolean>()
|
||||
const [pushEnabled, setPushEnabled] = useState<boolean>()
|
||||
const [pushCanAskAgain, setPushCanAskAgain] = useState<boolean>()
|
||||
const checkPush = async () => {
|
||||
const settings = await Notifications.getPermissionsAsync()
|
||||
layoutAnimation()
|
||||
setPushEnabled(settings.granted)
|
||||
setPushCanAskAgain(settings.canAskAgain)
|
||||
}
|
||||
const expoToken = useSelector(getExpoToken)
|
||||
|
||||
useEffect(() => {
|
||||
if (isDevelopment) {
|
||||
setPushAvailable(true)
|
||||
} else {
|
||||
setPushAvailable(!!expoToken)
|
||||
const checkPush = async () => {
|
||||
switch (Platform.OS) {
|
||||
case 'ios':
|
||||
const settings = await Notifications.getPermissionsAsync()
|
||||
layoutAnimation()
|
||||
setPushEnabled(settings.granted)
|
||||
setPushCanAskAgain(settings.canAskAgain)
|
||||
break
|
||||
case 'android':
|
||||
await setChannels(instance)
|
||||
layoutAnimation()
|
||||
dispatch(retrieveExpoToken())
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (serverKeyAvailable) {
|
||||
checkPush()
|
||||
|
||||
if (isDevelopment) {
|
||||
setPushAvailable(true)
|
||||
} else {
|
||||
setPushAvailable(!!expoToken)
|
||||
}
|
||||
}
|
||||
|
||||
checkPush()
|
||||
const subscription = AppState.addEventListener('change', checkPush)
|
||||
return () => {
|
||||
subscription.remove()
|
||||
}
|
||||
}, [])
|
||||
}, [serverKeyAvailable])
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(clearPushLoading())
|
||||
}, [])
|
||||
|
||||
const isLoading = instancePush?.global.loading || instancePush?.decode.loading
|
||||
|
||||
const alerts = useMemo(() => {
|
||||
return instancePush?.alerts
|
||||
? (
|
||||
['follow', 'follow_request', 'favourite', 'reblog', 'mention', 'poll', 'status'] as [
|
||||
'follow',
|
||||
'follow_request',
|
||||
'favourite',
|
||||
'reblog',
|
||||
'mention',
|
||||
'poll',
|
||||
'status'
|
||||
]
|
||||
).map(alert => (
|
||||
const alerts = () =>
|
||||
instancePush?.alerts
|
||||
? PUSH_DEFAULT.map(alert => (
|
||||
<MenuRow
|
||||
key={alert}
|
||||
title={t(`me.push.${alert}.heading`)}
|
||||
switchDisabled={!pushEnabled || !instancePush.global.value || isLoading}
|
||||
switchValue={instancePush?.alerts[alert].value}
|
||||
switchDisabled={!pushEnabled || !instancePush.global}
|
||||
switchValue={instancePush?.alerts[alert]}
|
||||
switchOnValueChange={() =>
|
||||
dispatch(
|
||||
updateInstancePushAlert({
|
||||
changed: alert,
|
||||
alerts: {
|
||||
...instancePush?.alerts,
|
||||
[alert]: {
|
||||
...instancePush?.alerts[alert],
|
||||
value: !instancePush?.alerts[alert].value
|
||||
}
|
||||
[alert]: instancePush?.alerts[alert]
|
||||
}
|
||||
})
|
||||
)
|
||||
|
@ -99,69 +104,120 @@ const TabMePush: React.FC = () => {
|
|||
/>
|
||||
))
|
||||
: null
|
||||
}, [pushEnabled, instancePush?.global, instancePush?.alerts, isLoading])
|
||||
|
||||
const profileQuery = useProfileQuery()
|
||||
const adminAlerts = () =>
|
||||
profileQuery.data?.role?.permissions
|
||||
? PUSH_ADMIN.map(({ type, permission }) =>
|
||||
checkPushAdminPermission(permission, profileQuery.data.role?.permissions) ? (
|
||||
<MenuRow
|
||||
key={type}
|
||||
title={t(`me.push.${type}.heading`)}
|
||||
switchDisabled={!pushEnabled || !instancePush.global}
|
||||
switchValue={instancePush?.alerts[type]}
|
||||
switchOnValueChange={() =>
|
||||
dispatch(
|
||||
updateInstancePushAlert({
|
||||
changed: type,
|
||||
alerts: {
|
||||
...instancePush?.alerts,
|
||||
[type]: instancePush?.alerts[type]
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : null
|
||||
)
|
||||
: null
|
||||
|
||||
return (
|
||||
<ScrollView>
|
||||
{!!pushAvailable ? (
|
||||
{!!serverKeyAvailable ? (
|
||||
<>
|
||||
{pushEnabled === false ? (
|
||||
<MenuContainer>
|
||||
<Button
|
||||
type='text'
|
||||
content={
|
||||
pushCanAskAgain ? t('me.push.enable.direct') : t('me.push.enable.settings')
|
||||
}
|
||||
style={{
|
||||
marginTop: StyleConstants.Spacing.Global.PagePadding,
|
||||
marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2
|
||||
}}
|
||||
onPress={async () => {
|
||||
if (pushCanAskAgain) {
|
||||
const result = await Notifications.requestPermissionsAsync()
|
||||
setPushEnabled(result.granted)
|
||||
setPushCanAskAgain(result.canAskAgain)
|
||||
} else {
|
||||
Linking.openSettings()
|
||||
{!!pushAvailable ? (
|
||||
<>
|
||||
{pushEnabled === false ? (
|
||||
<MenuContainer>
|
||||
<Button
|
||||
type='text'
|
||||
content={
|
||||
pushCanAskAgain ? t('me.push.enable.direct') : t('me.push.enable.settings')
|
||||
}
|
||||
style={{
|
||||
marginTop: StyleConstants.Spacing.Global.PagePadding,
|
||||
marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2
|
||||
}}
|
||||
onPress={async () => {
|
||||
if (pushCanAskAgain) {
|
||||
const result = await Notifications.requestPermissionsAsync()
|
||||
setPushEnabled(result.granted)
|
||||
setPushCanAskAgain(result.canAskAgain)
|
||||
} else {
|
||||
Linking.openSettings()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</MenuContainer>
|
||||
) : null}
|
||||
<MenuContainer>
|
||||
<MenuRow
|
||||
title={t('me.push.global.heading', {
|
||||
acct: `@${instance.account.acct}@${instance.uri}`
|
||||
})}
|
||||
description={t('me.push.global.description')}
|
||||
switchDisabled={!pushEnabled}
|
||||
switchValue={pushEnabled === false ? false : instancePush?.global}
|
||||
switchOnValueChange={() => dispatch(updateInstancePush(!instancePush?.global))}
|
||||
/>
|
||||
</MenuContainer>
|
||||
<MenuContainer>
|
||||
<MenuRow
|
||||
title={t('me.push.decode.heading')}
|
||||
description={t('me.push.decode.description')}
|
||||
loading={instancePush?.decode}
|
||||
switchDisabled={!pushEnabled || !instancePush?.global}
|
||||
switchValue={instancePush?.decode}
|
||||
switchOnValueChange={() =>
|
||||
dispatch(updateInstancePushDecode(!instancePush?.decode))
|
||||
}
|
||||
/>
|
||||
<MenuRow
|
||||
title={t('me.push.howitworks')}
|
||||
iconBack='ExternalLink'
|
||||
onPress={async () =>
|
||||
WebBrowser.openBrowserAsync('https://tooot.app/how-push-works', {
|
||||
browserPackage: await browserPackage()
|
||||
})
|
||||
}
|
||||
/>
|
||||
</MenuContainer>
|
||||
<MenuContainer children={alerts()} />
|
||||
<MenuContainer children={adminAlerts()} />
|
||||
</>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
minHeight: '100%',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding
|
||||
}}
|
||||
>
|
||||
<Icon name='Frown' size={StyleConstants.Font.Size.L} color={colors.primaryDefault} />
|
||||
<CustomText
|
||||
fontStyle='M'
|
||||
style={{
|
||||
color: colors.primaryDefault,
|
||||
textAlign: 'center',
|
||||
marginTop: StyleConstants.Spacing.S
|
||||
}}
|
||||
/>
|
||||
</MenuContainer>
|
||||
) : null}
|
||||
<MenuContainer>
|
||||
<MenuRow
|
||||
title={t('me.push.global.heading', {
|
||||
acct: `@${instanceAccount?.acct}@${instanceUri}`
|
||||
})}
|
||||
description={t('me.push.global.description')}
|
||||
loading={instancePush?.global.loading}
|
||||
switchDisabled={!pushEnabled || isLoading}
|
||||
switchValue={pushEnabled === false ? false : instancePush?.global.value}
|
||||
switchOnValueChange={() => dispatch(updateInstancePush(!instancePush?.global.value))}
|
||||
/>
|
||||
</MenuContainer>
|
||||
<MenuContainer>
|
||||
<MenuRow
|
||||
title={t('me.push.decode.heading')}
|
||||
description={t('me.push.decode.description')}
|
||||
loading={instancePush?.decode.loading}
|
||||
switchDisabled={!pushEnabled || !instancePush?.global.value || isLoading}
|
||||
switchValue={instancePush?.decode.value}
|
||||
switchOnValueChange={() =>
|
||||
dispatch(updateInstancePushDecode(!instancePush?.decode.value))
|
||||
}
|
||||
/>
|
||||
<MenuRow
|
||||
title={t('me.push.howitworks')}
|
||||
iconBack='ExternalLink'
|
||||
onPress={async () =>
|
||||
WebBrowser.openBrowserAsync('https://tooot.app/how-push-works', {
|
||||
browserPackage: await browserPackage()
|
||||
})
|
||||
}
|
||||
/>
|
||||
</MenuContainer>
|
||||
<MenuContainer>{alerts}</MenuContainer>
|
||||
>
|
||||
{t('me.push.notAvailable')}
|
||||
</CustomText>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<View
|
||||
|
@ -169,12 +225,29 @@ const TabMePush: React.FC = () => {
|
|||
flex: 1,
|
||||
minHeight: '100%',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding
|
||||
}}
|
||||
>
|
||||
<Icon name='Frown' size={StyleConstants.Font.Size.L} color={colors.primaryDefault} />
|
||||
<CustomText fontStyle='M' style={{ color: colors.primaryDefault }}>
|
||||
{t('me.push.notAvailable')}
|
||||
<CustomText
|
||||
fontStyle='M'
|
||||
style={{
|
||||
color: colors.primaryDefault,
|
||||
textAlign: 'center',
|
||||
marginTop: StyleConstants.Spacing.S
|
||||
}}
|
||||
>
|
||||
{t('me.push.missingServerKey.message')}
|
||||
</CustomText>
|
||||
<CustomText
|
||||
fontStyle='S'
|
||||
style={{
|
||||
color: colors.primaryDefault,
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
{t('me.push.missingServerKey.description')}
|
||||
</CustomText>
|
||||
</View>
|
||||
)}
|
||||
|
|
|
@ -50,10 +50,7 @@ const Collections: React.FC = () => {
|
|||
}
|
||||
}, [announcementsQuery.data])
|
||||
|
||||
const instancePush = useSelector(
|
||||
getInstancePush,
|
||||
(prev, next) => prev?.global.value === next?.global.value
|
||||
)
|
||||
const instancePush = useSelector(getInstancePush, (prev, next) => prev?.global === next?.global)
|
||||
|
||||
return (
|
||||
<MenuContainer>
|
||||
|
@ -102,11 +99,7 @@ const Collections: React.FC = () => {
|
|||
iconFront={instancePush ? 'Bell' : 'BellOff'}
|
||||
iconBack='ChevronRight'
|
||||
title={t('me.stacks.push.name')}
|
||||
content={
|
||||
instancePush.global.value
|
||||
? t('me.root.push.content.enabled')
|
||||
: t('me.root.push.content.disabled')
|
||||
}
|
||||
content={t('me.root.push.content', { context: instancePush.global.toString() })}
|
||||
onPress={() => navigation.navigate('Tab-Me-Push')}
|
||||
/>
|
||||
</MenuContainer>
|
||||
|
|
|
@ -4,13 +4,11 @@ import { useNavigation } from '@react-navigation/native'
|
|||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import * as Linking from 'expo-linking'
|
||||
import * as StoreReview from 'expo-store-review'
|
||||
import * as WebBrowser from 'expo-web-browser'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { getInstanceActive, getInstanceVersion } from '@utils/slices/instancesSlice'
|
||||
import { isDevelopment, isRelease } from '@utils/checkEnvironment'
|
||||
import { Platform } from 'react-native'
|
||||
import Constants from 'expo-constants'
|
||||
import { getExpoToken } from '@utils/slices/appSlice'
|
||||
|
@ -27,6 +25,12 @@ const SettingsTooot: React.FC = () => {
|
|||
|
||||
return (
|
||||
<MenuContainer>
|
||||
<MenuRow
|
||||
title={t('me.settings.support.heading')}
|
||||
content={<Icon name='Heart' size={StyleConstants.Font.Size.M} color={colors.red} />}
|
||||
iconBack='ChevronRight'
|
||||
onPress={() => Linking.openURL('https://www.buymeacoffee.com/xmflsct')}
|
||||
/>
|
||||
<MenuRow
|
||||
title={t('me.settings.feedback.heading')}
|
||||
content={
|
||||
|
@ -35,20 +39,6 @@ const SettingsTooot: React.FC = () => {
|
|||
iconBack='ChevronRight'
|
||||
onPress={() => Linking.openURL('https://feedback.tooot.app/feature-requests')}
|
||||
/>
|
||||
<MenuRow
|
||||
title={t('me.settings.support.heading')}
|
||||
content={<Icon name='Heart' size={StyleConstants.Font.Size.M} color={colors.red} />}
|
||||
iconBack='ChevronRight'
|
||||
onPress={() => Linking.openURL('https://www.buymeacoffee.com/xmflsct')}
|
||||
/>
|
||||
{isDevelopment || isRelease ? (
|
||||
<MenuRow
|
||||
title={t('me.settings.review.heading')}
|
||||
content={<Icon name='Star' size={StyleConstants.Font.Size.M} color='#FF9500' />}
|
||||
iconBack='ChevronRight'
|
||||
onPress={() => StoreReview?.isAvailableAsync().then(() => StoreReview?.requestReview())}
|
||||
/>
|
||||
) : null}
|
||||
<MenuRow
|
||||
title={t('me.settings.contact.heading')}
|
||||
content={<Icon name='Mail' size={StyleConstants.Font.Size.M} color={colors.secondary} />}
|
||||
|
|
|
@ -2,19 +2,18 @@ import haptics from '@components/haptics'
|
|||
import { MenuContainer, MenuRow } from '@components/Menu'
|
||||
import { LOCALES } from '@root/i18n/locales'
|
||||
import { TabMeStackScreenProps } from '@utils/navigation/navigators'
|
||||
import androidDefaults from '@utils/slices/instances/push/androidDefaults'
|
||||
import { setChannels } from '@utils/slices/instances/push/utils'
|
||||
import { getInstances } from '@utils/slices/instancesSlice'
|
||||
import { changeLanguage } from '@utils/slices/settingsSlice'
|
||||
import * as Notifications from 'expo-notifications'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FlatList, Platform } from 'react-native'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
const TabMeSettingsLanguage: React.FC<
|
||||
TabMeStackScreenProps<'Tab-Me-Settings-Language'>
|
||||
> = ({ navigation }) => {
|
||||
const { i18n, t } = useTranslation('screenTabs')
|
||||
const TabMeSettingsLanguage: React.FC<TabMeStackScreenProps<'Tab-Me-Settings-Language'>> = ({
|
||||
navigation
|
||||
}) => {
|
||||
const { i18n } = useTranslation('screenTabs')
|
||||
const languages = Object.entries(LOCALES)
|
||||
const instances = useSelector(getInstances)
|
||||
const dispatch = useDispatch()
|
||||
|
@ -27,45 +26,7 @@ const TabMeSettingsLanguage: React.FC<
|
|||
|
||||
// Update Android notification channel language
|
||||
if (Platform.OS === 'android') {
|
||||
instances.forEach(instance => {
|
||||
const accountFull = `@${instance.account.acct}@${instance.uri}`
|
||||
if (instance.push.decode.value === false) {
|
||||
Notifications.setNotificationChannelAsync(`${accountFull}_default`, {
|
||||
groupId: accountFull,
|
||||
name: t('me.push.default.heading'),
|
||||
...androidDefaults
|
||||
})
|
||||
} else {
|
||||
Notifications.setNotificationChannelAsync(`${accountFull}_follow`, {
|
||||
groupId: accountFull,
|
||||
name: t('me.push.follow.heading'),
|
||||
...androidDefaults
|
||||
})
|
||||
Notifications.setNotificationChannelAsync(
|
||||
`${accountFull}_favourite`,
|
||||
{
|
||||
groupId: accountFull,
|
||||
name: t('me.push.favourite.heading'),
|
||||
...androidDefaults
|
||||
}
|
||||
)
|
||||
Notifications.setNotificationChannelAsync(`${accountFull}_reblog`, {
|
||||
groupId: accountFull,
|
||||
name: t('me.push.reblog.heading'),
|
||||
...androidDefaults
|
||||
})
|
||||
Notifications.setNotificationChannelAsync(`${accountFull}_mention`, {
|
||||
groupId: accountFull,
|
||||
name: t('me.push.mention.heading'),
|
||||
...androidDefaults
|
||||
})
|
||||
Notifications.setNotificationChannelAsync(`${accountFull}_poll`, {
|
||||
groupId: accountFull,
|
||||
name: t('me.push.poll.heading'),
|
||||
...androidDefaults
|
||||
})
|
||||
}
|
||||
})
|
||||
instances.forEach(setChannels)
|
||||
}
|
||||
|
||||
navigation.pop(1)
|
||||
|
|
|
@ -23,7 +23,13 @@ const AccountInformationNote = React.memo(
|
|||
|
||||
return (
|
||||
<View style={styles.note}>
|
||||
<ParseHTML content={account.note!} size={'M'} emojis={account.emojis} selectable />
|
||||
<ParseHTML
|
||||
content={account.note!}
|
||||
size={'M'}
|
||||
emojis={account.emojis}
|
||||
selectable
|
||||
numberOfLines={999}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
},
|
||||
|
|
|
@ -72,16 +72,18 @@ const SearchEmpty: React.FC<Props> = ({ isLoading, inputRef, setSearchTerm }) =>
|
|||
</CustomText>
|
||||
</View>
|
||||
|
||||
<CustomText
|
||||
style={{
|
||||
color: colors.primaryDefault,
|
||||
marginTop: StyleConstants.Spacing.M,
|
||||
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding
|
||||
}}
|
||||
fontWeight='Bold'
|
||||
>
|
||||
{t('shared.search.empty.trending.tags')}
|
||||
</CustomText>
|
||||
{trendsTags.data?.length ? (
|
||||
<CustomText
|
||||
style={{
|
||||
color: colors.primaryDefault,
|
||||
marginTop: StyleConstants.Spacing.M,
|
||||
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding
|
||||
}}
|
||||
fontWeight='Bold'
|
||||
>
|
||||
{t('shared.search.empty.trending.tags')}
|
||||
</CustomText>
|
||||
) : null}
|
||||
<View>
|
||||
{trendsTags.data?.map((tag, index) => {
|
||||
const hashtag = tag as Mastodon.Tag
|
||||
|
|
|
@ -1,13 +1,8 @@
|
|||
import apiInstance from '@api/instance'
|
||||
import NetInfo from '@react-native-community/netinfo'
|
||||
import { store } from '@root/store'
|
||||
import initQuery from '@utils/initQuery'
|
||||
import { getPreviousTab } from '@utils/slices/contextsSlice'
|
||||
import removeInstance from '@utils/slices/instances/remove'
|
||||
import {
|
||||
getInstance,
|
||||
updateInstanceAccount
|
||||
} from '@utils/slices/instancesSlice'
|
||||
import { getInstance, updateInstanceAccount } from '@utils/slices/instancesSlice'
|
||||
import { onlineManager } from 'react-query'
|
||||
import log from './log'
|
||||
|
||||
|
@ -22,9 +17,7 @@ const netInfo = async (): Promise<{
|
|||
|
||||
onlineManager.setEventListener(setOnline => {
|
||||
return NetInfo.addEventListener(state => {
|
||||
setOnline(
|
||||
typeof state.isConnected === 'boolean' ? state.isConnected : undefined
|
||||
)
|
||||
setOnline(typeof state.isConnected === 'boolean' ? state.isConnected : undefined)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -60,23 +53,6 @@ const netInfo = async (): Promise<{
|
|||
})
|
||||
)
|
||||
|
||||
if (instance.timelinesLookback) {
|
||||
const previousTab = getPreviousTab(store.getState())
|
||||
let loadPage:
|
||||
| Extract<App.Pages, 'Following' | 'Local' | 'LocalPublic'>
|
||||
| undefined = undefined
|
||||
if (previousTab === 'Tab-Local') {
|
||||
loadPage = 'Following'
|
||||
} else if (previousTab === 'Tab-Public') {
|
||||
loadPage = 'LocalPublic'
|
||||
}
|
||||
|
||||
// await initQuery({
|
||||
// instance,
|
||||
// prefetch: { enabled: true, page: loadPage }
|
||||
// })
|
||||
}
|
||||
|
||||
return Promise.resolve({ connected: true })
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -46,7 +46,7 @@ const instancesPersistConfig = {
|
|||
key: 'instances',
|
||||
prefix,
|
||||
storage: Platform.OS === 'ios' ? secureStorage : AsyncStorage,
|
||||
version: 10,
|
||||
version: 11,
|
||||
// @ts-ignore
|
||||
migrate: createMigrate(instancesMigration)
|
||||
}
|
||||
|
|
|
@ -1,33 +1,11 @@
|
|||
import queryClient from '@helpers/queryClient'
|
||||
import { store } from '@root/store'
|
||||
import { InstanceLatest } from './migrations/instances/migration'
|
||||
// import { prefetchTimelineQuery } from './queryHooks/timeline'
|
||||
import { updateInstanceActive } from './slices/instancesSlice'
|
||||
|
||||
const initQuery = async ({
|
||||
instance,
|
||||
prefetch
|
||||
}: {
|
||||
instance: InstanceLatest
|
||||
prefetch?: { enabled: boolean; page?: 'Following' | 'LocalPublic' }
|
||||
}) => {
|
||||
const initQuery = async ({ instance }: { instance: InstanceLatest }) => {
|
||||
store.dispatch(updateInstanceActive(instance))
|
||||
await queryClient.resetQueries()
|
||||
|
||||
// if (prefetch?.enabled && instance.timelinesLookback) {
|
||||
// if (
|
||||
// prefetch.page &&
|
||||
// instance.timelinesLookback[prefetch.page]?.ids?.length > 0
|
||||
// ) {
|
||||
// await prefetchTimelineQuery(instance.timelinesLookback[prefetch.page])
|
||||
// }
|
||||
|
||||
// for (const page of Object.keys(instance.timelinesLookback)) {
|
||||
// if (page !== prefetch.page) {
|
||||
// prefetchTimelineQuery(instance.timelinesLookback[page])
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
export default initQuery
|
||||
|
|
|
@ -6,6 +6,7 @@ import { InstanceV7 } from './v7'
|
|||
import { InstanceV8 } from './v8'
|
||||
import { InstanceV9 } from './v9'
|
||||
import { InstanceV10 } from './v10'
|
||||
import { InstanceV11 } from './v11'
|
||||
|
||||
const instancesMigration = {
|
||||
4: (state: InstanceV3): InstanceV4 => {
|
||||
|
@ -128,9 +129,34 @@ const instancesMigration = {
|
|||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
11: (state: { instances: InstanceV10[] }): { instances: InstanceV11[] } => {
|
||||
return {
|
||||
instances: state.instances.map(instance => {
|
||||
return {
|
||||
...instance,
|
||||
push: {
|
||||
...instance.push,
|
||||
global: instance.push.global.value,
|
||||
decode: instance.push.decode.value,
|
||||
alerts: {
|
||||
follow: instance.push.alerts.follow.value,
|
||||
follow_request: instance.push.alerts.follow_request.value,
|
||||
favourite: instance.push.alerts.favourite.value,
|
||||
reblog: instance.push.alerts.reblog.value,
|
||||
mention: instance.push.alerts.mention.value,
|
||||
poll: instance.push.alerts.poll.value,
|
||||
status: instance.push.alerts.status.value,
|
||||
'admin.sign_up': false,
|
||||
'admin.report': false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { InstanceV10 as InstanceLatest }
|
||||
export { InstanceV11 as InstanceLatest }
|
||||
|
||||
export default instancesMigration
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
import { ComposeStateDraft } from '@screens/Compose/utils/types'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
|
||||
export type InstanceV11 = {
|
||||
active: boolean
|
||||
appData: {
|
||||
clientId: string
|
||||
clientSecret: string
|
||||
}
|
||||
url: string
|
||||
token: string
|
||||
uri: Mastodon.Instance['uri']
|
||||
urls: Mastodon.Instance['urls']
|
||||
account: {
|
||||
id: Mastodon.Account['id']
|
||||
acct: Mastodon.Account['acct']
|
||||
avatarStatic: Mastodon.Account['avatar_static']
|
||||
preferences: Mastodon.Preferences
|
||||
}
|
||||
version: string
|
||||
configuration?: Mastodon.Instance['configuration']
|
||||
filters: Mastodon.Filter[]
|
||||
notifications_filter: {
|
||||
follow: boolean
|
||||
follow_request: boolean
|
||||
favourite: boolean
|
||||
reblog: boolean
|
||||
mention: boolean
|
||||
poll: boolean
|
||||
status: boolean
|
||||
update: boolean
|
||||
}
|
||||
push: {
|
||||
global: boolean
|
||||
decode: boolean
|
||||
alerts: Mastodon.PushSubscription['alerts']
|
||||
keys: {
|
||||
auth?: string
|
||||
public?: string // legacy
|
||||
private?: string // legacy
|
||||
}
|
||||
}
|
||||
timelinesLookback?: {
|
||||
[key: string]: {
|
||||
queryKey: QueryKeyTimeline
|
||||
ids: Mastodon.Status['id'][]
|
||||
}
|
||||
}
|
||||
mePage: {
|
||||
lists: { shown: boolean }
|
||||
announcements: { shown: boolean; unread: number }
|
||||
}
|
||||
drafts: ComposeStateDraft[]
|
||||
frequentEmojis: {
|
||||
emoji: Pick<Mastodon.Emoji, 'shortcode' | 'url' | 'static_url'>
|
||||
score: number
|
||||
count: number
|
||||
lastUsed: number
|
||||
}[]
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import apiGeneral from '@api/general'
|
||||
import { handleError } from '@api/helpers'
|
||||
import apiTooot from '@api/tooot'
|
||||
import { displayMessage } from '@components/Message'
|
||||
import navigationRef from '@helpers/navigationRef'
|
||||
|
@ -6,7 +7,6 @@ import { useAppDispatch } from '@root/store'
|
|||
import * as Sentry from '@sentry/react-native'
|
||||
import { getExpoToken, retrieveExpoToken } from '@utils/slices/appSlice'
|
||||
import { disableAllPushes, getInstances } from '@utils/slices/instancesSlice'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import * as Notifications from 'expo-notifications'
|
||||
import { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
@ -15,7 +15,6 @@ import { useSelector } from 'react-redux'
|
|||
|
||||
const pushUseConnect = () => {
|
||||
const { t } = useTranslation('screens')
|
||||
const { theme } = useTheme()
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
useEffect(() => {
|
||||
|
@ -24,7 +23,7 @@ const pushUseConnect = () => {
|
|||
|
||||
const expoToken = useSelector(getExpoToken)
|
||||
const instances = useSelector(getInstances, (prev, next) => prev.length === next.length)
|
||||
const pushEnabled = instances.filter(instance => instance.push.global.value)
|
||||
const pushEnabled = instances.filter(instance => instance.push.global)
|
||||
|
||||
const connect = () => {
|
||||
apiTooot({
|
||||
|
@ -33,16 +32,12 @@ const pushUseConnect = () => {
|
|||
})
|
||||
.then(() => Notifications.setBadgeCountAsync(0))
|
||||
.catch(error => {
|
||||
Sentry.setContext('Error response', {
|
||||
...(error?.response && { response: error.response?._response })
|
||||
})
|
||||
Sentry.setContext('Error object', { error })
|
||||
Sentry.captureMessage('Push connect error')
|
||||
handleError({ message: 'Push connect error', captureResponse: true })
|
||||
|
||||
Notifications.setBadgeCountAsync(0)
|
||||
if (error?.status == 404) {
|
||||
displayMessage({
|
||||
theme,
|
||||
type: 'error',
|
||||
type: 'danger',
|
||||
duration: 'long',
|
||||
message: t('pushError.message'),
|
||||
description: t('pushError.description'),
|
||||
|
@ -65,7 +60,7 @@ const pushUseConnect = () => {
|
|||
dispatch(disableAllPushes())
|
||||
|
||||
instances.forEach(instance => {
|
||||
if (instance.push.global.value) {
|
||||
if (instance.push.global) {
|
||||
apiGeneral<{}>({
|
||||
method: 'delete',
|
||||
domain: instance.url,
|
||||
|
|
|
@ -31,10 +31,7 @@ const pushUseReceive = () => {
|
|||
description: notification.request.content.body!,
|
||||
onPress: () => {
|
||||
if (notificationIndex !== -1) {
|
||||
initQuery({
|
||||
instance: instances[notificationIndex],
|
||||
prefetch: { enabled: true }
|
||||
})
|
||||
initQuery({ instance: instances[notificationIndex] })
|
||||
}
|
||||
pushUseNavigate(payloadData.notification_id)
|
||||
}
|
||||
|
|
|
@ -27,10 +27,7 @@ const pushUseRespond = () => {
|
|||
instance.account.id === payloadData.accountId
|
||||
)
|
||||
if (notificationIndex !== -1) {
|
||||
initQuery({
|
||||
instance: instances[notificationIndex],
|
||||
prefetch: { enabled: true }
|
||||
})
|
||||
initQuery({ instance: instances[notificationIndex] })
|
||||
}
|
||||
pushUseNavigate(payloadData.notification_id)
|
||||
}
|
||||
|
|
|
@ -1,18 +1,41 @@
|
|||
import apiGeneral from '@api/general'
|
||||
import apiInstance from '@api/instance'
|
||||
import { AxiosError } from 'axios'
|
||||
import * as AuthSession from 'expo-auth-session'
|
||||
import { QueryFunctionContext, useQuery, UseQueryOptions } from 'react-query'
|
||||
import {
|
||||
QueryFunctionContext,
|
||||
useMutation,
|
||||
UseMutationOptions,
|
||||
useQuery,
|
||||
UseQueryOptions
|
||||
} from 'react-query'
|
||||
|
||||
export type QueryKeyApps = ['Apps', { domain?: string }]
|
||||
export type QueryKeyApps = ['Apps']
|
||||
|
||||
const queryFunction = ({ queryKey }: QueryFunctionContext<QueryKeyApps>) => {
|
||||
const redirectUri = AuthSession.makeRedirectUri({
|
||||
native: 'tooot://instance-auth',
|
||||
useProxy: false
|
||||
const queryFunctionApps = async ({ queryKey }: QueryFunctionContext<QueryKeyApps>) => {
|
||||
const res = await apiInstance<Mastodon.Apps>({
|
||||
method: 'get',
|
||||
url: 'apps/verify_credentials'
|
||||
})
|
||||
return res.body
|
||||
}
|
||||
|
||||
const { domain } = queryKey[1]
|
||||
const useAppsQuery = (
|
||||
params: {
|
||||
options?: UseQueryOptions<Mastodon.Apps, AxiosError>
|
||||
} | void
|
||||
) => {
|
||||
const queryKey: QueryKeyApps = ['Apps']
|
||||
return useQuery(queryKey, queryFunctionApps, params?.options)
|
||||
}
|
||||
|
||||
type MutationVarsApps = { domain: string }
|
||||
|
||||
export const redirectUri = AuthSession.makeRedirectUri({
|
||||
native: 'tooot://instance-auth',
|
||||
useProxy: false
|
||||
})
|
||||
const mutationFunctionApps = async ({ domain }: MutationVarsApps) => {
|
||||
const formData = new FormData()
|
||||
formData.append('client_name', 'tooot')
|
||||
formData.append('website', 'https://tooot.app')
|
||||
|
@ -21,20 +44,16 @@ const queryFunction = ({ queryKey }: QueryFunctionContext<QueryKeyApps>) => {
|
|||
|
||||
return apiGeneral<Mastodon.Apps>({
|
||||
method: 'post',
|
||||
domain: domain || '',
|
||||
domain: domain,
|
||||
url: `api/v1/apps`,
|
||||
body: formData
|
||||
}).then(res => res.body)
|
||||
}
|
||||
|
||||
const useAppsQuery = ({
|
||||
options,
|
||||
...queryKeyParams
|
||||
}: QueryKeyApps[1] & {
|
||||
options?: UseQueryOptions<Mastodon.Apps, AxiosError>
|
||||
}) => {
|
||||
const queryKey: QueryKeyApps = ['Apps', { ...queryKeyParams }]
|
||||
return useQuery(queryKey, queryFunction, options)
|
||||
const useAppsMutation = (
|
||||
options: UseMutationOptions<Mastodon.Apps, AxiosError, MutationVarsApps>
|
||||
) => {
|
||||
return useMutation(mutationFunctionApps, options)
|
||||
}
|
||||
|
||||
export { useAppsQuery }
|
||||
export { useAppsQuery, useAppsMutation }
|
||||
|
|
|
@ -2,20 +2,18 @@ import apiInstance from '@api/instance'
|
|||
import haptics from '@components/haptics'
|
||||
import { displayMessage } from '@components/Message'
|
||||
import queryClient from '@helpers/queryClient'
|
||||
import { Theme } from '@utils/styles/themes'
|
||||
import { AxiosError } from 'axios'
|
||||
import i18next from 'i18next'
|
||||
import { RefObject } from 'react'
|
||||
import FlashMessage from 'react-native-flash-message'
|
||||
import { useMutation, useQuery, UseQueryOptions } from 'react-query'
|
||||
|
||||
type AccountWithSource = Mastodon.Account &
|
||||
Required<Pick<Mastodon.Account, 'source'>>
|
||||
type AccountWithSource = Mastodon.Account & Required<Pick<Mastodon.Account, 'source'>>
|
||||
|
||||
type QueryKeyProfile = ['Profile']
|
||||
export type QueryKeyProfile = ['Profile']
|
||||
const queryKey: QueryKeyProfile = ['Profile']
|
||||
|
||||
const queryFunction = async () => {
|
||||
const queryFunctionProfile = async () => {
|
||||
const res = await apiInstance<AccountWithSource>({
|
||||
method: 'get',
|
||||
url: `accounts/verify_credentials`
|
||||
|
@ -23,12 +21,12 @@ const queryFunction = async () => {
|
|||
return res.body
|
||||
}
|
||||
|
||||
const useProfileQuery = ({
|
||||
options
|
||||
}: {
|
||||
options?: UseQueryOptions<AccountWithSource, AxiosError>
|
||||
}) => {
|
||||
return useQuery(queryKey, queryFunction, options)
|
||||
const useProfileQuery = (
|
||||
params: {
|
||||
options: UseQueryOptions<AccountWithSource, AxiosError>
|
||||
} | void
|
||||
) => {
|
||||
return useQuery(queryKey, queryFunctionProfile, params?.options)
|
||||
}
|
||||
|
||||
type MutationVarsProfileBase =
|
||||
|
@ -52,7 +50,6 @@ type MutationVarsProfileBase =
|
|||
}
|
||||
|
||||
type MutationVarsProfile = MutationVarsProfileBase & {
|
||||
theme: Theme
|
||||
messageRef: RefObject<FlashMessage>
|
||||
message: {
|
||||
text: string
|
||||
|
@ -92,73 +89,70 @@ const mutationFunction = async ({ type, data }: MutationVarsProfile) => {
|
|||
}
|
||||
|
||||
const useProfileMutation = () => {
|
||||
return useMutation<
|
||||
{ body: AccountWithSource },
|
||||
AxiosError,
|
||||
MutationVarsProfile
|
||||
>(mutationFunction, {
|
||||
onMutate: async variables => {
|
||||
await queryClient.cancelQueries(queryKey)
|
||||
return useMutation<{ body: AccountWithSource }, AxiosError, MutationVarsProfile>(
|
||||
mutationFunction,
|
||||
{
|
||||
onMutate: async variables => {
|
||||
await queryClient.cancelQueries(queryKey)
|
||||
|
||||
const oldData = queryClient.getQueryData<AccountWithSource>(queryKey)
|
||||
const oldData = queryClient.getQueryData<AccountWithSource>(queryKey)
|
||||
|
||||
queryClient.setQueryData<AccountWithSource | undefined>(queryKey, old => {
|
||||
if (old) {
|
||||
switch (variables.type) {
|
||||
case 'source[privacy]':
|
||||
return {
|
||||
...old,
|
||||
source: { ...old.source, privacy: variables.data }
|
||||
}
|
||||
case 'source[sensitive]':
|
||||
return {
|
||||
...old,
|
||||
source: { ...old.source, sensitive: variables.data }
|
||||
}
|
||||
case 'locked':
|
||||
return { ...old, locked: variables.data }
|
||||
case 'bot':
|
||||
return { ...old, bot: variables.data }
|
||||
default:
|
||||
return old
|
||||
queryClient.setQueryData<AccountWithSource | undefined>(queryKey, old => {
|
||||
if (old) {
|
||||
switch (variables.type) {
|
||||
case 'source[privacy]':
|
||||
return {
|
||||
...old,
|
||||
source: { ...old.source, privacy: variables.data }
|
||||
}
|
||||
case 'source[sensitive]':
|
||||
return {
|
||||
...old,
|
||||
source: { ...old.source, sensitive: variables.data }
|
||||
}
|
||||
case 'locked':
|
||||
return { ...old, locked: variables.data }
|
||||
case 'bot':
|
||||
return { ...old, bot: variables.data }
|
||||
default:
|
||||
return old
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return oldData
|
||||
},
|
||||
onError: (err, variables, context) => {
|
||||
queryClient.setQueryData(queryKey, context)
|
||||
haptics('Error')
|
||||
if (variables.message.failed) {
|
||||
displayMessage({
|
||||
ref: variables.messageRef,
|
||||
message: i18next.t('screenTabs:me.profile.feedback.failed', {
|
||||
type: i18next.t(`screenTabs:${variables.message.text}`)
|
||||
}),
|
||||
...(err && { description: err.message }),
|
||||
theme: variables.theme,
|
||||
type: 'error'
|
||||
})
|
||||
return oldData
|
||||
},
|
||||
onError: (err, variables, context) => {
|
||||
queryClient.setQueryData(queryKey, context)
|
||||
haptics('Error')
|
||||
if (variables.message.failed) {
|
||||
displayMessage({
|
||||
ref: variables.messageRef,
|
||||
message: i18next.t('screenTabs:me.profile.feedback.failed', {
|
||||
type: i18next.t(`screenTabs:${variables.message.text}`)
|
||||
}),
|
||||
...(err && { description: err.message }),
|
||||
type: 'danger'
|
||||
})
|
||||
}
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
if (variables.message.succeed) {
|
||||
haptics('Success')
|
||||
displayMessage({
|
||||
ref: variables.messageRef,
|
||||
message: i18next.t('screenTabs:me.profile.feedback.succeed', {
|
||||
type: i18next.t(`screenTabs:${variables.message.text}`)
|
||||
}),
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries(queryKey)
|
||||
}
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
if (variables.message.succeed) {
|
||||
haptics('Success')
|
||||
displayMessage({
|
||||
ref: variables.messageRef,
|
||||
message: i18next.t('screenTabs:me.profile.feedback.succeed', {
|
||||
type: i18next.t(`screenTabs:${variables.message.text}`)
|
||||
}),
|
||||
theme: variables.theme,
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries(queryKey)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
export { useProfileQuery, useProfileMutation }
|
||||
export { queryFunctionProfile, useProfileQuery, useProfileMutation }
|
||||
|
|
|
@ -90,16 +90,18 @@ const addInstance = createAsyncThunk(
|
|||
update: true
|
||||
},
|
||||
push: {
|
||||
global: { loading: false, value: false },
|
||||
decode: { loading: false, value: false },
|
||||
global: false,
|
||||
decode: false,
|
||||
alerts: {
|
||||
follow: { loading: false, value: true },
|
||||
follow_request: { loading: false, value: true },
|
||||
favourite: { loading: false, value: true },
|
||||
reblog: { loading: false, value: true },
|
||||
mention: { loading: false, value: true },
|
||||
poll: { loading: false, value: true },
|
||||
status: { loading: false, value: true }
|
||||
follow: true,
|
||||
follow_request: true,
|
||||
favourite: true,
|
||||
reblog: true,
|
||||
mention: true,
|
||||
poll: true,
|
||||
status: true,
|
||||
'admin.sign_up': false,
|
||||
'admin.report': false
|
||||
},
|
||||
keys: { auth: undefined, public: undefined, private: undefined }
|
||||
},
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
import * as Notifications from 'expo-notifications'
|
||||
|
||||
const androidDefaults = {
|
||||
importance: Notifications.AndroidImportance.DEFAULT,
|
||||
bypassDnd: false,
|
||||
showBadge: true,
|
||||
enableLights: true,
|
||||
enableVibrate: true
|
||||
}
|
||||
|
||||
export default androidDefaults
|
|
@ -1,15 +1,15 @@
|
|||
import apiInstance from '@api/instance'
|
||||
import apiTooot, { TOOOT_API_DOMAIN } from '@api/tooot'
|
||||
import i18n from '@root/i18n/i18n'
|
||||
import { displayMessage } from '@components/Message'
|
||||
import { RootState } from '@root/store'
|
||||
import * as Sentry from '@sentry/react-native'
|
||||
import { InstanceLatest } from '@utils/migrations/instances/migration'
|
||||
import { getInstance } from '@utils/slices/instancesSlice'
|
||||
import * as Notifications from 'expo-notifications'
|
||||
import * as Random from 'expo-random'
|
||||
import i18next from 'i18next'
|
||||
import { Platform } from 'react-native'
|
||||
import base64 from 'react-native-base64'
|
||||
import androidDefaults from './androidDefaults'
|
||||
import { setChannels } from './utils'
|
||||
|
||||
const subscribe = async ({
|
||||
expoToken,
|
||||
|
@ -74,6 +74,12 @@ const pushRegister = async (
|
|||
})
|
||||
|
||||
if (!res.body.server_key?.length) {
|
||||
displayMessage({
|
||||
type: 'danger',
|
||||
duration: 'long',
|
||||
message: i18next.t('screenTabs:me.push.missingServerKey.message'),
|
||||
description: i18next.t('screenTabs:me.push.missingServerKey.description')
|
||||
})
|
||||
Sentry.setContext('Push server key', {
|
||||
instance: instanceUri,
|
||||
resBody: res.body
|
||||
|
@ -88,50 +94,11 @@ const pushRegister = async (
|
|||
accountId,
|
||||
accountFull,
|
||||
serverKey: res.body.server_key,
|
||||
auth: instancePush.decode.value === false ? null : auth
|
||||
auth: instancePush.decode === false ? null : auth
|
||||
})
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
Notifications.setNotificationChannelGroupAsync(accountFull, {
|
||||
name: accountFull,
|
||||
...androidDefaults
|
||||
}).then(group => {
|
||||
if (group) {
|
||||
if (instancePush.decode.value === false) {
|
||||
Notifications.setNotificationChannelAsync(`${group.id}_default`, {
|
||||
groupId: group.id,
|
||||
name: i18n.t('meSettingsPush:content.default.heading'),
|
||||
...androidDefaults
|
||||
})
|
||||
} else {
|
||||
Notifications.setNotificationChannelAsync(`${group.id}_follow`, {
|
||||
groupId: group.id,
|
||||
name: i18n.t('meSettingsPush:content.follow.heading'),
|
||||
...androidDefaults
|
||||
})
|
||||
Notifications.setNotificationChannelAsync(`${group.id}_favourite`, {
|
||||
groupId: group.id,
|
||||
name: i18n.t('meSettingsPush:content.favourite.heading'),
|
||||
...androidDefaults
|
||||
})
|
||||
Notifications.setNotificationChannelAsync(`${group.id}_reblog`, {
|
||||
groupId: group.id,
|
||||
name: i18n.t('meSettingsPush:content.reblog.heading'),
|
||||
...androidDefaults
|
||||
})
|
||||
Notifications.setNotificationChannelAsync(`${group.id}_mention`, {
|
||||
groupId: group.id,
|
||||
name: i18n.t('meSettingsPush:content.mention.heading'),
|
||||
...androidDefaults
|
||||
})
|
||||
Notifications.setNotificationChannelAsync(`${group.id}_poll`, {
|
||||
groupId: group.id,
|
||||
name: i18n.t('meSettingsPush:content.poll.heading'),
|
||||
...androidDefaults
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
setChannels(instance)
|
||||
}
|
||||
|
||||
return Promise.resolve(auth)
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
import { PERMISSION_MANAGE_REPORTS, PERMISSION_MANAGE_USERS } from '@helpers/permissions'
|
||||
import queryClient from '@helpers/queryClient'
|
||||
import i18n from '@root/i18n/i18n'
|
||||
import { InstanceLatest } from '@utils/migrations/instances/migration'
|
||||
import { queryFunctionProfile, QueryKeyProfile } from '@utils/queryHooks/profile'
|
||||
import * as Notifications from 'expo-notifications'
|
||||
|
||||
export const PUSH_DEFAULT: [
|
||||
'follow',
|
||||
'follow_request',
|
||||
'favourite',
|
||||
'reblog',
|
||||
'mention',
|
||||
'poll',
|
||||
'status'
|
||||
] = ['follow', 'follow_request', 'favourite', 'reblog', 'mention', 'poll', 'status']
|
||||
|
||||
export const PUSH_ADMIN: { type: 'admin.sign_up' | 'admin.report'; permission: number }[] = [
|
||||
{ type: 'admin.sign_up', permission: PERMISSION_MANAGE_USERS },
|
||||
{ type: 'admin.report', permission: PERMISSION_MANAGE_REPORTS }
|
||||
]
|
||||
|
||||
export const checkPushAdminPermission = (
|
||||
permission: number,
|
||||
permissions?: string | number
|
||||
): boolean =>
|
||||
permissions
|
||||
? !!(
|
||||
(typeof permissions === 'string' ? parseInt(permissions || '0') : permissions) & permission
|
||||
)
|
||||
: false
|
||||
|
||||
export const setChannels = async (instance: InstanceLatest) => {
|
||||
const account = `@${instance.account.acct}@${instance.uri}`
|
||||
|
||||
const deleteChannel = async (type: string) =>
|
||||
Notifications.deleteNotificationChannelAsync(`${account}_${type}`)
|
||||
const setChannel = async (type: string) =>
|
||||
Notifications.setNotificationChannelAsync(`${account}_${type}`, {
|
||||
groupId: account,
|
||||
name: i18n.t(`screenTabs:me.push.${type}.heading`),
|
||||
importance: Notifications.AndroidImportance.DEFAULT,
|
||||
bypassDnd: false,
|
||||
showBadge: true,
|
||||
enableLights: true,
|
||||
enableVibrate: true
|
||||
})
|
||||
|
||||
const queryKey: QueryKeyProfile = ['Profile']
|
||||
const profileQuery = await queryClient.fetchQuery(queryKey, queryFunctionProfile)
|
||||
|
||||
const channelGroup = await Notifications.getNotificationChannelGroupAsync(account)
|
||||
if (!channelGroup) {
|
||||
await Notifications.setNotificationChannelGroupAsync(account, { name: account })
|
||||
}
|
||||
|
||||
if (!instance.push.decode) {
|
||||
await setChannel('default')
|
||||
for (const push of PUSH_DEFAULT) {
|
||||
await deleteChannel(push)
|
||||
}
|
||||
for (const { type } of PUSH_ADMIN) {
|
||||
await deleteChannel(type)
|
||||
}
|
||||
} else {
|
||||
await deleteChannel('default')
|
||||
for (const push of PUSH_DEFAULT) {
|
||||
await setChannel(push)
|
||||
}
|
||||
for (const { type, permission } of PUSH_ADMIN) {
|
||||
if (checkPushAdminPermission(permission, profileQuery.role?.permissions)) {
|
||||
await setChannel(type)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@ import { updateInstancePush } from './updatePush'
|
|||
const removeInstance = createAsyncThunk(
|
||||
'instances/remove',
|
||||
async (instance: InstanceLatest, { dispatch }): Promise<InstanceLatest> => {
|
||||
if (instance.push.global.value) {
|
||||
if (instance.push.global) {
|
||||
dispatch(updateInstancePush(false))
|
||||
}
|
||||
|
||||
|
|
|
@ -1,19 +1,17 @@
|
|||
import apiTooot from '@api/tooot'
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit'
|
||||
import i18n from '@root/i18n/i18n'
|
||||
import { RootState } from '@root/store'
|
||||
import { InstanceLatest } from '@utils/migrations/instances/migration'
|
||||
import * as Notifications from 'expo-notifications'
|
||||
import { Platform } from 'react-native'
|
||||
import { getInstance } from '../instancesSlice'
|
||||
import androidDefaults from './push/androidDefaults'
|
||||
import { setChannels } from './push/utils'
|
||||
|
||||
export const updateInstancePushDecode = createAsyncThunk(
|
||||
'instances/updatePushDecode',
|
||||
async (
|
||||
disable: boolean,
|
||||
{ getState }
|
||||
): Promise<{ disable: InstanceLatest['push']['decode']['value'] }> => {
|
||||
): Promise<{ disable: InstanceLatest['push']['decode'] }> => {
|
||||
const state = getState() as RootState
|
||||
const instance = getInstance(state)
|
||||
if (!instance?.url || !instance.account.id || !instance.push.keys) {
|
||||
|
@ -34,54 +32,7 @@ export const updateInstancePushDecode = createAsyncThunk(
|
|||
})
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
const accountFull = `@${instance.account.acct}@${instance.uri}`
|
||||
switch (disable) {
|
||||
case true:
|
||||
Notifications.deleteNotificationChannelAsync(`${accountFull}_default`)
|
||||
Notifications.setNotificationChannelAsync(`${accountFull}_follow`, {
|
||||
groupId: accountFull,
|
||||
name: i18n.t('meSettingsPush:content.follow.heading'),
|
||||
...androidDefaults
|
||||
})
|
||||
Notifications.setNotificationChannelAsync(
|
||||
`${accountFull}_favourite`,
|
||||
{
|
||||
groupId: accountFull,
|
||||
name: i18n.t('meSettingsPush:content.favourite.heading'),
|
||||
...androidDefaults
|
||||
}
|
||||
)
|
||||
Notifications.setNotificationChannelAsync(`${accountFull}_reblog`, {
|
||||
groupId: accountFull,
|
||||
name: i18n.t('meSettingsPush:content.reblog.heading'),
|
||||
...androidDefaults
|
||||
})
|
||||
Notifications.setNotificationChannelAsync(`${accountFull}_mention`, {
|
||||
groupId: accountFull,
|
||||
name: i18n.t('meSettingsPush:content.mention.heading'),
|
||||
...androidDefaults
|
||||
})
|
||||
Notifications.setNotificationChannelAsync(`${accountFull}_poll`, {
|
||||
groupId: accountFull,
|
||||
name: i18n.t('meSettingsPush:content.poll.heading'),
|
||||
...androidDefaults
|
||||
})
|
||||
break
|
||||
case false:
|
||||
Notifications.setNotificationChannelAsync(`${accountFull}_default`, {
|
||||
groupId: accountFull,
|
||||
name: i18n.t('meSettingsPush:content.default.heading'),
|
||||
...androidDefaults
|
||||
})
|
||||
Notifications.deleteNotificationChannelAsync(`${accountFull}_follow`)
|
||||
Notifications.deleteNotificationChannelAsync(
|
||||
`${accountFull}_favourite`
|
||||
)
|
||||
Notifications.deleteNotificationChannelAsync(`${accountFull}_reblog`)
|
||||
Notifications.deleteNotificationChannelAsync(`${accountFull}_mention`)
|
||||
Notifications.deleteNotificationChannelAsync(`${accountFull}_poll`)
|
||||
break
|
||||
}
|
||||
setChannels(instance)
|
||||
}
|
||||
|
||||
return Promise.resolve({ disable })
|
||||
|
|
|
@ -73,18 +73,11 @@ const instancesSlice = createSlice({
|
|||
},
|
||||
clearPushLoading: ({ instances }) => {
|
||||
const activeIndex = findInstanceActive(instances)
|
||||
instances[activeIndex].push.global.loading = false
|
||||
instances[activeIndex].push.decode.loading = false
|
||||
instances[activeIndex].push.alerts.favourite.loading = false
|
||||
instances[activeIndex].push.alerts.follow.loading = false
|
||||
instances[activeIndex].push.alerts.mention.loading = false
|
||||
instances[activeIndex].push.alerts.poll.loading = false
|
||||
instances[activeIndex].push.alerts.reblog.loading = false
|
||||
},
|
||||
disableAllPushes: ({ instances }) => {
|
||||
instances = instances.map(instance => {
|
||||
let newInstance = instance
|
||||
newInstance.push.global.value = false
|
||||
newInstance.push.global = false
|
||||
return newInstance
|
||||
})
|
||||
},
|
||||
|
@ -238,48 +231,21 @@ const instancesSlice = createSlice({
|
|||
// Update Instance Push Global
|
||||
.addCase(updateInstancePush.fulfilled, (state, action) => {
|
||||
const activeIndex = findInstanceActive(state.instances)
|
||||
state.instances[activeIndex].push.global.loading = false
|
||||
state.instances[activeIndex].push.global.value = action.meta.arg
|
||||
state.instances[activeIndex].push.global = action.meta.arg
|
||||
state.instances[activeIndex].push.keys = { auth: action.payload }
|
||||
})
|
||||
.addCase(updateInstancePush.rejected, state => {
|
||||
const activeIndex = findInstanceActive(state.instances)
|
||||
state.instances[activeIndex].push.global.loading = false
|
||||
})
|
||||
.addCase(updateInstancePush.pending, state => {
|
||||
const activeIndex = findInstanceActive(state.instances)
|
||||
state.instances[activeIndex].push.global.loading = true
|
||||
})
|
||||
|
||||
// Update Instance Push Decode
|
||||
.addCase(updateInstancePushDecode.fulfilled, (state, action) => {
|
||||
const activeIndex = findInstanceActive(state.instances)
|
||||
state.instances[activeIndex].push.decode.loading = false
|
||||
state.instances[activeIndex].push.decode.value = action.payload.disable
|
||||
})
|
||||
.addCase(updateInstancePushDecode.rejected, state => {
|
||||
const activeIndex = findInstanceActive(state.instances)
|
||||
state.instances[activeIndex].push.decode.loading = false
|
||||
})
|
||||
.addCase(updateInstancePushDecode.pending, state => {
|
||||
const activeIndex = findInstanceActive(state.instances)
|
||||
state.instances[activeIndex].push.decode.loading = true
|
||||
state.instances[activeIndex].push.decode = action.payload.disable
|
||||
})
|
||||
|
||||
// Update Instance Push Individual Alert
|
||||
.addCase(updateInstancePushAlert.fulfilled, (state, action) => {
|
||||
const activeIndex = findInstanceActive(state.instances)
|
||||
state.instances[activeIndex].push.alerts[action.meta.arg.changed].loading = false
|
||||
state.instances[activeIndex].push.alerts = action.payload
|
||||
})
|
||||
.addCase(updateInstancePushAlert.rejected, (state, action) => {
|
||||
const activeIndex = findInstanceActive(state.instances)
|
||||
state.instances[activeIndex].push.alerts[action.meta.arg.changed].loading = false
|
||||
})
|
||||
.addCase(updateInstancePushAlert.pending, (state, action) => {
|
||||
const activeIndex = findInstanceActive(state.instances)
|
||||
state.instances[activeIndex].push.alerts[action.meta.arg.changed].loading = true
|
||||
})
|
||||
|
||||
// Check if frequently used emojis still exist
|
||||
.addCase(checkEmojis.fulfilled, (state, action) => {
|
||||
|
@ -412,7 +378,6 @@ export const {
|
|||
updateInstanceNotificationsFilter,
|
||||
updateInstanceDraft,
|
||||
removeInstanceDraft,
|
||||
clearPushLoading,
|
||||
disableAllPushes,
|
||||
updateInstanceTimelineLookback,
|
||||
updateInstanceMePage,
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import { LayoutAnimation } from 'react-native'
|
||||
import { AccessibilityInfo, LayoutAnimation } from 'react-native'
|
||||
|
||||
const layoutAnimation = async () => {
|
||||
const disable = await AccessibilityInfo.isReduceMotionEnabled()
|
||||
if (disable) return
|
||||
|
||||
const layoutAnimation = () =>
|
||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
|
||||
}
|
||||
|
||||
export default layoutAnimation
|
||||
|
|
|
@ -10853,10 +10853,10 @@ react-native-iphone-x-helper@^1.3.1:
|
|||
resolved "https://registry.yarnpkg.com/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz#20c603e9a0e765fd6f97396638bdeb0e5a60b010"
|
||||
integrity sha512-HOf0jzRnq2/aFUcdCJ9w9JGzN3gdEg0zFE4FyYlp4jtidqU03D5X7ZegGKfT1EWteR0gPBGp9ye5T5FvSWi9Yg==
|
||||
|
||||
react-native-language-detection@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native-language-detection/-/react-native-language-detection-0.1.0.tgz#06b5d20bffb60dbbd599c8e62b6acf500952afa8"
|
||||
integrity sha512-26CLndVMmMbVp40Y9Herza73nfR08JFTcYkJ3MX5MIQbGRoqgNAG89z8pA1y7dPHHK1Nfa6AWKAYpNv7tMRCaw==
|
||||
react-native-language-detection@^0.2.2:
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/react-native-language-detection/-/react-native-language-detection-0.2.2.tgz#4cc94177aa1c4575c4656f6d42456fa6c72ed5db"
|
||||
integrity sha512-6u1JBgr+UG/GX/xMmT4K8CaBlSep4XfM91jwUzRA/Y3bMCHDx7bNVxGQvqvzkmOchby9h66XD8F5Eo+kV01CAA==
|
||||
|
||||
react-native-live-text-image-view@^0.4.0:
|
||||
version "0.4.0"
|
||||
|
|
Loading…
Reference in New Issue