1
0
mirror of https://github.com/tooot-app/app synced 2025-06-05 22:19:13 +02:00

Merge pull request #508 from tooot-app/main

Test audio playback
This commit is contained in:
xmflsct
2022-12-04 14:53:30 +01:00
committed by GitHub
48 changed files with 339 additions and 231 deletions

View File

@ -0,0 +1,14 @@
diff --git a/node_modules/expo-av/ios/EXAV/EXAudioSessionManager.m b/node_modules/expo-av/ios/EXAV/EXAudioSessionManager.m
index 81dce13..8664b90 100644
--- a/node_modules/expo-av/ios/EXAV/EXAudioSessionManager.m
+++ b/node_modules/expo-av/ios/EXAV/EXAudioSessionManager.m
@@ -168,9 +168,6 @@ - (void)moduleDidBackground:(id)backgroundingModule
// compact doesn't work, that's why we need the `|| !pointer` above
// http://www.openradar.me/15396578
[_foregroundedModules compact];
-
- // Any possible failures are silent
- [self _updateSessionConfiguration];
}
- (void)moduleDidForeground:(id)module

View File

@ -1,6 +1,5 @@
import axios from 'axios' import axios from 'axios'
import handleError, { ctx } from './handleError' import { ctx, handleError, userAgent } from './helpers'
import { userAgent } from './helpers'
export type Params = { export type Params = {
method: 'get' | 'post' | 'put' | 'delete' method: 'get' | 'post' | 'put' | 'delete'

View File

@ -1,31 +0,0 @@
import chalk from 'chalk'
export const ctx = new chalk.Instance({ level: 3 })
const handleError = (error: any) => {
if (error?.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
console.error(
ctx.bold(' API '),
ctx.bold('response'),
error.response.status,
error?.response.data?.error || error?.response.message || 'Unknown error'
)
return Promise.reject({
status: error?.response.status,
message: error?.response.data?.error || error?.response.message || 'Unknown error'
})
} else if (error?.request) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
console.error(ctx.bold(' API '), ctx.bold('request'), error)
return Promise.reject()
} else {
console.error(ctx.bold(' API '), ctx.bold('internal'), error?.message)
return Promise.reject()
}
}
export default handleError

View File

@ -1,6 +1,36 @@
import Constants from "expo-constants" import chalk from 'chalk'
import { Platform } from "react-native" import Constants from 'expo-constants'
import { Platform } from 'react-native'
const userAgent = { 'User-Agent': `tooot/${Constants.expoConfig?.version} ${Platform.OS}/${Platform.Version}` } const userAgent = {
'User-Agent': `tooot/${Constants.expoConfig?.version} ${Platform.OS}/${Platform.Version}`
}
export { userAgent } const ctx = new chalk.Instance({ level: 3 })
const handleError = (error: any) => {
if (error?.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
console.error(
ctx.bold(' API '),
ctx.bold('response'),
error.response.status,
error?.response.data?.error || error?.response.message || 'Unknown error'
)
return Promise.reject({
status: error?.response.status,
message: error?.response.data?.error || error?.response.message || 'Unknown error'
})
} else if (error?.request) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
console.error(ctx.bold(' API '), ctx.bold('request'), error)
return Promise.reject()
} else {
console.error(ctx.bold(' API '), ctx.bold('internal'), error?.message)
return Promise.reject()
}
}
export { ctx, handleError, userAgent }

View File

@ -1,8 +1,7 @@
import { RootState } from '@root/store' import { RootState } from '@root/store'
import axios, { AxiosRequestConfig } from 'axios' import axios, { AxiosRequestConfig } from 'axios'
import li from 'li' import li from 'li'
import handleError, { ctx } from './handleError' import { ctx, handleError, userAgent } from './helpers'
import { userAgent } from './helpers'
export type Params = { export type Params = {
method: 'get' | 'post' | 'put' | 'delete' | 'patch' method: 'get' | 'post' | 'put' | 'delete' | 'patch'

View File

@ -1,8 +1,7 @@
import * as Sentry from '@sentry/react-native' import * as Sentry from '@sentry/react-native'
import { mapEnvironment } from '@utils/checkEnvironment' import { mapEnvironment } from '@utils/checkEnvironment'
import axios from 'axios' import axios from 'axios'
import handleError, { ctx } from './handleError' import { ctx, handleError, userAgent } from './helpers'
import { userAgent } from './helpers'
export type Params = { export type Params = {
method: 'get' | 'post' | 'put' | 'delete' method: 'get' | 'post' | 'put' | 'delete'
@ -57,14 +56,12 @@ const apiTooot = async <T = unknown>({
}) })
}) })
.catch(error => { .catch(error => {
Sentry.setExtras({ Sentry.setContext('API request', { url, params, body })
API: 'tooot', Sentry.setContext('Error response', {
request: { url, params, body }, ...(error?.response && { response: error.response?._response })
...(error?.response && { response: error.response })
})
Sentry.captureMessage('API error', {
contexts: { errorObject: error }
}) })
Sentry.setContext('Error object', { error })
Sentry.captureMessage('API error')
return handleError(error) return handleError(error)
}) })

