diff --git a/README.md b/README.md index 990a9395..fb8dc4df 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Please **do not** create a pull request to update translation. tooot's translati ## Special thanks +- [@a_mento](https://crowdin.com/profile/a_mento) for Basques translation - [@dzmitry.zubialevich](https://crowdin.com/profile/dzmitry.zubialevich) for Belarusian translation - [@amrtf](https://crowdin.com/profile/amrtf) for Catalan and Spanish translation - [@forenta](https://github.com/forenta) for German translation @@ -20,6 +21,7 @@ Please **do not** create a pull request to update translation. tooot's translati - [@hellojaccc](https://github.com/hellojaccc) for Korean translation - [@jan-vandenberg](https://crowdin.com/profile/jan-vandenberg) for Dutch translation - [@gaute](https://gauteweb.net/) for Norwegian translation +- [@MStankiewiczOfficial](https://crowdin.com/profile/MStankiewiczOfficial) for Polish translation - [@luizpicolo](https://github.com/luizpicolo) for Brazilian Portuguese - [@janlindblom](https://github.com/janlindblom) for Swedish - [@ihoryan](https://crowdin.com/profile/ihoryan) for Ukrainian diff --git a/fastlane/metadata/en-US/release_notes.txt b/fastlane/metadata/en-US/release_notes.txt index da253fd4..bf034b53 100644 --- a/fastlane/metadata/en-US/release_notes.txt +++ b/fastlane/metadata/en-US/release_notes.txt @@ -1,2 +1,3 @@ Enjoy toooting! This version includes following improvements and fixes: -- Added Belarusian language \ No newline at end of file +- Added Basque language +- Added Polish language \ No newline at end of file diff --git a/fastlane/metadata/zh-Hans/release_notes.txt b/fastlane/metadata/zh-Hans/release_notes.txt index 81692af7..40cc3d46 100644 --- a/fastlane/metadata/zh-Hans/release_notes.txt +++ b/fastlane/metadata/zh-Hans/release_notes.txt @@ -1,2 +1,4 @@ toooting愉快!此版本包括以下改进和修复: -- 新增白俄罗斯语 \ No newline at end of file +- 添加neodb.social书影音展示卡片 +- 新增巴斯克语 +- 新增波兰语 \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 2a67dd4d..72afa9af 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -8,15 +8,13 @@ PODS: - ReactCommon/turbomodule/core - EXConstants (14.2.1): - ExpoModulesCore - - EXErrorRecovery (4.1.1): - - ExpoModulesCore - EXFileSystem (15.2.2): - ExpoModulesCore - EXFont (11.1.1): - ExpoModulesCore - EXNotifications (0.18.1): - ExpoModulesCore - - Expo (48.0.0-beta.2): + - Expo (48.0.7): - ExpoModulesCore - ExpoCrypto (12.2.1): - ExpoModulesCore @@ -32,7 +30,7 @@ PODS: - ExpoModulesCore - ExpoLocalization (14.1.1): - ExpoModulesCore - - ExpoModulesCore (1.2.1): + - ExpoModulesCore (1.2.5): - React-Core - React-RCTAppDelegate - ReactCommon/turbomodule/core @@ -340,9 +338,9 @@ PODS: - glog - react-native-blur (4.3.0): - React-Core - - react-native-cameraroll (5.3.0): + - react-native-cameraroll (5.3.1): - React-Core - - react-native-image-picker (5.1.0): + - react-native-image-picker (5.3.1): - React-Core - react-native-ios-context-menu (1.15.3): - React-Core @@ -488,9 +486,9 @@ PODS: - RNScreens (3.20.0): - React-Core - React-RCTImage - - RNSentry (5.1.0): + - RNSentry (5.1.1): - React-Core - - Sentry/HybridSDK (= 8.2.0) + - Sentry/HybridSDK (= 8.3.0) - RNShareMenu (6.0.0): - React - RNSVG (13.8.0): @@ -506,9 +504,9 @@ PODS: - SDWebImageWebPCoder (0.9.1): - libwebp (~> 1.0) - SDWebImage/Core (~> 5.13) - - Sentry/HybridSDK (8.2.0): - - SentryPrivate (= 8.2.0) - - SentryPrivate (8.2.0) + - Sentry/HybridSDK (8.3.0): + - SentryPrivate (= 8.3.0) + - SentryPrivate (8.3.0) - Swime (3.0.6) - Yoga (1.14.0) @@ -518,7 +516,6 @@ DEPENDENCIES: - EXApplication (from `../node_modules/expo-application/ios`) - EXAV (from `../node_modules/expo-av/ios`) - EXConstants (from `../node_modules/expo-constants/ios`) - - EXErrorRecovery (from `../node_modules/expo-error-recovery/ios`) - EXFileSystem (from `../node_modules/expo-file-system/ios`) - EXFont (from `../node_modules/expo-font/ios`) - EXNotifications (from `../node_modules/expo-notifications/ios`) @@ -620,8 +617,6 @@ EXTERNAL SOURCES: :path: "../node_modules/expo-av/ios" EXConstants: :path: "../node_modules/expo-constants/ios" - EXErrorRecovery: - :path: "../node_modules/expo-error-recovery/ios" EXFileSystem: :path: "../node_modules/expo-file-system/ios" EXFont: @@ -767,17 +762,16 @@ SPEC CHECKSUMS: EXApplication: d8f53a7eee90a870a75656280e8d4b85726ea903 EXAV: f1f69397ecdcf44cfacd4ff5d338cd1b96891e87 EXConstants: f348da07e21b23d2b085e270d7b74f282df1a7d9 - EXErrorRecovery: ebb57ae947ff94667f1cbc12f403bb5a043d734d EXFileSystem: 844e86ca9b5375486ecc4ef06d3838d5597d895d EXFont: 6ea3800df746be7233208d80fe379b8ed74f4272 EXNotifications: dd628737af60fc8cc62dccebacd326b0fbbc0dcb - Expo: 1b7b4ec09bd939db6d98985231a0789aa3f6670a + Expo: 707f9b0039eacc6a1dce90c08c9e37b9c417bba2 ExpoCrypto: 477dfe89c81527b376f2c344ca1d2a01244b243c ExpoHaptics: 5156bc5160d8e04c170dd6e645a71154951a2ad9 ExpoImage: b6a65c4aa891cdf00bfba0da46df14b27ae09cc7 ExpoKeepAwake: 69f5f627670d62318410392d03e0b5db0f85759a ExpoLocalization: f26cd431ad9ea3533c5b08c4fabd879176a794bb - ExpoModulesCore: 2f4bd2ae0cd03d30c3c286f5d843e22f72ccdb55 + ExpoModulesCore: 397fc99e9d6c9dcc010f36d5802097c17b90424c ExpoStoreReview: d057dcca4b9c95f3c9db11bd2e168dab9cba59f3 ExpoVideoThumbnails: 0021303b614a89fcc5df8b59d9d37ddf14a7d4cf ExpoWebBrowser: 033d34c478d9986da2f1679729041423837626e0 @@ -812,8 +806,8 @@ SPEC CHECKSUMS: React-jsinspector: 1f51e775819199d3fe9410e69ee8d4c4161c7b06 React-logger: 0d58569ec51d30d1792c5e86a8e3b78d24b582c6 react-native-blur: 50c9feabacbc5f49b61337ebc32192c6be7ec3c3 - react-native-cameraroll: a05136a5e648c35f2e8cced939ba85966e3bba8e - react-native-image-picker: c33d4e79f0a14a2b66e5065e14946ae63749660b + react-native-cameraroll: f3050460fe1708378698c16686bfaa5f34099be2 + react-native-image-picker: ec9b713e248760bfa0f879f0715391de4651a7cb react-native-ios-context-menu: e529171ba760a1af7f2ef0729f5a7f4d226171c5 react-native-language-detection: f414937fa715108ab50a6269a3de0bcb95e4ceb0 react-native-mmkv: a2a40a0458bdbc9d43c4e7752ecfc5e3a87b66dd @@ -841,15 +835,15 @@ SPEC CHECKSUMS: RNGestureHandler: 071d7a9ad81e8b83fe7663b303d132406a7d8f39 RNReanimated: f0dd6b881808e635ef0673f89642937d6c141314 RNScreens: 218801c16a2782546d30bd2026bb625c0302d70f - RNSentry: bf01c99057573813a9b965d213801bac4c6a661b + RNSentry: 43658c8c327376e0c06149ce981899f5f84e90d9 RNShareMenu: cb9dac548c8bf147d06f0bf07296ad51ea9f5fc3 RNSVG: c1e76b81c76cdcd34b4e1188852892dc280eb902 SDWebImage: fd7e1a22f00303e058058278639bf6196ee431fe SDWebImageAVIFCoder: d759e21cf4efb640cc97250566aa556ad8bb877c SDWebImageSVGCoder: 6fc109f9c2a82ab44510fff410b88b1a6c271ee8 SDWebImageWebPCoder: 18503de6621dd2c420d680e33d46bf8e1d5169b0 - Sentry: cf1d35c866266da58964fe7b62526bda93ffcb38 - SentryPrivate: 2909bcc7b19a827b49e9bde0e56116b08d40dfdf + Sentry: 757565eb01e2a6ef6b26e897e4e47e8213e12f06 + SentryPrivate: 668d6ce46835769b32e61dc8b5c78ef0b6cdcef8 Swime: d7b2c277503b6cea317774aedc2dce05613f8b0b Yoga: 79dd7410de6f8ad73a77c868d3d368843f0c93e0 diff --git a/ios/eu.lproj/InfoPlist.strings b/ios/eu.lproj/InfoPlist.strings new file mode 100644 index 00000000..3411b8fb --- /dev/null +++ b/ios/eu.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +"NSPhotoLibraryAddUsageDescription" = "Baimendu tooot-i irudiak zure kameraren rollean gordetzeko"; +"NSPhotoLibraryUsageDescription" = "Baimendu tooot-i irudiak zure kameraren rollean gordetzeko"; diff --git a/ios/pl.lproj/InfoPlist.strings b/ios/pl.lproj/InfoPlist.strings new file mode 100644 index 00000000..72428324 --- /dev/null +++ b/ios/pl.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +"NSPhotoLibraryAddUsageDescription" = "Zezwól toootowi na zapisywanie zdjęć w rolce z aparatu"; +"NSPhotoLibraryUsageDescription" = "Zezwól toootowi na zapisywanie zdjęć w rolce z aparatu"; diff --git a/ios/tooot.xcodeproj/project.pbxproj b/ios/tooot.xcodeproj/project.pbxproj index c7998199..ee7c7442 100644 --- a/ios/tooot.xcodeproj/project.pbxproj +++ b/ios/tooot.xcodeproj/project.pbxproj @@ -79,6 +79,7 @@ E63E7FF0292A828100C76FD4 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/InfoPlist.strings; sourceTree = ""; }; E66C0842291F095800DFFF60 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/InfoPlist.strings; sourceTree = ""; }; E671BDF8290EAFB800287BD0 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/InfoPlist.strings"; sourceTree = ""; }; + E690907B29C1133000489554 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/InfoPlist.strings; sourceTree = ""; }; E690AF692926B737002C38A8 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = ""; }; E69EBACA28DF282D0057EDEC /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = ""; }; E69EBACB28DF283A0057EDEC /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -86,6 +87,7 @@ E69EBACD28DF284D0057EDEC /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/InfoPlist.strings"; sourceTree = ""; }; E69EBACE28DF28560057EDEC /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/InfoPlist.strings; sourceTree = ""; }; E6A4895D293C1F740047951A /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/InfoPlist.strings; sourceTree = ""; }; + E6B76A1E29C1147B00187ABB /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = ""; }; E6C8B26628F5F9FC0062CF2E /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = ""; }; E6D64C7A294A90840098F3AC /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/InfoPlist.strings; sourceTree = ""; }; E6FD3AA7299EE8A900774C18 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -304,6 +306,8 @@ uk, nb, be, + eu, + pl, ); mainGroup = 83CBB9F61A601CBA00E9B192; productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; @@ -539,6 +543,8 @@ E6D64C7A294A90840098F3AC /* uk */, E6FD3AA7299EE8A900774C18 /* nb */, E6179D6E29B94551001930D5 /* be */, + E690907B29C1133000489554 /* eu */, + E6B76A1E29C1147B00187ABB /* pl */, ); name = InfoPlist.strings; sourceTree = ""; diff --git a/package.json b/package.json index a8e87ab3..1a919d89 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tooot", - "version": "4.9.1", + "version": "4.9.2", "description": "tooot for Mastodon", "author": "xmflsct ", "license": "GPL-3.0-or-later", @@ -28,7 +28,7 @@ "@mattermost/react-native-paste-input": "^0.6.2", "@neverdull-agency/expo-unlimited-secure-store": "^1.0.10", "@react-native-async-storage/async-storage": "~1.17.11", - "@react-native-camera-roll/camera-roll": "^5.3.0", + "@react-native-camera-roll/camera-roll": "^5.3.1", "@react-native-clipboard/clipboard": "^1.11.2", "@react-native-community/blur": "^4.3.0", "@react-native-community/netinfo": "9.3.7", @@ -39,12 +39,12 @@ "@react-navigation/native": "^6.1.6", "@react-navigation/native-stack": "^6.9.12", "@react-navigation/stack": "^6.3.16", - "@sentry/react-native": "5.1.0", + "@sentry/react-native": "5.1.1", "@sharcoux/slider": "^6.1.1", "@tanstack/react-query": "^4.26.1", "axios": "^1.3.4", "diff": "^5.1.0", - "expo": "48.0.0-beta.2", + "expo": "48.0.7", "expo-auth-session": "^4.0.3", "expo-av": "^13.2.1", "expo-constants": "^14.2.1", @@ -73,7 +73,7 @@ "react-native": "^0.71.4", "react-native-flash-message": "^0.4.0", "react-native-gesture-handler": "~2.9.0", - "react-native-image-picker": "^5.1.0", + "react-native-image-picker": "^5.3.1", "react-native-ios-context-menu": "^1.15.3", "react-native-language-detection": "^0.2.2", "react-native-mmkv": "~2.7.0", @@ -88,11 +88,10 @@ "react-native-swipe-list-view": "^3.2.9", "react-native-tab-view": "^3.5.1", "rn-placeholder": "^3.0.3", - "url-parse": "^1.5.10", - "zeego": "^1.3.1" + "zeego": "^1.4.1" }, "devDependencies": { - "@babel/core": "^7.21.0", + "@babel/core": "^7.21.3", "@babel/plugin-proposal-optional-chaining": "^7.21.0", "@babel/preset-typescript": "^7.21.0", "@expo/config": "^8.0.2", @@ -102,7 +101,6 @@ "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", "@types/react-native-share-menu": "^5.0.2", - "@types/url-parse": "^1.4.8", "babel-plugin-module-resolver": "^5.0.0", "babel-plugin-transform-remove-console": "^6.9.4", "chalk": "^4.1.2", diff --git a/src/components/GracefullyImage.tsx b/src/components/GracefullyImage.tsx index f4b4ca49..5f6b76c5 100644 --- a/src/components/GracefullyImage.tsx +++ b/src/components/GracefullyImage.tsx @@ -1,7 +1,7 @@ import { useAccessibility } from '@utils/accessibility/AccessibilityManager' import { connectMedia } from '@utils/api/helpers/connect' import { useTheme } from '@utils/styles/ThemeManager' -import { Image, ImageSource, ImageStyle } from 'expo-image' +import { Image, ImageContentFit, ImageSource, ImageStyle } from 'expo-image' import React, { useState } from 'react' import { AccessibilityProps, Pressable, StyleProp, View, ViewStyle } from 'react-native' @@ -21,6 +21,7 @@ export interface Props { onPress?: () => void style?: StyleProp imageStyle?: ImageStyle + contentFit?: ImageContentFit dim?: boolean withoutTransition?: boolean @@ -36,6 +37,7 @@ const GracefullyImage = ({ onPress, style, imageStyle, + contentFit, dim, withoutTransition = false, enableLiveTextInteraction = false @@ -63,6 +65,7 @@ const GracefullyImage = ({ source={hidden ? sources.blurhash : connectMedia(source)} {...(!withoutTransition && !reduceMotionEnabled && { transition: { duration: 120 } })} style={{ flex: 1, ...imageStyle }} + contentFit={contentFit} onError={() => { if ( sources.default?.uri && diff --git a/src/components/Instance/index.tsx b/src/components/Instance/index.tsx index ef24f068..5ea043cd 100644 --- a/src/components/Instance/index.tsx +++ b/src/components/Instance/index.tsx @@ -21,6 +21,7 @@ import { useTheme } from '@utils/styles/ThemeManager' import * as AuthSession from 'expo-auth-session' import * as Crypto from 'expo-crypto' import { Image } from 'expo-image' +import * as Linking from 'expo-linking' import * as WebBrowser from 'expo-web-browser' import { debounce } from 'lodash' import React, { RefObject, useCallback, useState } from 'react' @@ -28,7 +29,6 @@ import { Trans, useTranslation } from 'react-i18next' import { Alert, KeyboardAvoidingView, Platform, TextInput, View } from 'react-native' import { ScrollView } from 'react-native-gesture-handler' import { fromByteArray } from 'react-native-quick-base64' -import parse from 'url-parse' import CustomText from '../Text' export interface Props { @@ -51,7 +51,7 @@ const ComponentInstance: React.FC = ({ const whitelisted: boolean = !!domain.length && !!errorCode && - !!(parse(`https://${domain}/`).hostname === domain) && + !!(Linking.parse(`https://${domain}/`).hostname === domain) && errorCode === 401 const instanceQuery = useInstanceQuery({ @@ -129,7 +129,7 @@ const ComponentInstance: React.FC = ({ (instanceQuery.data as Mastodon.Instance_V2)?.domain || instanceQuery.data?.account_domain || ((instanceQuery.data as Mastodon.Instance_V1)?.uri - ? parse((instanceQuery.data as Mastodon.Instance_V1).uri).hostname + ? Linking.parse((instanceQuery.data as Mastodon.Instance_V1).uri).hostname : undefined) || (instanceQuery.data as Mastodon.Instance_V1)?.uri, 'auth.account.avatar_static': avatar_static, diff --git a/src/components/Timeline/Shared/Card/Neodb.tsx b/src/components/Timeline/Shared/Card/Neodb.tsx new file mode 100644 index 00000000..f412746b --- /dev/null +++ b/src/components/Timeline/Shared/Card/Neodb.tsx @@ -0,0 +1,148 @@ +import GracefullyImage from '@components/GracefullyImage' +import openLink from '@components/openLink' +import CustomText from '@components/Text' +import { useNeodbQuery } from '@utils/queryHooks/neodb' +import { StyleConstants } from '@utils/styles/constants' +import { useTheme } from '@utils/styles/ThemeManager' +import * as Linking from 'expo-linking' +import { useState } from 'react' +import { Pressable, View } from 'react-native' +import { Rating } from './Rating' + +export type Props = { + card: Mastodon.Card +} + +export const CardNeodb: React.FC = ({ card }) => { + const { colors } = useTheme() + + const segments = Linking.parse(card.url).path?.split('/') + if (!segments || !(segments[0] === 'movie' || segments[0] === 'book' || segments[0] === 'tv')) + return null + + const { data } = useNeodbQuery({ path: `${segments[0]}/${segments[1]}` }) + + if (!data) return null + + const pressableProps = { + style: { + marginTop: StyleConstants.Spacing.M, + backgroundColor: colors.shimmerDefault, + borderRadius: StyleConstants.BorderRadius, + padding: StyleConstants.Spacing.S, + flexDirection: 'row' as 'row' + }, + onPress: () => openLink(card.url) + } + const contentProps = { style: { flex: 1, gap: StyleConstants.Spacing.S } } + + const [headingLines, setHeadingLines] = useState(3) + + const itemImage = data.cover_image_url ? ( + + ) : null + const itemHeading = (value: string) => ( + setHeadingLines(nativeEvent.lines.length)} + /> + ) + const itemDetails = (value: string) => ( + + ) + + switch (segments[0]) { + case 'movie': + return ( + + {itemImage} + + {itemHeading( + [data.title, data.orig_title, data.year ? `(${data.year})` : null] + .filter(d => d) + .join(' ') + )} + + {itemDetails( + [ + data.duration + ? parseInt(data.duration).toString() === data.duration + ? `${data.duration}分钟` + : data.duration + : null, + data.area?.join(' '), + data.genre?.join(' '), + data.director?.join(' ') + ] + .filter(d => d) + .join(' / ') + )} + + + ) + case 'book': + return ( + + {itemImage} + + {itemHeading(data.title)} + + {itemDetails( + [ + data.author?.join(' '), + data.pages ? `${data.pages}页` : null, + data.language, + data.pub_house + ] + .filter(d => d) + .join(' / ') + )} + + + ) + case 'tv': + return ( + + {itemImage} + + {itemHeading( + [data.title, data.orig_title, data.year ? `(${data.year})` : null] + .filter(d => d) + .join(' ') + )} + + {itemDetails( + [ + data.season_count ? `共${data.season_count}季` : null, + data.area?.join(' '), + data.genre?.join(' '), + data.director?.join(' ') + ] + .filter(d => d) + .join(' / ') + )} + + + ) + default: + return null + } +} diff --git a/src/components/Timeline/Shared/Card/Rating.tsx b/src/components/Timeline/Shared/Card/Rating.tsx new file mode 100644 index 00000000..8fa0901b --- /dev/null +++ b/src/components/Timeline/Shared/Card/Rating.tsx @@ -0,0 +1,57 @@ +import { StyleConstants } from '@utils/styles/constants' +import { View } from 'react-native' +import { Star } from './Star' + +interface StarRatingProps { + rating?: number + + unit?: 'full' | 'half' | 'float' + size?: number + count?: number + roundedCorner?: boolean +} + +const starUnitMap = { + full: 100, + half: 50, + float: 10 +} +export const Rating: React.FC = ({ + rating, + size = StyleConstants.Font.Size.M, + count = 5, + roundedCorner = true, + unit = 'float' +}) => { + if (!rating) return null + + const unitValue = starUnitMap[unit] + + const getSelectedOffsetPercent = (starIndex: number) => { + const roundedSelectedValue = Math.floor(rating) + if (starIndex < roundedSelectedValue) { + return 100 + } else if (starIndex > roundedSelectedValue) { + return 0 + } else { + const currentStarOffsetPercentage = (rating % 1) * 100 + return Math.ceil(currentStarOffsetPercentage / unitValue) * unitValue + } + } + + return ( + + {Array.from({ length: count }, (v, i) => { + return ( + + ) + })} + + ) +} diff --git a/src/components/Timeline/Shared/Card/Star.tsx b/src/components/Timeline/Shared/Card/Star.tsx new file mode 100644 index 00000000..679e3aad --- /dev/null +++ b/src/components/Timeline/Shared/Card/Star.tsx @@ -0,0 +1,48 @@ +import { useTheme } from '@utils/styles/ThemeManager' +import { uniqueId } from 'lodash' +import { Defs, LinearGradient, Path, Stop, Svg } from 'react-native-svg' + +interface StarProps { + size: number + strokeLinejoin: 'miter' | 'round' + strokeLinecap: 'butt' | 'round' + offset: number +} + +const NUM_POINT = 5 +export const Star: React.FC = ({ size, strokeLinejoin, strokeLinecap, offset }) => { + const { colors } = useTheme() + + const innerRadius = 25 + const outerRadius = 50 + + const id = uniqueId() + + const center = Math.max(innerRadius, outerRadius) + const angle = Math.PI / NUM_POINT + const points = [] + + for (let i = 0; i < NUM_POINT * 2; i++) { + let radius = i % 2 === 0 ? outerRadius : innerRadius + points.push(center + radius * Math.sin(i * angle)) + points.push(center - radius * Math.cos(i * angle)) + } + + return ( + + + + + + + + + + + ) +} diff --git a/src/components/Timeline/Shared/Card.tsx b/src/components/Timeline/Shared/Card/index.tsx similarity index 93% rename from src/components/Timeline/Shared/Card.tsx rename to src/components/Timeline/Shared/Card/index.tsx index d77ae525..6d6ac8e5 100644 --- a/src/components/Timeline/Shared/Card.tsx +++ b/src/components/Timeline/Shared/Card/index.tsx @@ -11,14 +11,24 @@ import { useStatusQuery } from '@utils/queryHooks/status' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import React, { useContext, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' import { Pressable, View } from 'react-native' -import TimelineDefault from '../Default' -import StatusContext from './Context' +import TimelineDefault from '../../Default' +import StatusContext from '../Context' +import { CardNeodb } from './Neodb' const TimelineCard: React.FC = () => { const { status, spoilerHidden, disableDetails, inThread } = useContext(StatusContext) if (!status || !status.card) return null + const { i18n } = useTranslation() + if ( + status.card.url.includes('://neodb.social/') && + i18n.language.toLowerCase().startsWith('zh-hans') + ) { + return + } + const { colors } = useTheme() const navigation = useNavigation>() diff --git a/src/components/contextMenu/instance.ts b/src/components/contextMenu/instance.ts index 7b0a2af8..418b6c80 100644 --- a/src/components/contextMenu/instance.ts +++ b/src/components/contextMenu/instance.ts @@ -2,9 +2,9 @@ import { displayMessage } from '@components/Message' import { useQueryClient } from '@tanstack/react-query' import { QueryKeyTimeline, useTimelineMutation } from '@utils/queryHooks/timeline' import { getAccountStorage } from '@utils/storage/actions' +import * as Linking from 'expo-linking' import { useTranslation } from 'react-i18next' import { Alert } from 'react-native' -import parse from 'url-parse' const menuInstance = ({ status, @@ -32,9 +32,9 @@ const menuInstance = ({ const menus: ContextMenu = [] - const instance = parse(status.uri).hostname + const instance = Linking.parse(status.uri).hostname - if (instance !== getAccountStorage.string('auth.domain')) { + if (instance && instance !== getAccountStorage.string('auth.domain')) { menus.push([ { type: 'item', diff --git a/src/i18n/be/screens/tabs.json b/src/i18n/be/screens/tabs.json index c0b6020a..7df2598c 100644 --- a/src/i18n/be/screens/tabs.json +++ b/src/i18n/be/screens/tabs.json @@ -226,7 +226,7 @@ "profile": { "feedback": { "succeed": "{{type}} абноўлена", - "failed": "" + "failed": "Збой абнаўлення {{type}}, паспрабуйце яшчэ раз" }, "root": { "name": { @@ -280,7 +280,7 @@ }, "decode": { "heading": "Паказаць дэталі паведамлення", - "description": "" + "description": "Паведамленні, якія перадаюцца праз сервер tooot, зашыфраваныя, але вы можаце ўключыць расшыфроўку паведамленняў на серверы. Зыходны код нашага сервера адкрыты і даступны для ўсіх. Таксама наш сервер не захоўвае вашы даныя." }, "default": { "heading": "Прадвызначана" @@ -386,10 +386,10 @@ "shared": { "account": { "actions": { - "accessibilityLabel": "", + "accessibilityLabel": "Дзеянні для карыстальніка {{user}}", "accessibilityHint": "" }, - "followed_by": "", + "followed_by": " падпісаны на вас", "privateNote": "", "moved": "", "created_at": "", @@ -418,7 +418,7 @@ "name": "Гісторыя рэдагавання" }, "report": { - "name": "", + "name": "Паскардзіцца на {{acct}}", "report": "Скарга", "forward": { "heading": "Ананімна пераслаць на аддалены сервер {{instance}}" @@ -473,12 +473,12 @@ }, "users": { "accounts": { - "following": "", - "followers": "" + "following": "Падпіскі {{count}}", + "followers": "{{count}} падпісчыкаў" }, "statuses": { - "reblogged_by": "", - "favourited_by": "" + "reblogged_by": "{{count}} пашырэнняў", + "favourited_by": "{{count}} дадалі ў абранае" }, "resultIncomplete": "Вынікі з аддаленага інстанса няпоўныя" } diff --git a/src/i18n/eu/components/contextMenu.json b/src/i18n/eu/components/contextMenu.json index 9b9ac2bf..4e27c3cb 100644 --- a/src/i18n/eu/components/contextMenu.json +++ b/src/i18n/eu/components/contextMenu.json @@ -80,23 +80,23 @@ "action": "Ezabatu tuta", "alert": { "title": "Ezabaketa berretsi?", - "message": "" + "message": "Bultzada eta gogoko guztiak ezabatuko dira, erantzun guztiak barne." } }, "deleteEdit": { - "action": "", + "action": "Ezabatu tuta eta berrargitaratu", "alert": { - "title": "", - "message": "" + "title": "Ezabatu eta berrargitaratzea nahi duzu?", + "message": "Bultzada eta gogoko guztiak ezabatuko dira, erantzun guztiak barne." } }, "mute": { - "action_false": "", - "action_true": "" + "action_false": "Mututu tuta eta erantzunak", + "action_true": "Desmututu tuta eta erantzunak" }, "pin": { - "action_false": "", - "action_true": "" + "action_false": "Finkatu tuta", + "action_true": "Desfinkatu tuta" }, "filter": { "action_false": "Tuta iragazi...", diff --git a/src/i18n/eu/index.ts b/src/i18n/eu/index.ts new file mode 100644 index 00000000..61e0f840 --- /dev/null +++ b/src/i18n/eu/index.ts @@ -0,0 +1,17 @@ +export default { + common: require('./common'), + + screens: require('./screens'), + screenAnnouncements: require('./screens/announcements'), + screenCompose: require('./screens/compose'), + screenImageViewer: require('./screens/imageViewer'), + screenTabs: require('./screens/tabs'), + + componentContextMenu: require('./components/contextMenu'), + componentEmojis: require('./components/emojis'), + componentInstance: require('./components/instance'), + componentMediaSelector: require('./components/mediaSelector'), + componentParse: require('./components/parse'), + componentRelationship: require('./components/relationship'), + componentTimeline: require('./components/timeline') +} diff --git a/src/i18n/eu/screens/accountSelection.json b/src/i18n/eu/screens/accountSelection.json index b16795a6..3c1cc371 100644 --- a/src/i18n/eu/screens/accountSelection.json +++ b/src/i18n/eu/screens/accountSelection.json @@ -1,6 +1,6 @@ { - "heading": "", + "heading": "Hona partekatu ...", "content": { - "select_account": "" + "select_account": "Aukeratu kontua" } } \ No newline at end of file diff --git a/src/i18n/eu/screens/tabs.json b/src/i18n/eu/screens/tabs.json index 7dac2fab..2c3586fa 100644 --- a/src/i18n/eu/screens/tabs.json +++ b/src/i18n/eu/screens/tabs.json @@ -196,97 +196,97 @@ }, "preferencesFilter": { "name": "Izena", - "expiration": "", + "expiration": "Epemuga", "expirationOptions": { - "0": "", - "1800": "", - "3600": "", - "43200": "", - "86400": "", - "604800": "", - "18144000": "" + "0": "Inoiz", + "1800": "30 minuturen ondoren", + "3600": "Ordu baten ondoren", + "43200": "12 orduren ondoren", + "86400": "Egun baten ondoren", + "604800": "Aste baten ondoren", + "18144000": "Hilabete baten ondoren" }, - "context": "", + "context": "Aplikatzen da", "contexts": { - "home": "", - "notifications": "", - "public": "", - "thread": "", - "account": "" + "home": "Jarraitutakoak eta zerrendak", + "notifications": "Jakinarazpenak", + "public": "Denbora-lerro federatua", + "thread": "Elkarrizketaren bista", + "account": "Profilaren bista" }, - "action": "", + "action": "Bat etortzean", "actions": { - "warn": "", - "hide": "" + "warn": "Ezkutuan baina erakuts daiteke", + "hide": "Guztiz ezkutatua" }, - "keywords": "", - "keyword": "", - "statuses": "" + "keywords": "Hitz-gako hauekin bat etortzean", + "keyword": "Hitz-gako", + "statuses": "Tut hauekin bat etortzean" }, "profile": { "feedback": { - "succeed": "", - "failed": "" + "succeed": "{{type}} eguneratua", + "failed": "{{type}}(r)en eguneraketak huts egin du, saia zaitez berriro" }, "root": { "name": { - "title": "" + "title": "Bistaratutako izena" }, "avatar": { - "title": "", - "description": "" + "title": "Abatarra", + "description": "400x400px-etara eskalatuko da" }, "header": { - "title": "", - "description": "" + "title": "Goiburu-irudia", + "description": "1500x1500px-etara eskalatuko da" }, "note": { - "title": "" + "title": "Deskribapena" }, "fields": { - "title": "", - "total_one": "", - "total_other": "" + "title": "Metadatuak", + "total_one": "Eremu {{count}}", + "total_other": "{{count}} eremu" }, "lock": { - "title": "", - "description": "" + "title": "Kontua babestu", + "description": "Jarraitzaileak eskuz onartu beharko dituzu" }, "bot": { - "title": "", - "description": "" + "title": "Bot kontua", + "description": "Kontu hau, oro har, ekintza automatizatuak egiten ditu eta monitorizatu gabe egon liteke" } }, "fields": { - "group": "", - "label": "", - "content": "" + "group": "{{index}} taldea", + "label": "Etiketa", + "content": "Edukia" }, - "mediaSelectionFailed": "" + "mediaSelectionFailed": "Irudi-prozesatzeak huts egin du. Mesedez, saia zaitez berriro." }, "push": { - "notAvailable": "", + "notAvailable": "Zure telefonoa ez da toooten push jakinarazpenekin bateragarria", "enable": { - "direct": "", - "settings": "" + "direct": "Gaitu push jakinarazpenak", + "settings": "Gaitu ezarpenetan" }, "missingServerKey": { - "message": "", - "description": "" + "message": "Zerbitzaria oker konfiguratua push jakinarazpenetarako", + "description": "Mesedez, jar zaitez zerbitzariko administratzailearekin harremanetan push jakinarazpenen bateragarritasuna konfiguratzeko" }, "global": { - "heading": "", - "description": "" + "heading": "Gaitu {{acct}}-(e)rako", + "description": "Mezuak toooten zerbitzariaren bidez bidaltzen dira" }, "decode": { - "heading": "", - "description": "" + "heading": "Erakutsi mezuaren xehetasunak", + "description": "toooten zerbitzariaren bidez bidalitako mezuak enkriptatuak daude, baina, zerbitzarian desenkripta daitezen aukera dezakezu. Gure zerbitzariko iturburu-kodea irekia da, eta erregistro gabeko politika du." }, "default": { - "heading": "" + "heading": "Lehenetsia" }, "follow": { - "heading": "" + "heading": "Jarraitzaile berria" }, "follow_request": { "heading": "Jarraitzeko eskaera" diff --git a/src/i18n/index.ts b/src/i18n/index.ts index 4cb4b8a8..33efecba 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -7,12 +7,14 @@ import ca from './ca' import de from './de' import en from './en' import es from './es' +import eu from './eu' import fr from './fr' import it from './it' import ja from './ja' import ko from './ko' import nl from './nl' import no from './no' +import pl from './pl' import pt_BR from './pt_BR' import sv from './sv' import uk from './uk' @@ -29,12 +31,14 @@ import '@formatjs/intl-pluralrules/locale-data/ca' import '@formatjs/intl-pluralrules/locale-data/de' import '@formatjs/intl-pluralrules/locale-data/en' import '@formatjs/intl-pluralrules/locale-data/es' +import '@formatjs/intl-pluralrules/locale-data/eu' import '@formatjs/intl-pluralrules/locale-data/fr' import '@formatjs/intl-pluralrules/locale-data/it' import '@formatjs/intl-pluralrules/locale-data/ja' import '@formatjs/intl-pluralrules/locale-data/ko' import '@formatjs/intl-pluralrules/locale-data/nl' import '@formatjs/intl-pluralrules/locale-data/no' +import '@formatjs/intl-pluralrules/locale-data/pl' import '@formatjs/intl-pluralrules/locale-data/pt' import '@formatjs/intl-pluralrules/locale-data/sv' import '@formatjs/intl-pluralrules/locale-data/uk' @@ -47,12 +51,14 @@ import '@formatjs/intl-numberformat/locale-data/ca' import '@formatjs/intl-numberformat/locale-data/de' import '@formatjs/intl-numberformat/locale-data/en' import '@formatjs/intl-numberformat/locale-data/es' +import '@formatjs/intl-numberformat/locale-data/eu' import '@formatjs/intl-numberformat/locale-data/fr' import '@formatjs/intl-numberformat/locale-data/it' import '@formatjs/intl-numberformat/locale-data/ja' import '@formatjs/intl-numberformat/locale-data/ko' import '@formatjs/intl-numberformat/locale-data/nl' import '@formatjs/intl-numberformat/locale-data/no' +import '@formatjs/intl-numberformat/locale-data/pl' import '@formatjs/intl-numberformat/locale-data/pt' import '@formatjs/intl-numberformat/locale-data/sv' import '@formatjs/intl-numberformat/locale-data/uk' @@ -67,12 +73,14 @@ import '@formatjs/intl-datetimeformat/locale-data/ca' import '@formatjs/intl-datetimeformat/locale-data/de' import '@formatjs/intl-datetimeformat/locale-data/en' import '@formatjs/intl-datetimeformat/locale-data/es' +import '@formatjs/intl-datetimeformat/locale-data/eu' import '@formatjs/intl-datetimeformat/locale-data/fr' import '@formatjs/intl-datetimeformat/locale-data/it' import '@formatjs/intl-datetimeformat/locale-data/ja' import '@formatjs/intl-datetimeformat/locale-data/ko' import '@formatjs/intl-datetimeformat/locale-data/nl' import '@formatjs/intl-datetimeformat/locale-data/no' +import '@formatjs/intl-datetimeformat/locale-data/pl' import '@formatjs/intl-datetimeformat/locale-data/pt' import '@formatjs/intl-datetimeformat/locale-data/sv' import '@formatjs/intl-datetimeformat/locale-data/uk' @@ -86,12 +94,14 @@ import '@formatjs/intl-relativetimeformat/locale-data/ca' import '@formatjs/intl-relativetimeformat/locale-data/de' import '@formatjs/intl-relativetimeformat/locale-data/en' import '@formatjs/intl-relativetimeformat/locale-data/es' +import '@formatjs/intl-relativetimeformat/locale-data/eu' import '@formatjs/intl-relativetimeformat/locale-data/fr' import '@formatjs/intl-relativetimeformat/locale-data/it' import '@formatjs/intl-relativetimeformat/locale-data/ja' import '@formatjs/intl-relativetimeformat/locale-data/ko' import '@formatjs/intl-relativetimeformat/locale-data/nl' import '@formatjs/intl-relativetimeformat/locale-data/no' +import '@formatjs/intl-relativetimeformat/locale-data/pl' import '@formatjs/intl-relativetimeformat/locale-data/pt' import '@formatjs/intl-relativetimeformat/locale-data/sv' import '@formatjs/intl-relativetimeformat/locale-data/uk' @@ -112,12 +122,14 @@ i18n.use(initReactI18next).init({ de, en, es, + eu, fr, it, ja, ko, nl, no, + pl, 'pt-BR': pt_BR, sv, uk, diff --git a/src/i18n/locales.ts b/src/i18n/locales.ts index be97db67..29b1cfc3 100644 --- a/src/i18n/locales.ts +++ b/src/i18n/locales.ts @@ -4,12 +4,14 @@ const LOCALES = { de: 'Deutsch', en: 'English', es: 'Español', + eu: 'Euskara', fr: 'Français', it: 'Italiano', ja: '日本語', ko: '한국어', nl: 'Nederlands', no: 'Norsk', + pl: 'Polski', 'pt-br': 'Português (Brasil)', sv: 'Svenska', uk: 'українська', diff --git a/src/i18n/pl/index.ts b/src/i18n/pl/index.ts new file mode 100644 index 00000000..61e0f840 --- /dev/null +++ b/src/i18n/pl/index.ts @@ -0,0 +1,17 @@ +export default { + common: require('./common'), + + screens: require('./screens'), + screenAnnouncements: require('./screens/announcements'), + screenCompose: require('./screens/compose'), + screenImageViewer: require('./screens/imageViewer'), + screenTabs: require('./screens/tabs'), + + componentContextMenu: require('./components/contextMenu'), + componentEmojis: require('./components/emojis'), + componentInstance: require('./components/instance'), + componentMediaSelector: require('./components/mediaSelector'), + componentParse: require('./components/parse'), + componentRelationship: require('./components/relationship'), + componentTimeline: require('./components/timeline') +} diff --git a/src/screens/ImageViewer/index.tsx b/src/screens/ImageViewer/index.tsx index 5438b16e..eaba4d9a 100644 --- a/src/screens/ImageViewer/index.tsx +++ b/src/screens/ImageViewer/index.tsx @@ -159,8 +159,8 @@ const ScreenImagesViewer = ({ }) => { const screenRatio = WINDOW_WIDTH / WINDOW_HEIGHT const imageRatio = item.width && item.height ? item.width / item.height : 1 - const imageWidth = item.width || 100 - const imageHeight = item.height || 100 + const imageWidth = item.width + const imageHeight = item.height const maxWidthScale = item.width ? (item.width / WINDOW_WIDTH / PixelRatio.get()) * 4 @@ -198,16 +198,21 @@ const ScreenImagesViewer = ({ default: { uri: item.url }, remote: { uri: item.remote_url } }} - dimension={{ - width: - screenRatio > imageRatio - ? (WINDOW_HEIGHT / imageHeight) * imageWidth - : WINDOW_WIDTH, - height: - screenRatio > imageRatio - ? WINDOW_HEIGHT - : (WINDOW_WIDTH / imageWidth) * imageHeight - }} + style={{ flex: 1 }} + contentFit='contain' + {...(imageWidth && + imageHeight && { + dimension: { + width: + screenRatio > imageRatio + ? (WINDOW_HEIGHT / imageHeight) * imageWidth + : WINDOW_WIDTH, + height: + screenRatio > imageRatio + ? WINDOW_HEIGHT + : (WINDOW_WIDTH / imageWidth) * imageHeight + } + })} enableLiveTextInteraction /> diff --git a/src/screens/Tabs/Me/FollowedTags.tsx b/src/screens/Tabs/Me/FollowedTags.tsx index 928a8d37..b55482b2 100644 --- a/src/screens/Tabs/Me/FollowedTags.tsx +++ b/src/screens/Tabs/Me/FollowedTags.tsx @@ -60,8 +60,8 @@ const TabMeFollowedTags: React.FC>