diff --git a/Gemfile.lock b/Gemfile.lock index 4a89e4dc..ec69c7a9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -17,16 +17,16 @@ GEM artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.582.0) - aws-sdk-core (3.130.2) + aws-partitions (1.598.0) + aws-sdk-core (3.131.1) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.525.0) aws-sigv4 (~> 1.1) - jmespath (~> 1.0) - aws-sdk-kms (1.56.0) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.57.0) aws-sdk-core (~> 3, >= 3.127.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.113.2) + aws-sdk-s3 (1.114.0) aws-sdk-core (~> 3, >= 3.127.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) @@ -106,8 +106,8 @@ GEM faraday-em_synchrony (1.0.0) faraday-excon (1.1.0) faraday-httpclient (1.0.1) - faraday-multipart (1.0.3) - multipart-post (>= 1.2, < 3) + faraday-multipart (1.0.4) + multipart-post (~> 2) faraday-net_http (1.0.1) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) @@ -116,7 +116,7 @@ GEM faraday_middleware (1.2.0) faraday (~> 1.0) fastimage (2.2.6) - fastlane (2.205.2) + fastlane (2.206.2) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -163,9 +163,9 @@ GEM fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.19.0) + google-apis-androidpublisher_v3 (0.21.0) google-apis-core (>= 0.4, < 2.a) - google-apis-core (0.4.2) + google-apis-core (0.5.0) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) httpclient (>= 2.8.1, < 3.a) @@ -178,7 +178,7 @@ GEM google-apis-core (>= 0.4, < 2.a) google-apis-playcustomapp_v1 (0.7.0) google-apis-core (>= 0.4, < 2.a) - google-apis-storage_v1 (0.13.0) + google-apis-storage_v1 (0.14.0) google-apis-core (>= 0.4, < 2.a) google-cloud-core (1.6.0) google-cloud-env (~> 1.0) @@ -202,14 +202,14 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.4) + http-cookie (1.0.5) domain_name (~> 0.5) httpclient (2.8.3) i18n (1.10.0) concurrent-ruby (~> 1.0) jmespath (1.6.1) - json (2.6.1) - jwt (2.3.0) + json (2.6.2) + jwt (2.4.1) memoist (0.16.2) mini_magick (4.11.0) mini_mime (1.1.2) @@ -226,7 +226,7 @@ GEM plist (3.6.0) public_suffix (4.0.7) rake (13.0.6) - representable (3.1.1) + representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) @@ -260,7 +260,7 @@ GEM uber (0.1.0) unf (0.1.4) unf_ext - unf_ext (0.0.8.1) + unf_ext (0.0.8.2) unicode-display_width (1.8.0) webrick (1.7.0) word_wrap (1.0.0) diff --git a/README.md b/README.md index c4c48d1b..0e64ec9f 100644 --- a/README.md +++ b/README.md @@ -15,3 +15,5 @@ [@luizpicolo](https://github.com/luizpicolo) for Brazilian Portuguese [@duy@mas.to](https://mas.to/@duy) for Vietnamese translation + +[@jk@mastodon.social](https://mastodon.social/@jk) for the famous Mastodon boop sound diff --git a/ios/Podfile.lock b/ios/Podfile.lock index aa187b13..b00f4cf7 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -4,6 +4,8 @@ PODS: - DoubleConversion (1.1.6) - EASClient (0.2.1): - ExpoModulesCore + - EXApplication (4.1.0): + - ExpoModulesCore - EXAV (11.2.3): - ExpoModulesCore - React-runtimeexecutor @@ -23,6 +25,8 @@ PODS: - EXFirebaseCore (5.0.0): - ExpoModulesCore - Firebase/Core (= 8.14.0) + - EXFont (10.1.0): + - ExpoModulesCore - EXImageLoader (3.2.0): - ExpoModulesCore - React-Core @@ -37,8 +41,13 @@ PODS: - ExpoModulesCore - ExpoHaptics (11.2.0): - ExpoModulesCore + - ExpoImageManipulator (10.3.1): + - EXImageLoader + - ExpoModulesCore - ExpoImagePicker (13.1.1): - ExpoModulesCore + - ExpoKeepAwake (10.1.1): + - ExpoModulesCore - ExpoLocalization (13.0.0): - ExpoModulesCore - ExpoModulesCore (0.9.2): @@ -590,6 +599,7 @@ DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - EASClient (from `../node_modules/expo-eas-client/ios`) + - EXApplication (from `../node_modules/expo-application/ios`) - EXAV (from `../node_modules/expo-av/ios`) - EXConstants (from `../node_modules/expo-constants/ios`) - EXDevice (from `../node_modules/expo-device/ios`) @@ -597,6 +607,7 @@ DEPENDENCIES: - EXFileSystem (from `../node_modules/expo-file-system/ios`) - EXFirebaseAnalytics (from `../node_modules/expo-firebase-analytics/ios`) - EXFirebaseCore (from `../node_modules/expo-firebase-core/ios`) + - EXFont (from `../node_modules/expo-font/ios`) - EXImageLoader (from `../node_modules/expo-image-loader/ios`) - EXJSONUtils (from `../node_modules/expo-json-utils/ios`) - EXManifests (from `../node_modules/expo-manifests/ios`) @@ -604,7 +615,9 @@ DEPENDENCIES: - Expo (from `../node_modules/expo/ios`) - ExpoCrypto (from `../node_modules/expo-crypto/ios`) - ExpoHaptics (from `../node_modules/expo-haptics/ios`) + - ExpoImageManipulator (from `../node_modules/expo-image-manipulator/ios`) - ExpoImagePicker (from `../node_modules/expo-image-picker/ios`) + - ExpoKeepAwake (from `../node_modules/expo-keep-awake/ios`) - ExpoLocalization (from `../node_modules/expo-localization/ios`) - ExpoModulesCore (from `../node_modules/expo-modules-core/ios`) - ExpoRandom (from `../node_modules/expo-random/ios`) @@ -701,6 +714,8 @@ EXTERNAL SOURCES: :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" EASClient: :path: "../node_modules/expo-eas-client/ios" + EXApplication: + :path: "../node_modules/expo-application/ios" EXAV: :path: "../node_modules/expo-av/ios" EXConstants: @@ -715,6 +730,8 @@ EXTERNAL SOURCES: :path: "../node_modules/expo-firebase-analytics/ios" EXFirebaseCore: :path: "../node_modules/expo-firebase-core/ios" + EXFont: + :path: "../node_modules/expo-font/ios" EXImageLoader: :path: "../node_modules/expo-image-loader/ios" EXJSONUtils: @@ -729,8 +746,12 @@ EXTERNAL SOURCES: :path: "../node_modules/expo-crypto/ios" ExpoHaptics: :path: "../node_modules/expo-haptics/ios" + ExpoImageManipulator: + :path: "../node_modules/expo-image-manipulator/ios" ExpoImagePicker: :path: "../node_modules/expo-image-picker/ios" + ExpoKeepAwake: + :path: "../node_modules/expo-keep-awake/ios" ExpoLocalization: :path: "../node_modules/expo-localization/ios" ExpoModulesCore: @@ -859,6 +880,7 @@ SPEC CHECKSUMS: boost: a7c83b31436843459a1961bfd74b96033dc77234 DoubleConversion: 831926d9b8bf8166fd87886c4abab286c2422662 EASClient: 93565f4d024559b75eac62bc7d50acaa354614f6 + EXApplication: d6562af1204162e0ac46d341a7d4e5dc720b33de EXAV: 88f61c5af8415715b7ee51f084c1020235b85c56 EXConstants: fdbe52259365b6a6faaa5e99a3b82cfa6bc2eb61 EXDevice: 0115b360059ccd32c1701744e374e3259ffbdd3c @@ -866,6 +888,7 @@ SPEC CHECKSUMS: EXFileSystem: 2aa2d9289f84bca9532b9ccbd81504fa31eb1ded EXFirebaseAnalytics: aeefc63f92277313c3ee86da6a7ecf892f345ed1 EXFirebaseCore: bdfa87df74fa1b74a6b38957561456aabad28a4f + EXFont: 04235cc22e6fef86028feb67db452978dc6f240f EXImageLoader: b88e053d760f85a82405b1db2de4abf11978fc9f EXJSONUtils: 2a74b8f40f1523cc3f92af99c91aa78201737a77 EXManifests: 0c6134b7b6f3236a93a778c3f44ba1cfb3f9fa3d @@ -873,7 +896,9 @@ SPEC CHECKSUMS: Expo: b9fff0a1eac0f424fc68ea49b4347fb308e52e17 ExpoCrypto: d0d0f3e20875dc450b4ec88f0fb608da5c2c6c17 ExpoHaptics: ad58ec96a25e57579c14a47c7d71f0de0de8656a + ExpoImageManipulator: b55580bbc7b10099c7707949903e7176a8542ee8 ExpoImagePicker: d9d6b4f29db437fc7796f13cee5f133f5b4b5f7c + ExpoKeepAwake: c0c494b442ecd8122974c13b93ccfb57bd408e88 ExpoLocalization: 8f619bb6eec64575cd5220bfabbd7b4e2d6f33f8 ExpoModulesCore: e4278a668e8c13c0269ed8b8a4200989deea2973 ExpoRandom: 14df0976aa363a71a730ceb7655250f3047c0e42 diff --git a/package.json b/package.json index 28035f02..53b9592a 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "expo-file-system": "14.0.0", "expo-firebase-analytics": "7.0.0", "expo-haptics": "11.2.0", + "expo-image-manipulator": "^10.3.1", "expo-image-picker": "13.1.1", "expo-linking": "3.1.0", "expo-localization": "13.0.0", @@ -153,4 +154,4 @@ } } } -} +} \ No newline at end of file diff --git a/src/Screens.tsx b/src/Screens.tsx index ae85cbca..b5e1c889 100644 --- a/src/Screens.tsx +++ b/src/Screens.tsx @@ -178,11 +178,11 @@ const Screens: React.FC = ({ localCorrupt }) => { } let text: string | undefined = undefined - let media: { path: string; mime: string }[] = [] + let media: { uri: string; mime: string }[] = [] const typesImage = ['png', 'jpg', 'jpeg', 'gif'] const typesVideo = ['mp4', 'm4v', 'mov', 'webm', 'mpeg'] - const filterMedia = ({ path, mime }: { path: string; mime: string }) => { + const filterMedia = ({ uri, mime }: { uri: string; mime: string }) => { if (mime.startsWith('image/')) { if (!typesImage.includes(mime.split('/')[1])) { console.warn('Image type not supported:', mime.split('/')[1]) @@ -195,7 +195,7 @@ const Screens: React.FC = ({ localCorrupt }) => { }) return } - media.push({ path, mime }) + media.push({ uri, mime }) } else if (mime.startsWith('video/')) { if (!typesVideo.includes(mime.split('/')[1])) { console.warn('Video type not supported:', mime.split('/')[1]) @@ -208,17 +208,17 @@ const Screens: React.FC = ({ localCorrupt }) => { }) return } - media.push({ path, mime }) + media.push({ uri, mime }) } else { - if (typesImage.includes(path.split('.').pop() || '')) { - media.push({ path: path, mime: 'image/jpg' }) + if (typesImage.includes(uri.split('.').pop() || '')) { + media.push({ uri, mime: 'image/jpg' }) return } - if (typesVideo.includes(path.split('.').pop() || '')) { - media.push({ path: path, mime: 'video/mp4' }) + if (typesVideo.includes(uri.split('.').pop() || '')) { + media.push({ uri, mime: 'video/mp4' }) return } - text = !text ? path : text.concat(text, `\n${path}`) + text = !text ? uri : text.concat(text, `\n${uri}`) } } @@ -230,7 +230,7 @@ const Screens: React.FC = ({ localCorrupt }) => { for (const d of item.data) { if (typeof d !== 'string') { - filterMedia({ path: d.data, mime: d.mimeType }) + filterMedia({ uri: d.data, mime: d.mimeType }) } } break @@ -245,7 +245,7 @@ const Screens: React.FC = ({ localCorrupt }) => { tempData = item.data } for (const d of item.data) { - filterMedia({ path: d, mime: item.mimeType }) + filterMedia({ uri: d, mime: item.mimeType }) } break } diff --git a/src/api/general.ts b/src/api/general.ts index a39f815e..cbcc784f 100644 --- a/src/api/general.ts +++ b/src/api/general.ts @@ -68,8 +68,8 @@ const apiGeneral = async ({ error.response.data.error ) return Promise.reject({ - status: error.response.status, - message: error.response.data.error + status: error?.response.status, + message: error?.response.data.error }) } else if (error?.request) { // The request was made but no response was received diff --git a/src/api/instance.ts b/src/api/instance.ts index 86d9a367..883302e7 100644 --- a/src/api/instance.ts +++ b/src/api/instance.ts @@ -110,8 +110,8 @@ const apiInstance = async ({ error.response.data.error ) return Promise.reject({ - status: error.response.status, - message: error.response.data.error + status: error?.response.status, + message: error?.response.data.error }) } else if (error?.request) { // The request was made but no response was received diff --git a/src/api/tooot.ts b/src/api/tooot.ts index b4264a76..9c029b53 100644 --- a/src/api/tooot.ts +++ b/src/api/tooot.ts @@ -66,7 +66,7 @@ const apiTooot = async ({ }) }) .catch(error => { - if (sentry && Math.random() < 0.1) { + if (sentry) { Sentry.Native.setExtras({ API: 'tooot', ...(error?.response && { response: error.response }), @@ -85,8 +85,8 @@ const apiTooot = async ({ error.response.data.error ) return Promise.reject({ - status: error.response.status, - message: error.response.data.error + status: error?.response.status, + message: error?.response.data.error }) } else if (error?.request) { // The request was made but no response was received diff --git a/src/components/Parse/HTML.tsx b/src/components/Parse/HTML.tsx index c4c0ff62..3c1b26f4 100644 --- a/src/components/Parse/HTML.tsx +++ b/src/components/Parse/HTML.tsx @@ -49,7 +49,7 @@ const renderNode = ({ const href = node.attribs.href if (classes) { if (classes.includes('hashtag')) { - const tag = href.split(new RegExp(/\/tag\/(.*)|\/tags\/(.*)/)) + const tag = href?.split(new RegExp(/\/tag\/(.*)|\/tags\/(.*)/)) const differentTag = routeParams?.hashtag ? routeParams.hashtag !== tag[1] && routeParams.hashtag !== tag[2] : true @@ -107,7 +107,7 @@ const renderNode = ({ ) } } else { - const domain = href.split(new RegExp(/:\/\/(.[^\/]+)/)) + const domain = href?.split(new RegExp(/:\/\/(.[^\/]+)/)) // Need example here const content = node.children && node.children[0] && node.children[0].data @@ -124,11 +124,15 @@ const renderNode = ({ }} onPress={async () => { analytics('status_link_press') - !disableDetails && !shouldBeTag - ? await openLink(href, navigation) - : navigation.push('Tab-Shared-Hashtag', { + if (!disableDetails) { + if (shouldBeTag) { + navigation.push('Tab-Shared-Hashtag', { hashtag: content.substring(1) }) + } else { + await openLink(href, navigation) + } + } }} > {(content && content !== href && content) || diff --git a/src/components/Timeline/Shared/Actioned.tsx b/src/components/Timeline/Shared/Actioned.tsx index 0ff986e3..59db82b9 100644 --- a/src/components/Timeline/Shared/Actioned.tsx +++ b/src/components/Timeline/Shared/Actioned.tsx @@ -22,7 +22,7 @@ const TimelineActioned = React.memo( const { colors } = useTheme() const navigation = useNavigation>() - const name = account?.display_name || account.username + const name = account?.display_name || account?.username const iconColor = colors.primaryDefault const content = (content: string) => ( diff --git a/src/components/Timeline/Shared/HeaderShared/Account.tsx b/src/components/Timeline/Shared/HeaderShared/Account.tsx index a3a1f84d..5efe3bf9 100644 --- a/src/components/Timeline/Shared/HeaderShared/Account.tsx +++ b/src/components/Timeline/Shared/HeaderShared/Account.tsx @@ -27,7 +27,7 @@ const HeaderSharedAccount = React.memo( numberOfLines={1} > diff --git a/src/components/mediaSelector.ts b/src/components/mediaSelector.ts index ffc92b9f..4d18cb78 100644 --- a/src/components/mediaSelector.ts +++ b/src/components/mediaSelector.ts @@ -2,9 +2,10 @@ import analytics from '@components/analytics' import { ActionSheetOptions } from '@expo/react-native-action-sheet' import { store } from '@root/store' import { getInstanceConfigurationStatusMaxAttachments } from '@utils/slices/instancesSlice' +import { manipulateAsync, SaveFormat } from 'expo-image-manipulator' import * as ExpoImagePicker from 'expo-image-picker' import i18next from 'i18next' -import { Alert, Linking } from 'react-native' +import { Alert, Linking, Platform } from 'react-native' import ImagePicker, { Image, ImageOrVideo @@ -27,7 +28,7 @@ const mediaSelector = async ({ maximum, indicateMaximum = false, showActionSheetWithOptions -}: Props): Promise => { +}: Props): Promise<({ uri: string } & Omit)[]> => { const checkLibraryPermission = async (): Promise => { const { status } = await ExpoImagePicker.requestMediaLibraryPermissionsAsync() @@ -110,17 +111,40 @@ const mediaSelector = async ({ multiple: true, minFiles: 1, maxFiles: _maximum, - loadingLabelText: '', - compressImageMaxWidth: 4096, - compressImageMaxHeight: 4096 + smartAlbums: ['UserLibrary'], + writeTempFile: false, + loadingLabelText: '' }).catch(() => {}) if (!images) { return reject() } + // react-native-image-crop-picker may return HEIC as JPG that causes upload failure + if (Platform.OS === 'ios') { + for (const [index, image] of images.entries()) { + if (image.mime === 'image/heic') { + const converted = await manipulateAsync(image.sourceURL!, [], { + base64: false, + compress: 0.8, + format: SaveFormat.JPEG + }) + images[index] = { + ...images[index], + sourceURL: converted.uri, + mime: 'image/jpeg' + } + } + } + } + if (!resize) { - return resolve(images) + return resolve( + images.map(image => ({ + ...image, + uri: image.sourceURL || `file://${image.path}` + })) + ) } else { const croppedImages: Image[] = [] for (const image of images) { @@ -135,7 +159,12 @@ const mediaSelector = async ({ }).catch(() => {}) croppedImage && croppedImages.push(croppedImage) } - return resolve(croppedImages) + return resolve( + croppedImages.map(image => ({ + ...image, + uri: `file://${image.path}` + })) + ) } } const selectVideo = async () => { @@ -146,7 +175,9 @@ const mediaSelector = async ({ }).catch(() => {}) if (video) { - return resolve([video]) + return resolve([ + { ...video, uri: video.sourceURL || `file://${video.path}` } + ]) } else { return reject() } diff --git a/src/components/mediaTransformation.ts b/src/components/mediaTransformation.ts new file mode 100644 index 00000000..29bb542d --- /dev/null +++ b/src/components/mediaTransformation.ts @@ -0,0 +1,149 @@ +import { store } from '@root/store' +import { getInstanceConfigurationMediaAttachments } from '@utils/slices/instancesSlice' +import { Action, manipulateAsync, SaveFormat } from 'expo-image-manipulator' +import i18next from 'i18next' +import { Platform } from 'react-native' +import ImagePicker from 'react-native-image-crop-picker' + +export interface Props { + type: 'image' | 'video' + uri: string // This can be pure path or uri starting with file:// + mime?: string + transform: { + imageFormat?: SaveFormat.JPEG | SaveFormat.PNG + resize?: boolean + width?: number + height?: number + } +} + +const getFileExtension = (uri: string) => { + const extension = uri.split('.').pop() + // Using mime type standard of jpeg + return extension === 'jpg' ? 'jpeg' : extension +} + +const mediaTransformation = async ({ + type, + uri, + mime, + transform +}: Props): Promise<{ + uri: string + mime: string + width: number + height: number +}> => { + const configurationMediaAttachments = + getInstanceConfigurationMediaAttachments(store.getState()) + + const fileExtension = getFileExtension(uri) + + switch (type) { + case 'image': + if (mime === 'image/gif' || fileExtension === 'gif') { + return Promise.reject('GIFs should not be transformed') + } + let targetFormat: SaveFormat.JPEG | SaveFormat.PNG = SaveFormat.JPEG + + const supportedImageTypes = + configurationMediaAttachments.supported_mime_types.filter(mime => + mime.startsWith('image/') + ) + + // @ts-ignore + const transformations: Action[] = [ + !transform.resize && (transform.width || transform.height) + ? { + resize: { width: transform.width, height: transform.height } + } + : null + ].filter(t => !!t) + + if (mime) { + if ( + mime !== `image/${fileExtension}` || + !supportedImageTypes.includes(mime) + ) { + targetFormat = transform.imageFormat || SaveFormat.JPEG + } else { + targetFormat = mime.split('/').pop() as any + } + } else { + if (!fileExtension) { + return Promise.reject('Unable to get file extension') + } + if (!supportedImageTypes.includes(`image/${fileExtension}`)) { + targetFormat = transform.imageFormat || SaveFormat.JPEG + } else { + targetFormat = fileExtension as any + } + } + + const converted = await manipulateAsync(uri, transformations, { + base64: false, + compress: Platform.OS === 'ios' ? 0.8 : 1, + format: targetFormat + }) + + if (transform.resize) { + const resized = await ImagePicker.openCropper({ + mediaType: 'photo', + path: converted.uri, + width: transform.width, + height: transform.height, + cropperChooseText: i18next.t('common:buttons.apply'), + cropperCancelText: i18next.t('common:buttons.cancel'), + hideBottomControls: true + }) + if (!resized) { + return Promise.reject('Resize failed') + } else { + return { + uri: resized.path, + mime: resized.mime, + width: resized.width, + height: resized.height + } + } + } else { + return { + uri: converted.uri, + mime: transform.imageFormat || SaveFormat.JPEG, + width: converted.width, + height: converted.height + } + } + case 'video': + const supportedVideoTypes = + configurationMediaAttachments.supported_mime_types.filter(mime => + mime.startsWith('video/') + ) + + if (mime) { + if (mime !== `video/${fileExtension}`) { + console.warn('Video mime type and file extension does not match') + } + if (!supportedVideoTypes.includes(mime)) { + return Promise.reject('Video file type is not supported') + } + } else { + if (!fileExtension) { + return Promise.reject('Unable to get file extension') + } + if (!supportedVideoTypes.includes(`video/${fileExtension}`)) { + return Promise.reject('Video file type is not supported') + } + } + + return { + uri: uri, + mime: mime || `video/${fileExtension}`, + width: 0, + height: 0 + } + break + } +} + +export default mediaTransformation diff --git a/src/i18n/pt_BR/components/contextMenu.json b/src/i18n/pt_BR/components/contextMenu.json index 67bf128b..f7ee3d83 100644 --- a/src/i18n/pt_BR/components/contextMenu.json +++ b/src/i18n/pt_BR/components/contextMenu.json @@ -1,7 +1,7 @@ { "accessibilityHint": "Ações para este toot, como o seu usuário publicado", "account": { - "title": "", + "title": "Ações do Usuário", "mute": { "action": "Silenciar usuário" }, @@ -13,11 +13,11 @@ } }, "instance": { - "title": "", + "title": "Ação da Instância", "block": { "action": "Bloquear a instância {{instance}}", "alert": { - "title": "", + "title": "Confirmar o bloqueio da instância {{instance}}?", "message": "Na maioria das vezes, você pode silenciar ou bloquear determinado usuário.\n\nDepois de bloquear a instância, todo seu conteúdo, incluindo seguidores, será removido!", "buttons": { "confirm": "Confirmar" @@ -34,7 +34,7 @@ } }, "status": { - "title": "", + "title": "Ações do toot", "edit": { "action": "Editar toot" }, @@ -42,29 +42,29 @@ "action": "Remover toot", "alert": { "title": "Confirme a exclusão?", - "message": "", + "message": "Todos os boosts e favoritos serão limpos, incluindo todas as respostas.", "buttons": { "confirm": "Confirmar" } } }, "deleteEdit": { - "action": "", + "action": "Excluir toot e repostar", "alert": { - "title": "", - "message": "", + "title": "Confirmar exclusão e repostagem?", + "message": "Todos os boosts e favoritos serão limpos, incluindo todas as respostas.", "buttons": { "confirm": "Confirmar" } } }, "mute": { - "action-muted_false": "", - "action-muted_true": "" + "action-muted_false": "Silenciar este toot e respostas", + "action-muted_true": "Desbloquear este toot e respostas" }, "pin": { - "action-pinned_false": "", - "action-pinned_true": "" + "action-pinned_false": "Toot fixado", + "action-pinned_true": "Desafixar toot" } } } \ No newline at end of file diff --git a/src/i18n/pt_BR/components/mediaSelector.json b/src/i18n/pt_BR/components/mediaSelector.json index 07a73554..f100048b 100644 --- a/src/i18n/pt_BR/components/mediaSelector.json +++ b/src/i18n/pt_BR/components/mediaSelector.json @@ -1,10 +1,10 @@ { "title": "Selecionar fonte de mídia", "options": { - "image": "", - "image_max": "", - "video": "", - "video_max": "" + "image": "Enviar fotos", + "image_max": "Carregar fotos (máx. {{max}})", + "video": "Enviar vídeo", + "video_max": "Carregar vídeo (máx. {{max}})" }, "library": { "alert": { diff --git a/src/i18n/pt_BR/components/timeline.json b/src/i18n/pt_BR/components/timeline.json index 31e94ca7..cc998cc9 100644 --- a/src/i18n/pt_BR/components/timeline.json +++ b/src/i18n/pt_BR/components/timeline.json @@ -30,7 +30,7 @@ "default": "{{name}} boostou", "notification": "{{name}} deu boost no teu toot" }, - "update": "" + "update": "Toot foi editado" }, "actions": { "reply": { diff --git a/src/i18n/pt_BR/screens/compose.json b/src/i18n/pt_BR/screens/compose.json index 13e665fa..aa3677e0 100644 --- a/src/i18n/pt_BR/screens/compose.json +++ b/src/i18n/pt_BR/screens/compose.json @@ -15,7 +15,7 @@ "button": { "default": "Toot", "conversation": "Toot DM", - "reply": "Resposta de toot", + "reply": "Responder", "deleteEdit": "Toot", "edit": "Toot", "share": "Toot" diff --git a/src/i18n/pt_BR/screens/tabs.json b/src/i18n/pt_BR/screens/tabs.json index 7788ce8e..8581178c 100644 --- a/src/i18n/pt_BR/screens/tabs.json +++ b/src/i18n/pt_BR/screens/tabs.json @@ -32,7 +32,7 @@ "me": { "stacks": { "bookmarks": { - "name": "Favoritos" + "name": "Salvos" }, "conversations": { "name": "Mensagens diretas" @@ -82,7 +82,7 @@ "demo": "

Esta é uma demonstração também😊. Você pode escolher entre várias opções abaixo.

Esta configuração afeta apenas o conteúdo principal dos toots, mas não os tamanhos de outra fonte.

", "availableSizes": "Tamanhos disponíveis", "sizes": { - "S": "S", + "S": "P", "M": "M - Padrão", "L": "Grande", "XL": "Extra grande", diff --git a/src/screens/Actions/Status.tsx b/src/screens/Actions/Status.tsx index da423c41..dcffd6bd 100644 --- a/src/screens/Actions/Status.tsx +++ b/src/screens/Actions/Status.tsx @@ -49,7 +49,7 @@ const ActionsStatus: React.FC = ({ message: t('common:message.error.message', { function: t(`shared.header.actions.status.${theFunction}.function`) }), - ...(err.status && + ...(err?.status && typeof err.status === 'number' && err.data && err.data.error && diff --git a/src/screens/Compose/EditAttachment/Image.tsx b/src/screens/Compose/EditAttachment/Image.tsx index 7dd754d2..bebc9a15 100644 --- a/src/screens/Compose/EditAttachment/Image.tsx +++ b/src/screens/Compose/EditAttachment/Image.tsx @@ -128,8 +128,8 @@ const ComposeEditAttachmentImage: React.FC = ({ index }) => { height: imageDimensionis.height }} source={{ - uri: theAttachmentLocal?.path - ? `file://${theAttachmentLocal?.path}` + uri: theAttachmentLocal?.uri + ? theAttachmentLocal.uri : theAttachmentRemote?.preview_url }} /> diff --git a/src/screens/Compose/EditAttachment/Root.tsx b/src/screens/Compose/EditAttachment/Root.tsx index eeff4604..bff60bd9 100644 --- a/src/screens/Compose/EditAttachment/Root.tsx +++ b/src/screens/Compose/EditAttachment/Root.tsx @@ -34,7 +34,7 @@ const ComposeEditAttachmentRoot: React.FC = ({ index }) => { video={ video.local ? ({ - url: `file://${video.local.path}`, + url: video.local.uri, preview_url: video.local.local_thumbnail, blurhash: video.remote?.blurhash } as Mastodon.AttachmentVideo) diff --git a/src/screens/Compose/Root/Footer/addAttachment.ts b/src/screens/Compose/Root/Footer/addAttachment.ts index 268efa3c..63e11904 100644 --- a/src/screens/Compose/Root/Footer/addAttachment.ts +++ b/src/screens/Compose/Root/Footer/addAttachment.ts @@ -22,28 +22,25 @@ export const uploadAttachment = async ({ media }: { composeDispatch: Dispatch - media: Pick + media: { uri: string } & Pick }) => { const hash = await Crypto.digestStringAsync( Crypto.CryptoDigestAlgorithm.SHA256, - media.path + Math.random() + media.uri + Math.random() ) - let attachmentType: string switch (media.mime.split('/')[0]) { case 'image': - attachmentType = `image/${media.path.split('.')[1]}` composeDispatch({ type: 'attachment/upload/start', payload: { - local: { ...media, type: 'image', local_thumbnail: media.path, hash }, + local: { ...media, type: 'image', local_thumbnail: media.uri, hash }, uploading: true } }) break case 'video': - attachmentType = `video/${media.path.split('.')[1]}` - VideoThumbnails.getThumbnailAsync(media.path) + VideoThumbnails.getThumbnailAsync(media.uri) .then(({ uri, width, height }) => composeDispatch({ type: 'attachment/upload/start', @@ -71,7 +68,6 @@ export const uploadAttachment = async ({ ) break default: - attachmentType = 'unknown' composeDispatch({ type: 'attachment/upload/start', payload: { @@ -105,9 +101,9 @@ export const uploadAttachment = async ({ const formData = new FormData() formData.append('file', { - uri: `file://${media.path}`, - name: attachmentType, - type: attachmentType + uri: media.uri, + name: media.uri.match(new RegExp(/.*\/(.*)/))?.[1] || 'file.jpg', + type: media.mime } as any) return apiInstance({ diff --git a/src/screens/Compose/Root/Header/TextInput.tsx b/src/screens/Compose/Root/Header/TextInput.tsx index ff272a98..e148cbca 100644 --- a/src/screens/Compose/Root/Header/TextInput.tsx +++ b/src/screens/Compose/Root/Header/TextInput.tsx @@ -90,7 +90,7 @@ const ComposeTextInput: React.FC = () => { uploadAttachment({ composeDispatch, media: { - path: file.uri, + uri: file.uri, mime: file.type, width: 100, height: 100 diff --git a/src/screens/Compose/utils/reducer.ts b/src/screens/Compose/utils/reducer.ts index d727079b..0480393b 100644 --- a/src/screens/Compose/utils/reducer.ts +++ b/src/screens/Compose/utils/reducer.ts @@ -58,7 +58,7 @@ const composeReducer = ( attachments: { ...state.attachments, uploads: state.attachments.uploads.map(upload => - upload.local?.path === action.payload.local?.path + upload.local?.uri === action.payload.local?.uri ? { ...upload, remote: action.payload.remote, uploading: false } : upload ) diff --git a/src/screens/Compose/utils/types.d.ts b/src/screens/Compose/utils/types.d.ts index 6a109fcf..586be113 100644 --- a/src/screens/Compose/utils/types.d.ts +++ b/src/screens/Compose/utils/types.d.ts @@ -2,11 +2,11 @@ import { ImageOrVideo } from 'react-native-image-crop-picker' export type ExtendedAttachment = { remote?: Mastodon.Attachment - local?: Pick & { - type: 'image' | 'video' | 'unknown' - local_thumbnail?: string - hash?: string - } + local?: { uri: string } & Pick & { + type: 'image' | 'video' | 'unknown' + local_thumbnail?: string + hash?: string + } uploading?: boolean } @@ -123,7 +123,7 @@ export type ComposeAction = type: 'attachment/upload/end' payload: { remote: Mastodon.Attachment - local: Pick + local: { uri: string } & Pick } } | { diff --git a/src/screens/Tabs/Me/Profile/Root/AvatarHeader.tsx b/src/screens/Tabs/Me/Profile/Root/AvatarHeader.tsx index 7e9dfc7b..23d83b9e 100644 --- a/src/screens/Tabs/Me/Profile/Root/AvatarHeader.tsx +++ b/src/screens/Tabs/Me/Profile/Root/AvatarHeader.tsx @@ -46,7 +46,7 @@ const ProfileAvatarHeader: React.FC = ({ type, messageRef }) => { failed: true }, type, - data: image[0].path + data: image[0].uri }) }} /> diff --git a/src/utils/navigation/navigators.ts b/src/utils/navigation/navigators.ts index 283ccf5d..5937edab 100644 --- a/src/utils/navigation/navigators.ts +++ b/src/utils/navigation/navigators.ts @@ -42,7 +42,7 @@ export type RootStackParamList = { | { type: 'share' text?: string - media?: { path: string; mime: string }[] + media?: { uri: string; mime: string }[] } | undefined 'Screen-ImagesViewer': { diff --git a/src/utils/slices/instancesSlice.ts b/src/utils/slices/instancesSlice.ts index fd9d845e..845a38b1 100644 --- a/src/utils/slices/instancesSlice.ts +++ b/src/utils/slices/instancesSlice.ts @@ -249,7 +249,7 @@ const instancesSlice = createSlice({ // Update Instance Configuration .addCase(updateConfiguration.fulfilled, (state, action) => { const activeIndex = findInstanceActive(state.instances) - state.instances[activeIndex].version = action.payload.version + state.instances[activeIndex].version = action.payload?.version || '0' state.instances[activeIndex].configuration = action.payload.configuration }) @@ -316,7 +316,7 @@ const instancesSlice = createSlice({ state.instances[activeIndex].frequentEmojis = state.instances[ activeIndex ].frequentEmojis?.filter(emoji => { - return action.payload.find( + return action.payload?.find( e => e.shortcode === emoji.emoji.shortcode && e.url === emoji.emoji.url ) @@ -381,6 +381,43 @@ export const getInstanceConfigurationStatusCharsURL = ({ instances[findInstanceActive(instances)]?.configuration?.statuses .characters_reserved_per_url || 23 +export const getInstanceConfigurationMediaAttachments = ({ + instances: { instances } +}: RootState) => + instances[findInstanceActive(instances)]?.configuration + ?.media_attachments || { + supported_mime_types: [ + 'image/jpeg', + 'image/png', + 'image/gif', + 'video/webm', + 'video/mp4', + 'video/quicktime', + 'video/ogg', + 'audio/wave', + 'audio/wav', + 'audio/x-wav', + 'audio/x-pn-wave', + 'audio/ogg', + 'audio/vorbis', + 'audio/mpeg', + 'audio/mp3', + 'audio/webm', + 'audio/flac', + 'audio/aac', + 'audio/m4a', + 'audio/x-m4a', + 'audio/mp4', + 'audio/3gpp', + 'video/x-ms-asf' + ], + image_size_limit: 10485760, + image_matrix_limit: 16777216, + video_size_limit: 41943040, + video_frame_rate_limit: 60, + video_matrix_limit: 2304000 + } + export const getInstanceConfigurationPoll = ({ instances: { instances } }: RootState) => diff --git a/yarn.lock b/yarn.lock index 54a33b2c..4a445a9c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4428,6 +4428,13 @@ expo-image-loader@~3.2.0: resolved "https://registry.yarnpkg.com/expo-image-loader/-/expo-image-loader-3.2.0.tgz#d98b021660edef7243f7c5ec011b8d0545626d41" integrity sha512-LU3Q2prn64/HxdToDmxgMIRXS1ZvD9Q3iCxRVTZn1fPQNNDciIQFE5okaa74Ogx20DFHs90r6WoUd7w9Af1OGQ== +expo-image-manipulator@^10.3.1: + version "10.3.1" + resolved "https://registry.yarnpkg.com/expo-image-manipulator/-/expo-image-manipulator-10.3.1.tgz#e16dd76a550c7f5d653a2a666f26429eba311a6b" + integrity sha512-D08dMD6MerxBu23DmCIhurySQih+eLP7VxKvY5mWqYz9payjDOS1cAGs3HvXPrEDusPQFQ0uIfqc+oqeMNFBIA== + dependencies: + expo-image-loader "~3.2.0" + expo-image-picker@13.1.1: version "13.1.1" resolved "https://registry.yarnpkg.com/expo-image-picker/-/expo-image-picker-13.1.1.tgz#e039bf9748ccb7b89370ff2969c3ef07cc949192"