View File

@ -5,22 +5,17 @@ import { TabLocalStackParamList } from '@utils/navigation/navigators'
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 React, { PropsWithChildren } from 'react' import React, { PropsWithChildren } from 'react'
import { Pressable, View } from 'react-native' import { Pressable, PressableProps, View } from 'react-native'
import GracefullyImage from './GracefullyImage' import GracefullyImage from './GracefullyImage'
import Icon from './Icon'
import CustomText from './Text' import CustomText from './Text'
export interface Props { export interface Props {
account: Mastodon.Account account: Mastodon.Account
Component?: typeof View | typeof Pressable props?: PressableProps
props?: {}
} }
const ComponentAccount: React.FC<PropsWithChildren & Props> = ({ const ComponentAccount: React.FC<PropsWithChildren & Props> = ({ account, props, children }) => {
account,
Component,
props,
children
}) => {
const { colors } = useTheme() const { colors } = useTheme()
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>() const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
@ -28,50 +23,62 @@ const ComponentAccount: React.FC<PropsWithChildren & Props> = ({
props = { onPress: () => navigation.push('Tab-Shared-Account', { account }) } props = { onPress: () => navigation.push('Tab-Shared-Account', { account }) }
} }
return React.createElement( return (
Component || Pressable, <Pressable
{ {...props}
...props, style={{
style: {
flex: 1, flex: 1,
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding, paddingHorizontal: StyleConstants.Spacing.Global.PagePadding,
paddingVertical: StyleConstants.Spacing.M, paddingVertical: StyleConstants.Spacing.M,
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center' alignItems: 'center'
}}
children={
<>
<View style={{ flex: 1, flexDirection: 'row', alignItems: 'center' }}>
<GracefullyImage
uri={{ original: account.avatar, static: account.avatar_static }}
style={{
width: StyleConstants.Avatar.S,
height: StyleConstants.Avatar.S,
borderRadius: 6,
marginRight: StyleConstants.Spacing.S
}}
/>
<View>
<CustomText numberOfLines={1}>
<ParseEmojis
content={account.display_name || account.username}
emojis={account.emojis}
size='S'
fontBold
/>
</CustomText>
<CustomText
numberOfLines={1}
style={{
marginTop: StyleConstants.Spacing.XS,
color: colors.secondary
}}
>
@{account.acct}
</CustomText>
</View>
</View>
{props.onPress && !props.disabled ? (
<Icon
name='ChevronRight'
size={StyleConstants.Font.Size.L}
color={colors.secondary}
style={{ marginLeft: 8 }}
/>
) : (
children || null
)}
</>
} }
}, />
<View style={{ flex: 1, flexDirection: 'row', alignItems: 'center' }}>
<GracefullyImage
uri={{ original: account.avatar, static: account.avatar_static }}
style={{
width: StyleConstants.Avatar.S,
height: StyleConstants.Avatar.S,
borderRadius: 6,
marginRight: StyleConstants.Spacing.S
}}
/>
<View>
<CustomText numberOfLines={1}>
<ParseEmojis
content={account.display_name || account.username}
emojis={account.emojis}
size='S'
fontBold
/>
</CustomText>
<CustomText
numberOfLines={1}
style={{
marginTop: StyleConstants.Spacing.XS,
color: colors.secondary
}}
>
@{account.acct}
</CustomText>
</View>
</View>,
children
) )
} }

View File

