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