@ -1,15 +1,8 @@
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import layoutAnimation from '@utils/styles/layoutAnimation'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useEffect, useMemo, useRef, useState } from 'react' import React, { useMemo, useState } from 'react'
import { import { AccessibilityProps, Pressable, StyleProp, View, ViewStyle } from 'react-native'
AccessibilityProps,
Pressable,
StyleProp,
View,
ViewStyle
} from 'react-native'
import { Flow } from 'react-native-animated-spinkit' import { Flow } from 'react-native-animated-spinkit'
import CustomText from './Text' import CustomText from './Text'
@ -57,15 +50,6 @@ const Button: React.FC<Props> = ({
}) => { }) => {
const { colors, theme } = useTheme() const { colors, theme } = useTheme()
const mounted = useRef(false)
useEffect(() => {
if (mounted.current) {
layoutAnimation()
} else {
mounted.current = true
}
}, [content, loading, disabled])
const loadingSpinkit = useMemo( const loadingSpinkit = useMemo(
() => ( () => (
<View style={{ position: 'absolute' }}> <View style={{ position: 'absolute' }}>
@ -120,8 +104,7 @@ const Button: React.FC<Props> = ({
<CustomText <CustomText
style={{ style={{
color: mainColor, color: mainColor,
fontSize: fontSize: StyleConstants.Font.Size[size] * (size === 'L' ? 1.25 : 1),
StyleConstants.Font.Size[size] * (size === 'L' ? 1.25 : 1),
opacity: loading ? 0 : 1 opacity: loading ? 0 : 1
}} }}
fontWeight={fontBold ? 'Bold' : 'Normal'} fontWeight={fontBold ? 'Bold' : 'Normal'}
@ -156,15 +139,13 @@ const Button: React.FC<Props> = ({
borderColor: mainColor, borderColor: mainColor,
backgroundColor: colorBackground, backgroundColor: colorBackground,
paddingVertical: StyleConstants.Spacing[spacing], paddingVertical: StyleConstants.Spacing[spacing],
paddingHorizontal: paddingHorizontal: StyleConstants.Spacing[spacing] + StyleConstants.Spacing.XS,
StyleConstants.Spacing[spacing] + StyleConstants.Spacing.XS,
width: round && layoutHeight ? layoutHeight : undefined width: round && layoutHeight ? layoutHeight : undefined
}, },
customStyle customStyle
]} ]}
{...(round && { {...(round && {
onLayout: ({ nativeEvent }) => onLayout: ({ nativeEvent }) => setLayoutHeight(nativeEvent.layout.height)
setLayoutHeight(nativeEvent.layout.height)
})} })}
testID='base' testID='base'
onPress={onPress} onPress={onPress}

View File

@ -26,7 +26,6 @@ const AttachmentVideo: React.FC<Props> = ({
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)
const [videoPosition, setVideoPosition] = useState<number>(0)
const [videoResizeMode, setVideoResizeMode] = useState<ResizeMode>(ResizeMode.COVER) const [videoResizeMode, setVideoResizeMode] = useState<ResizeMode>(ResizeMode.COVER)
const playOnPress = useCallback(async () => { const playOnPress = useCallback(async () => {
setVideoLoading(true) setVideoLoading(true)
@ -34,19 +33,15 @@ const AttachmentVideo: React.FC<Props> = ({
await videoPlayer.current?.loadAsync({ uri: video.url }) await videoPlayer.current?.loadAsync({ uri: video.url })
} }
Platform.OS === 'android' && setVideoResizeMode(ResizeMode.CONTAIN) Platform.OS === 'android' && setVideoResizeMode(ResizeMode.CONTAIN)
await videoPlayer.current?.setPositionAsync(videoPosition)
await videoPlayer.current?.presentFullscreenPlayer() await videoPlayer.current?.presentFullscreenPlayer()
videoPlayer.current?.playAsync() videoPlayer.current?.playAsync()
setVideoLoading(false) setVideoLoading(false)
videoPlayer.current?.setOnPlaybackStatusUpdate(props => { videoPlayer.current?.setOnPlaybackStatusUpdate(props => {
if (props.isLoaded) { if (props.isLoaded) {
setVideoLoaded(true) setVideoLoaded(true)
if (props.positionMillis) {
setVideoPosition(props.positionMillis)
}
} }
}) })
}, [videoLoaded, videoPosition]) }, [videoLoaded])
const appState = useRef(AppState.currentState) const appState = useRef(AppState.currentState)
useEffect(() => { useEffect(() => {
@ -107,7 +102,7 @@ const AttachmentVideo: React.FC<Props> = ({
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) {
await videoPlayer.current?.pauseAsync() await videoPlayer.current?.stopAsync()
} }
} }
}} }}

View File

@ -0,0 +1,50 @@
import { useNavigation } from '@react-navigation/native'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { RootStackParamList } from '@utils/navigation/navigators'
import { useTranslation } from 'react-i18next'
const menuAt = ({ account }: { account: Mastodon.Account }): ContextMenu[][] => {
const { t } = useTranslation('componentContextMenu')
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>()
const menus: ContextMenu[][] = []
menus.push([
{
key: 'at-direct',
item: {
onSelect: () =>
navigation.navigate('Screen-Compose', {
type: 'conversation',
accts: [account.acct],
visibility: 'direct'
}),
disabled: false,
destructive: false,
hidden: false
},
title: t('at.direct'),
icon: 'envelope'
},
{
key: 'at-public',
item: {
onSelect: () =>
navigation.navigate('Screen-Compose', {
type: 'conversation',
accts: [account.acct],
visibility: 'public'
}),
disabled: false,
destructive: false,
hidden: false
},
title: t('at.public'),
icon: 'at'
}
])
return menus
}
export default menuAt

View File

@ -19,6 +19,10 @@
"action": "Denuncia i bloqueja l'usuari" "action": "Denuncia i bloqueja l'usuari"
} }
}, },
"at": {
"direct": "",
"public": ""
},
"copy": { "copy": {
"action": "Copia la publicació", "action": "Copia la publicació",
"succeed": "Copiat" "succeed": "Copiat"

View File

@ -19,6 +19,10 @@
"action": "" "action": ""
} }
}, },
"at": {
"direct": "",
"public": ""
},
"copy": { "copy": {
"action": "", "action": "",
"succeed": "" "succeed": ""

View File

@ -19,6 +19,10 @@
"action": "Nutzer melden und blockieren" "action": "Nutzer melden und blockieren"
} }
}, },
"at": {
"direct": "",
"public": ""
},
"copy": { "copy": {
"action": "Tröt kopieren", "action": "Tröt kopieren",
"succeed": "Kopiert" "succeed": "Kopiert"

View File

@ -352,7 +352,7 @@
} }
}, },
"trending": { "trending": {
"tags": "" "tags": "Angesagte Tags"
} }
}, },
"sections": { "sections": {

View File

@ -19,6 +19,10 @@
"action": "Report and block user" "action": "Report and block user"
} }
}, },
"at": {
"direct": "Direct message",
"public": "Public message"
},
"copy": { "copy": {
"action": "Copy toot", "action": "Copy toot",
"succeed": "Copied" "succeed": "Copied"

View File

@ -19,6 +19,10 @@
"action": "Reportar y bloquear usuario" "action": "Reportar y bloquear usuario"
} }
}, },
"at": {
"direct": "",
"public": ""
},
"copy": { "copy": {
"action": "Copiar toot", "action": "Copiar toot",
"succeed": "Copiado" "succeed": "Copiado"

View File

@ -19,6 +19,10 @@
"action": "" "action": ""
} }
}, },
"at": {
"direct": "",
"public": ""
},
"copy": { "copy": {
"action": "Copier le Pouet", "action": "Copier le Pouet",
"succeed": "Copié" "succeed": "Copié"

View File

@ -19,6 +19,10 @@
"action": "" "action": ""
} }
}, },
"at": {
"direct": "",
"public": ""
},
"copy": { "copy": {
"action": "", "action": "",
"succeed": "Copiato" "succeed": "Copiato"

View File

@ -19,6 +19,10 @@
"action": "ユーザーの報告とブロック" "action": "ユーザーの報告とブロック"
} }
}, },
"at": {
"direct": "",
"public": ""
},
"copy": { "copy": {
"action": "トゥートをコピー", "action": "トゥートをコピー",
"succeed": "コピー完了" "succeed": "コピー完了"

View File

@ -352,7 +352,7 @@
} }
}, },
"trending": { "trending": {
"tags": "" "tags": "トレンドタグ"
} }
}, },
"sections": { "sections": {

View File

@ -3,13 +3,13 @@
"OK": "확인", "OK": "확인",
"apply": "적용", "apply": "적용",
"cancel": "취소", "cancel": "취소",
"discard": "", "discard": "취소",
"continue": "", "continue": "계속",
"delete": "", "delete": "삭제",
"done": "" "done": "완료"
}, },
"customEmoji": { "customEmoji": {
"accessibilityLabel": "커스텀 모지 {{emoji}}" "accessibilityLabel": "커스텀 모지 {{emoji}}"
}, },
"message": { "message": {
"success": { "success": {
@ -24,7 +24,7 @@
}, },
"separator": ", ", "separator": ", ",
"discard": { "discard": {
"title": "", "title": "변경 사항이 저장되지 않음",
"message": "" "message": "변경 사항이 저장되지 않았습니다. 작업 내용 저장을 취소할까요?"
} }
} }

View File

@ -3,10 +3,10 @@
"account": { "account": {
"title": "사용자 동작", "title": "사용자 동작",
"following": { "following": {
"action_false": "", "action_false": "사용자 팔로우",
"action_true": "" "action_true": "사용자 팔로우 해제"
}, },
"inLists": "", "inLists": "리스트의 사용자 관리",
"mute": { "mute": {
"action_false": "사용자 뮤트", "action_false": "사용자 뮤트",
"action_true": "사용자 뮤트 해제" "action_true": "사용자 뮤트 해제"
@ -16,9 +16,13 @@
"action_true": "사용자 차단 해제" "action_true": "사용자 차단 해제"
}, },
"reports": { "reports": {
"action": "" "action": "사용자 신고 및 차단"
} }
}, },
"at": {
"direct": "",
"public": ""
},
"copy": { "copy": {
"action": "툿 복사", "action": "툿 복사",
"succeed": "복사됨" "succeed": "복사됨"

View File

@ -1,7 +1,7 @@
{ {
"screenshot": { "screenshot": {
"title": "개인정보 보호", "title": "개인정보 보호",
"message": "다른 사용자의 사용자 이름이나, 아바타 등의 정보를 유출하지 말아주세요. 고마워요!", "message": "다른 사용자의 이름이나, 프로필 사진 등의 정보를 유출하지 말아주세요. 고마워요!",
"button": "확인" "button": "확인"
}, },
"localCorrupt": { "localCorrupt": {

View File

@ -50,13 +50,13 @@
"name": "목록: {{list}}" "name": "목록: {{list}}"
}, },
"listAccounts": { "listAccounts": {
"name": "" "name": "{{list}} 리스트의 사용자"
}, },
"listAdd": { "listAdd": {
"name": "" "name": "리스트에 추가"
}, },
"listEdit": { "listEdit": {
"name": "" "name": "리스트 상세 편집"
}, },
"lists": { "lists": {
"name": "목록" "name": "목록"
@ -97,27 +97,27 @@
} }
}, },
"listAccounts": { "listAccounts": {
"heading": "", "heading": "사용자 관리",
"error": "", "error": "사용자를 리스트에서 제거",
"empty": "" "empty": "이 리스트에 추가된 사용자가 없어요"
}, },
"listEdit": { "listEdit": {
"heading": "", "heading": "리스트 상세 편집",
"title": "", "title": "이름",
"repliesPolicy": { "repliesPolicy": {
"heading": "", "heading": "답장을 표시할 대상:",
"options": { "options": {
"none": "", "none": "없음",
"list": "", "list": "리스트의 사용자",
"followed": "" "followed": "모든 팔로우 중인 사용자"
} }
} }
}, },
"listDelete": { "listDelete": {
"heading": "", "heading": "리스트 삭제",
"confirm": { "confirm": {
"title": "", "title": "리스트 \"{{list}}\"를 삭제할까요?",
"message": "" "message": "이 작업은 되돌릴 수 없습니다."
} }
}, },
"profile": { "profile": {
@ -127,18 +127,18 @@
}, },
"root": { "root": {
"name": { "name": {
"title": "표시 이름" "title": "사옹자 이름"
}, },
"avatar": { "avatar": {
"title": "아바타", "title": "아바타",
"description": "400x400px으로 다운스케일되어요" "description": "400x400px 크기로 조정돼요"
}, },
"header": { "header": {
"title": "배너", "title": "배너",
"description": "1500x500px으로 다운스케일되어요" "description": "1500x500px 크기로 조정돼요"
}, },
"note": { "note": {
"title": "설명" "title": "자기소개"
}, },
"fields": { "fields": {
"title": "메타데이터", "title": "메타데이터",
@ -154,15 +154,15 @@
} }
}, },
"sensitive": { "sensitive": {
"title": "미디어 민감함으로 포스트" "title": "미디어 민감함 표시 후 게시"
}, },
"lock": { "lock": {
"title": "계정 잠그기", "title": "계정 잠그기",
"description": "내가 직접 팔로워를 수락해야해요" "description": "직접 승인한 사람만 나를 팔로우 할 수 있어요"
}, },
"bot": { "bot": {
"title": "봇 계정", "title": "봇 계정",
"description": "이 계정이 대부분 자동으로 작업을 수행하고 잘 확인하지 않는다는 것을 알려요." "description": "이 계정이 대부분 자동으로 작업을 수행하고 잘 확인하지 않는다는 것을 알려요"
} }
}, },
"fields": { "fields": {
@ -180,17 +180,17 @@
}, },
"global": { "global": {
"heading": "{{acct}} 활성화", "heading": "{{acct}} 활성화",
"description": "메시지는 tooot의 서버를 거쳐 라우트되어요" "description": "메시지는 tooot의 서버를 거쳐 전달돼요"
}, },
"decode": { "decode": {
"heading": "메시지 세부 정보", "heading": "메시지 세부 정보",
"description": "tooot의 서버를 거치는 메시지는 암호화되지만, 메시지를 서버에서 복호화하도록 설정할 수 있습니다. 서버의 소스는 오픈 소스이고, 로그하지 않습니다." "description": "tooot의 서버를 거치는 메시지는 암호화되어 있지만, 서버에서 이를 복호화하도록 선택할 수 있어요. 서버의 소스 코드는 오픈 소스로 관리되며 로그를 남기지 않습니다."
}, },
"default": { "default": {
"heading": "기본값" "heading": "기본값"
}, },
"follow": { "follow": {
"heading": "새 팔로워" "heading": "새로운 팔로워"
}, },
"follow_request": { "follow_request": {
"heading": "팔로우 요청" "heading": "팔로우 요청"
@ -202,7 +202,7 @@
"heading": "부스트됨" "heading": "부스트됨"
}, },
"mention": { "mention": {
"heading": "멘션했어요" "heading": "멘션"
}, },
"poll": { "poll": {
"heading": "투표 업데이트" "heading": "투표 업데이트"
@ -210,7 +210,7 @@
"status": { "status": {
"heading": "구독한 사용자의 툿" "heading": "구독한 사용자의 툿"
}, },
"howitworks": "라우팅 방 알아보기" "howitworks": "메시지 라우팅 방식 더 알아보기"
}, },
"root": { "root": {
"announcements": { "announcements": {
@ -255,7 +255,7 @@
"heading": "$t(me.stacks.language.name)" "heading": "$t(me.stacks.language.name)"
}, },
"theme": { "theme": {
"heading": "모양", "heading": "테마",
"options": { "options": {
"auto": "시스템과 동일", "auto": "시스템과 동일",
"light": "밝은 모드", "light": "밝은 모드",
@ -296,7 +296,7 @@
"instanceVersion": "마스토돈 버전 v{{version}}" "instanceVersion": "마스토돈 버전 v{{version}}"
}, },
"switch": { "switch": {
"existing": "로그인된 것 중 선택", "existing": "로그인 한 계정 선택",
"new": "인스턴스에 로그인" "new": "인스턴스에 로그인"
} }
}, },
@ -318,15 +318,15 @@
"default": "툿", "default": "툿",
"all": "툿과 답장" "all": "툿과 답장"
}, },
"suspended": "계정이 서버 관리자에 의해 정지되었어요." "suspended": "계정이 서버 관리자에 의해 정지되었어요"
}, },
"accountInLists": { "accountInLists": {
"name": "", "name": "@{{username}}의 리스트",
"inLists": "", "inLists": "포함된 리스트",
"notInLists": "" "notInLists": "다른 리스트"
}, },
"attachments": { "attachments": {
"name": "<0 /><1>\"의 미디어</1>" "name": "<0 /><1>의 미디어</1>"
}, },
"hashtag": { "hashtag": {
"follow": "팔로우", "follow": "팔로우",
@ -337,11 +337,11 @@
}, },
"search": { "search": {
"header": { "header": {
"prefix": "무엇을", "prefix": "검색할",
"placeholder": "검색할까요..." "placeholder": "내용을 입력..."
}, },
"empty": { "empty": {
"general": "키워드를 입력해 <bold>$t(screenTabs:shared.search.sections.accounts)</bold>, <bold>$t(screenTabs:shared.search.sections.hashtags)</bold>이나 <bold>$t(screenTabs:shared.search.sections.statuses)</bold> 검색할 수 있어요", "general": "키워드를 입력해 <bold>$t(screenTabs:shared.search.sections.accounts)</bold>, <bold>$t(screenTabs:shared.search.sections.hashtags)</bold>, 또는 <bold>$t(screenTabs:shared.search.sections.statuses)</bold> 등을 검색할 수 있어요",
"advanced": { "advanced": {
"header": "고급 검색", "header": "고급 검색",
"example": { "example": {
@ -352,7 +352,7 @@
} }
}, },
"trending": { "trending": {
"tags": "" "tags": "유행하는 태그"
} }
}, },
"sections": { "sections": {
@ -367,12 +367,12 @@
}, },
"users": { "users": {
"accounts": { "accounts": {
"following": "팔로잉 {{count}}", "following": "{{count}} 팔로잉",
"followers": "{{count}} 팔로워" "followers": "{{count}} 팔로워"
}, },
"statuses": { "statuses": {
"reblogged_by": "{{count}} 부스트", "reblogged_by": "{{count}} 부스트",
"favourited_by": "{{count}} 즐겨찾기" "favourited_by": "{{count}} 즐겨찾기"
} }
} }
} }

View File

@ -19,6 +19,10 @@
"action": "Rapporteren en blokkeren" "action": "Rapporteren en blokkeren"
} }
}, },
"at": {
"direct": "",
"public": ""
},
"copy": { "copy": {
"action": "Toot kopiëren", "action": "Toot kopiëren",
"succeed": "Gekopieerd" "succeed": "Gekopieerd"

View File

@ -19,6 +19,10 @@
"action": "" "action": ""
} }
}, },
"at": {
"direct": "",
"public": ""
},
"copy": { "copy": {
"action": "", "action": "",
"succeed": "" "succeed": ""

View File

@ -19,6 +19,10 @@
"action": "" "action": ""
} }
}, },
"at": {
"direct": "",
"public": ""
},
"copy": { "copy": {
"action": "", "action": "",
"succeed": "Copiado" "succeed": "Copiado"

View File

@ -19,6 +19,10 @@
"action": "Rapportera och blockera användare" "action": "Rapportera och blockera användare"
} }
}, },
"at": {
"direct": "Direktmeddelande",
"public": "Offentligt meddelande"
},
"copy": { "copy": {
"action": "Kopiera inlägg", "action": "Kopiera inlägg",
"succeed": "Kopierat" "succeed": "Kopierat"

View File

@ -352,7 +352,7 @@
} }
}, },
"trending": { "trending": {
"tags": "" "tags": "Trendande hashtaggar"
} }
}, },
"sections": { "sections": {

View File

@ -19,6 +19,10 @@
"action": "Báo cáo và chặn" "action": "Báo cáo và chặn"
} }
}, },
"at": {
"direct": "",
"public": ""
},
"copy": { "copy": {
"action": "Sao chép tút", "action": "Sao chép tút",
"succeed": "Đã sao chép" "succeed": "Đã sao chép"

View File

@ -19,6 +19,10 @@
"action": "举报并屏蔽用户" "action": "举报并屏蔽用户"
} }
}, },
"at": {
"direct": "私信",
"public": "公开信息"
},
"copy": { "copy": {
"action": "复制嘟文", "action": "复制嘟文",
"succeed": "已复制" "succeed": "已复制"

View File

@ -19,6 +19,10 @@
"action": "檢舉並封鎖使用者" "action": "檢舉並封鎖使用者"
} }
}, },
"at": {
"direct": "私訊",
"public": "公開訊息"
},
"copy": { "copy": {
"action": "複製嘟文", "action": "複製嘟文",
"succeed": "已複製" "succeed": "已複製"

View File

@ -352,7 +352,7 @@
} }
}, },
"trending": { "trending": {
"tags": "" "tags": "熱門標籤"
} }
}, },
"sections": { "sections": {

View File

@ -259,7 +259,9 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
type='text' type='text'
content={ content={
params?.type params?.type
? t(`heading.right.button.${params.type}`) ? params.type === 'conversation' && params.visibility === 'direct'
? t(`heading.right.button.${params.type}`)
: t('heading.right.button.default')
: t('heading.right.button.default') : t('heading.right.button.default')
} }
onPress={() => { onPress={() => {
@ -317,9 +319,8 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
] ]
) )
} else { } else {
Sentry.captureMessage('Compose posting', { Sentry.setContext('Error object', { error })
contexts: { errorObject: error } Sentry.captureMessage('Posting error')
})
haptics('Error') haptics('Error')
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, [

View File

@ -73,8 +73,6 @@ const ComposeEditAttachmentRoot: React.FC<Props> = ({ index }) => {
color: colors.primaryDefault color: colors.primaryDefault
}} }}
onFocus={() => scrollViewRef.current?.scrollToEnd()} onFocus={() => scrollViewRef.current?.scrollToEnd()}
autoCapitalize='none'
autoCorrect={false}
maxLength={1500} maxLength={1500}
multiline multiline
onChangeText={(e) => onChangeText={(e) =>

View File

@ -35,8 +35,6 @@ const ComposeSpoilerInput: React.FC = () => {
fontSize: adaptedFontsize, fontSize: adaptedFontsize,
lineHeight: adaptedLineheight lineHeight: adaptedLineheight
}} }}
autoCapitalize='none'
autoCorrect={false}
autoFocus autoFocus
enablesReturnKeyAutomatically enablesReturnKeyAutomatically
multiline multiline

View File

@ -92,7 +92,7 @@ const composeParseState = (
...composeInitialState, ...composeInitialState,
dirty: true, dirty: true,
timestamp: Date.now(), timestamp: Date.now(),
...assignVisibility('direct') ...assignVisibility(params.visibility || 'direct')
} }
} }
} }

View File

@ -57,7 +57,6 @@ const TabMeListAccounts: React.FC<TabMeStackScreenProps<'Tab-Me-List-Accounts'>>
<ComponentAccount <ComponentAccount
key={index} key={index}
account={item} account={item}
Component={View}
children={ children={
<Button <Button
type='icon' type='icon'
@ -68,6 +67,7 @@ const TabMeListAccounts: React.FC<TabMeStackScreenProps<'Tab-Me-List-Accounts'>>
} }
/> />
} }
props={{ disabled: true }}
/> />
)} )}
ListEmptyComponent={ ListEmptyComponent={

View File

@ -58,6 +58,7 @@ const SettingsTooot: React.FC = () => {
navigation.navigate('Screen-Compose', { navigation.navigate('Screen-Compose', {
type: 'conversation', type: 'conversation',
accts: ['tooot@xmflsct.com'], accts: ['tooot@xmflsct.com'],
visibility: 'direct',
text: text:
'[' + '[' +
`${Platform.OS}/${Platform.Version}` + `${Platform.OS}/${Platform.Version}` +

View File

@ -1,4 +1,5 @@
import Button from '@components/Button' import Button from '@components/Button'
import menuAt from '@components/contextMenu/at'
import { RelationshipOutgoing } from '@components/Relationship' import { RelationshipOutgoing } from '@components/Relationship'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { useRelationshipQuery } from '@utils/queryHooks/relationship' import { useRelationshipQuery } from '@utils/queryHooks/relationship'
@ -8,32 +9,13 @@ import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { StyleSheet, View } from 'react-native' import { StyleSheet, View } from 'react-native'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import * as DropdownMenu from 'zeego/dropdown-menu'
export interface Props { export interface Props {
account: Mastodon.Account | undefined account: Mastodon.Account | undefined
myInfo?: boolean myInfo?: boolean
} }
const Conversation = ({ account }: { account: Mastodon.Account }) => {
const navigation = useNavigation<any>()
const query = useRelationshipQuery({ id: account.id })
return query.data && !query.data.blocked_by ? (
<Button
round
type='icon'
content='Mail'
style={styles.actionLeft}
onPress={() =>
navigation.navigate('Screen-Compose', {
type: 'conversation',
accts: [account.acct]
})
}
/>
) : null
}
const AccountInformationActions: React.FC<Props> = ({ account, myInfo }) => { const AccountInformationActions: React.FC<Props> = ({ account, myInfo }) => {
if (!account || account.suspended) { if (!account || account.suspended) {
return null return null
@ -71,10 +53,38 @@ const AccountInformationActions: React.FC<Props> = ({ account, myInfo }) => {
const instanceAccount = useSelector(getInstanceAccount, () => true) const instanceAccount = useSelector(getInstanceAccount, () => true)
const ownAccount = account?.id === instanceAccount?.id && account?.acct === instanceAccount?.acct const ownAccount = account?.id === instanceAccount?.id && account?.acct === instanceAccount?.acct
const query = useRelationshipQuery({ id: account.id })
const mAt = menuAt({ account })
if (!ownAccount && account) { if (!ownAccount && account) {
return ( return (
<View style={styles.base}> <View style={styles.base}>
<Conversation account={account} /> {query.data && !query.data.blocked_by ? (
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Button
round
type='icon'
content='AtSign'
style={{ marginRight: StyleConstants.Spacing.S }}
onPress={() => {}}
/>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
{mAt.map((mGroup, index) => (
<DropdownMenu.Group key={index}>
{mGroup.map(menu => (
<DropdownMenu.Item key={menu.key} {...menu.item}>
<DropdownMenu.ItemTitle children={menu.title} />
<DropdownMenu.ItemIcon iosIconName={menu.icon} />
</DropdownMenu.Item>
))}
</DropdownMenu.Group>
))}
</DropdownMenu.Content>
</DropdownMenu.Root>
) : null}
<RelationshipOutgoing id={account.id} /> <RelationshipOutgoing id={account.id} />
</View> </View>
) )
@ -86,9 +96,9 @@ const AccountInformationActions: React.FC<Props> = ({ account, myInfo }) => {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
base: { base: {
alignSelf: 'flex-end', alignSelf: 'flex-end',
flexDirection: 'row' flexDirection: 'row',
}, alignItems: 'center'
actionLeft: { marginRight: StyleConstants.Spacing.S } }
}) })
export default AccountInformationActions export default AccountInformationActions

View File

@ -71,12 +71,12 @@ const TabSharedSearch: React.FC<TabSharedStackScreenProps<'Tab-Shared-Search'>>
})} })}
autoCapitalize='none' autoCapitalize='none'
autoCorrect={false} autoCorrect={false}
clearButtonMode='never' clearButtonMode='always'
keyboardType='web-search' keyboardType='web-search'
onSubmitEditing={({ nativeEvent: { text } }) => navigation.setParams({ text })} onSubmitEditing={({ nativeEvent: { text } }) => navigation.setParams({ text })}
placeholder={t('shared.search.header.placeholder')} placeholder={t('shared.search.header.placeholder')}
placeholderTextColor={colors.secondary} placeholderTextColor={colors.secondary}
returnKeyType='go' returnKeyType='search'
/> />
</View> </View>
) )

View File

@ -2,6 +2,7 @@ import { BottomTabScreenProps } from '@react-navigation/bottom-tabs'
import { NavigatorScreenParams } from '@react-navigation/native' import { NavigatorScreenParams } from '@react-navigation/native'
import { NativeStackScreenProps } from '@react-navigation/native-stack' import { NativeStackScreenProps } from '@react-navigation/native-stack'
import { StackNavigationProp } from '@react-navigation/stack' import { StackNavigationProp } from '@react-navigation/stack'
import { ComposeState } from '@screens/Compose/utils/types'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
export type RootStackParamList = { export type RootStackParamList = {
@ -38,6 +39,7 @@ export type RootStackParamList = {
| { | {
type: 'conversation' type: 'conversation'
accts: Mastodon.Account['acct'][] accts: Mastodon.Account['acct'][]
visibility: ComposeState['visibility']
text?: string // For contacting tooot only text?: string // For contacting tooot only
} }
| { | {

View File

@ -33,14 +33,11 @@ const pushUseConnect = () => {
}) })
.then(() => Notifications.setBadgeCountAsync(0)) .then(() => Notifications.setBadgeCountAsync(0))
.catch(error => { .catch(error => {
Sentry.setExtras({ Sentry.setContext('Error response', {
API: 'tooot', ...(error?.response && { response: error.response?._response })
expoToken,
...(error?.response && { response: error.response })
})
Sentry.captureMessage('Push connect error', {
contexts: { errorObject: error }
}) })
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({
@ -84,10 +81,7 @@ const pushUseConnect = () => {
} }
useEffect(() => { useEffect(() => {
Sentry.setExtras({ Sentry.setContext('Push', { expoToken, pushEnabledCount: pushEnabled.length })
expoToken,
pushEnabledCount: pushEnabled
})
if (expoToken && pushEnabled.length) { if (expoToken && pushEnabled.length) {
connect() connect()

View File

@ -74,8 +74,7 @@ const pushRegister = async (
}) })
if (!res.body.server_key?.length) { if (!res.body.server_key?.length) {
Sentry.setExtras({ Sentry.setContext('Push server key', {
API: 'tooot',
instance: instanceUri, instance: instanceUri,
resBody: res.body resBody: res.body
}) })