diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 5e6575f2..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "javascript.inlayHints.functionLikeReturnTypes.enabled": false -} \ No newline at end of file diff --git a/demo/screenshots/Component-Instance.png b/demo/screenshots/Component-Instance.png new file mode 100644 index 00000000..8b31c0a9 Binary files /dev/null and b/demo/screenshots/Component-Instance.png differ diff --git a/demo/screenshots/Component-MediaSelector.png b/demo/screenshots/Component-MediaSelector.png new file mode 100644 index 00000000..c47692d8 Binary files /dev/null and b/demo/screenshots/Component-MediaSelector.png differ diff --git a/demo/screenshots/Logout-Confirmation.png b/demo/screenshots/Logout-Confirmation.png new file mode 100644 index 00000000..330d6b04 Binary files /dev/null and b/demo/screenshots/Logout-Confirmation.png differ diff --git a/demo/screenshots/Screen-AccountSelection.png b/demo/screenshots/Screen-AccountSelection.png new file mode 100644 index 00000000..14509119 Binary files /dev/null and b/demo/screenshots/Screen-AccountSelection.png differ diff --git a/demo/screenshots/Tab-Local.png b/demo/screenshots/Tab-Local.png new file mode 100644 index 00000000..2ec6d60a Binary files /dev/null and b/demo/screenshots/Tab-Local.png differ diff --git a/demo/screenshots/Tab-Me-List-Edit.png b/demo/screenshots/Tab-Me-List-Edit.png new file mode 100644 index 00000000..02b20608 Binary files /dev/null and b/demo/screenshots/Tab-Me-List-Edit.png differ diff --git a/demo/screenshots/Tab-Me-ListAccounts_Empty.png b/demo/screenshots/Tab-Me-ListAccounts_Empty.png new file mode 100644 index 00000000..024c9e44 Binary files /dev/null and b/demo/screenshots/Tab-Me-ListAccounts_Empty.png differ diff --git a/demo/screenshots/Tab-Me-ListAccounts_Error.png b/demo/screenshots/Tab-Me-ListAccounts_Error.png new file mode 100644 index 00000000..b668b6a0 Binary files /dev/null and b/demo/screenshots/Tab-Me-ListAccounts_Error.png differ diff --git a/demo/screenshots/Tab-Me-List_Delete.png b/demo/screenshots/Tab-Me-List_Delete.png new file mode 100644 index 00000000..324421fd Binary files /dev/null and b/demo/screenshots/Tab-Me-List_Delete.png differ diff --git a/demo/screenshots/Tab-Me-List_Menu.png b/demo/screenshots/Tab-Me-List_Menu.png new file mode 100644 index 00000000..7c4706b0 Binary files /dev/null and b/demo/screenshots/Tab-Me-List_Menu.png differ diff --git a/demo/screenshots/Tab-Me-Profile-Fields.png b/demo/screenshots/Tab-Me-Profile-Fields.png new file mode 100644 index 00000000..d4bf5060 Binary files /dev/null and b/demo/screenshots/Tab-Me-Profile-Fields.png differ diff --git a/demo/screenshots/Tab-Me-Profile.png b/demo/screenshots/Tab-Me-Profile.png new file mode 100644 index 00000000..9690c9d8 Binary files /dev/null and b/demo/screenshots/Tab-Me-Profile.png differ diff --git a/demo/screenshots/Tab-Me-Profile_Feedback.png b/demo/screenshots/Tab-Me-Profile_Feedback.png new file mode 100644 index 00000000..1d965ac4 Binary files /dev/null and b/demo/screenshots/Tab-Me-Profile_Feedback.png differ diff --git a/demo/screenshots/Tab-Me-Push_Bottom.png b/demo/screenshots/Tab-Me-Push_Bottom.png new file mode 100644 index 00000000..de8166d3 Binary files /dev/null and b/demo/screenshots/Tab-Me-Push_Bottom.png differ diff --git a/demo/screenshots/Tab-Me-Push_MissingServerKey.png b/demo/screenshots/Tab-Me-Push_MissingServerKey.png new file mode 100644 index 00000000..60cea2ff Binary files /dev/null and b/demo/screenshots/Tab-Me-Push_MissingServerKey.png differ diff --git a/demo/screenshots/Tab-Me-Push_NotAvailable.png b/demo/screenshots/Tab-Me-Push_NotAvailable.png new file mode 100644 index 00000000..72a8e2f9 Binary files /dev/null and b/demo/screenshots/Tab-Me-Push_NotAvailable.png differ diff --git a/demo/screenshots/Tab-Me-Push_ReEnable.png b/demo/screenshots/Tab-Me-Push_ReEnable.png new file mode 100644 index 00000000..b3b69953 Binary files /dev/null and b/demo/screenshots/Tab-Me-Push_ReEnable.png differ diff --git a/demo/screenshots/Tab-Me-Push_Top.png b/demo/screenshots/Tab-Me-Push_Top.png new file mode 100644 index 00000000..a62fb995 Binary files /dev/null and b/demo/screenshots/Tab-Me-Push_Top.png differ diff --git a/demo/screenshots/Tab-Me-Settings-Appearance.png b/demo/screenshots/Tab-Me-Settings-Appearance.png new file mode 100644 index 00000000..c6723283 Binary files /dev/null and b/demo/screenshots/Tab-Me-Settings-Appearance.png differ diff --git a/demo/screenshots/Tab-Me-Settings-DarkTheme.png b/demo/screenshots/Tab-Me-Settings-DarkTheme.png new file mode 100644 index 00000000..e0f968f9 Binary files /dev/null and b/demo/screenshots/Tab-Me-Settings-DarkTheme.png differ diff --git a/demo/screenshots/Tab-Me-Settings-FontSize.png b/demo/screenshots/Tab-Me-Settings-FontSize.png new file mode 100644 index 00000000..af70d0c9 Binary files /dev/null and b/demo/screenshots/Tab-Me-Settings-FontSize.png differ diff --git a/demo/screenshots/Tab-Me-Settings-OpeningLink.png b/demo/screenshots/Tab-Me-Settings-OpeningLink.png new file mode 100644 index 00000000..d820a1be Binary files /dev/null and b/demo/screenshots/Tab-Me-Settings-OpeningLink.png differ diff --git a/demo/screenshots/Tab-Me-Settings.png b/demo/screenshots/Tab-Me-Settings.png new file mode 100644 index 00000000..efdd60fc Binary files /dev/null and b/demo/screenshots/Tab-Me-Settings.png differ diff --git a/demo/screenshots/Tab-Me-Switch.png b/demo/screenshots/Tab-Me-Switch.png new file mode 100644 index 00000000..0b1129e9 Binary files /dev/null and b/demo/screenshots/Tab-Me-Switch.png differ diff --git a/demo/screenshots/Tab-Me.png b/demo/screenshots/Tab-Me.png new file mode 100644 index 00000000..a601e90c Binary files /dev/null and b/demo/screenshots/Tab-Me.png differ diff --git a/demo/screenshots/Tab-Notifications-Filter.png b/demo/screenshots/Tab-Notifications-Filter.png new file mode 100644 index 00000000..e2f5daaf Binary files /dev/null and b/demo/screenshots/Tab-Notifications-Filter.png differ diff --git a/demo/screenshots/Tab-Notifications.png b/demo/screenshots/Tab-Notifications.png new file mode 100644 index 00000000..53707170 Binary files /dev/null and b/demo/screenshots/Tab-Notifications.png differ diff --git a/demo/screenshots/Tab-Public.png b/demo/screenshots/Tab-Public.png new file mode 100644 index 00000000..e6e3a0f0 Binary files /dev/null and b/demo/screenshots/Tab-Public.png differ diff --git a/demo/screenshots/Tab-Shared-Account.png b/demo/screenshots/Tab-Shared-Account.png new file mode 100644 index 00000000..a39301bf Binary files /dev/null and b/demo/screenshots/Tab-Shared-Account.png differ diff --git a/demo/screenshots/Tab-Shared-AccountInLists.png b/demo/screenshots/Tab-Shared-AccountInLists.png new file mode 100644 index 00000000..623915f6 Binary files /dev/null and b/demo/screenshots/Tab-Shared-AccountInLists.png differ diff --git a/demo/screenshots/Tab-Shared-Attachments.png b/demo/screenshots/Tab-Shared-Attachments.png new file mode 100644 index 00000000..cbc46c80 Binary files /dev/null and b/demo/screenshots/Tab-Shared-Attachments.png differ diff --git a/demo/screenshots/Tab-Shared-Hashtag.png b/demo/screenshots/Tab-Shared-Hashtag.png new file mode 100644 index 00000000..642cac97 Binary files /dev/null and b/demo/screenshots/Tab-Shared-Hashtag.png differ diff --git a/demo/screenshots/Tab-Shared-History.png b/demo/screenshots/Tab-Shared-History.png new file mode 100644 index 00000000..ef9a5ab2 Binary files /dev/null and b/demo/screenshots/Tab-Shared-History.png differ diff --git a/demo/screenshots/Tab-Shared-Search.png b/demo/screenshots/Tab-Shared-Search.png new file mode 100644 index 00000000..9fd18b89 Binary files /dev/null and b/demo/screenshots/Tab-Shared-Search.png differ diff --git a/demo/screenshots/Tab-Shared-Toot.png b/demo/screenshots/Tab-Shared-Toot.png new file mode 100644 index 00000000..148478ea Binary files /dev/null and b/demo/screenshots/Tab-Shared-Toot.png differ diff --git a/demo/statuses.ts b/demo/statuses.ts index 63d5e47a..1da8785c 100644 --- a/demo/statuses.ts +++ b/demo/statuses.ts @@ -1,6 +1,7 @@ -const demoStatuses = [ +const demoStatus: Mastodon.Status[] = [ { id: '1', + uri: 'https://example.com', created_at: new Date().toISOString(), sensitive: false, visibility: 'public', @@ -13,7 +14,6 @@ const demoStatuses = [ bookmarked: false, content: '

Would you like to try out this simple, beautiful and open-source mobile app for Mastodon? 😊

', - reblog: null, application: { name: 'tooot', website: 'https://tooot.app' @@ -23,19 +23,31 @@ const demoStatuses = [ username: 'tooot📱', acct: 'tooot@xmflsct.com', display_name: 'tooot📱', - avatar_static: - 'https://avatars.githubusercontent.com/u/77554750?s=200&v=4' + avatar: 'https://avatars.githubusercontent.com/u/77554750?s=200&v=4', + avatar_static: 'https://avatars.githubusercontent.com/u/77554750?s=200&v=4', + url: '', + header: '', + header_static: '', + locked: false, + discoverable: false, + created_at: new Date().toISOString(), + last_status_at: new Date().toISOString(), + statuses_count: 1, + followers_count: 1, + following_count: 1, + fields: [], + bot: false }, media_attachments: [], poll: { id: '1', - expires_at: new Date().setDate(new Date().getDate() + 5), + expires_at: new Date().setDate(new Date().getDate() + 5).toString(), expired: false, multiple: false, votes_count: 10, - voters_count: null, + voters_count: 2, voted: false, - own_votes: null, + own_votes: undefined, options: [ { title: 'I would love to!', @@ -48,11 +60,15 @@ const demoStatuses = [ ], emojis: [] }, - mentions: [] + mentions: [], + tags: [], + emojis: [], + pinned: false }, { id: '2', - created_at: new Date().setMinutes(new Date().getMinutes() - 2), + uri: 'https://example.com', + created_at: new Date().setMinutes(new Date().getMinutes() - 2).toString(), sensitive: false, spoiler_text: '', visibility: 'public', @@ -65,18 +81,26 @@ const demoStatuses = [ bookmarked: false, content: '

Mastodon is a free and open-source self-hosted social networking service. It allows anyone to host their own server node in the network, and its various separately operated user bases are federated across many different servers. These nodes are referred to as "instances" by Mastodon users.

', - reblog: null, - application: { - name: 'Web', - website: null - }, + application: { name: 'Web' }, account: { id: '1000', username: 'Mastodon', acct: 'mastodon', display_name: 'Mastodon', - avatar_static: - 'https://mastodon.social/apple-touch-icon.png' + avatar: 'https://mastodon.social/apple-touch-icon.png', + avatar_static: 'https://mastodon.social/apple-touch-icon.png', + url: '', + header: '', + header_static: '', + locked: false, + discoverable: false, + created_at: new Date().toISOString(), + last_status_at: new Date().toISOString(), + statuses_count: 1, + followers_count: 1, + following_count: 1, + fields: [], + bot: false }, media_attachments: [], card: { @@ -85,18 +109,31 @@ const demoStatuses = [ description: 'Mastodon is an open source decentralized social network - by the people for the people. Join the federation and take back control of your social media!', type: 'link', - image: - 'https://mastodon.social/apple-touch-icon.png' + image: 'https://mastodon.social/apple-touch-icon.png', + author_name: '', + author_url: '', + provider_name: '', + provider_url: '', + html: '

', + width: 100, + height: 100, + embed_url: 'https://example.com', + blurhash: '' }, - mentions: [] + mentions: [], + tags: [], + emojis: [], + pinned: false }, { id: '3', - created_at: '2021-01-24T09:50:00.901Z', + uri: '', + created_at: new Date().setHours(new Date().getHours() - 1).toString(), + sensitive: false, spoiler_text: '', visibility: 'public', replies_count: 2, - reblogs_count: null, + reblogs_count: 1, favourites_count: 3, favourited: false, reblogged: false, @@ -104,24 +141,38 @@ const demoStatuses = [ bookmarked: true, content: '

These servers are connected as a federated social network, allowing users from different servers to interact with each other seamlessly. Once a Mastodon server knows another Mastodon server, it "federates" with the other Mastodon server. Mastodon is a part of the wider Fediverse, allowing its users to also interact with users on different open platforms that support the same protocol, such as PeerTube and Friendica.

', - reblog: null, - application: { - name: 'Web', - website: null - }, + application: { name: 'Web' }, account: { id: '1001', username: 'Fediverse', acct: 'fediverse', display_name: 'Fediverse', + avatar: + 'https://e7.pngegg.com/pngimages/667/514/png-clipart-mastodon-fediverse-social-media-free-software-logo-social-media-blue-text.png', avatar_static: - 'https://e7.pngegg.com/pngimages/667/514/png-clipart-mastodon-fediverse-social-media-free-software-logo-social-media-blue-text.png' + 'https://e7.pngegg.com/pngimages/667/514/png-clipart-mastodon-fediverse-social-media-free-software-logo-social-media-blue-text.png', + url: '', + header: '', + header_static: '', + locked: false, + discoverable: false, + created_at: new Date().toISOString(), + last_status_at: new Date().toISOString(), + statuses_count: 1, + followers_count: 1, + following_count: 1, + fields: [], + bot: false }, media_attachments: [], - mentions: [] + mentions: [], + tags: [], + emojis: [], + pinned: false }, { id: '4', + uri: 'https://example.com', created_at: '2021-01-24T08:50:00.901Z', sensitive: false, visibility: 'public', @@ -134,7 +185,6 @@ const demoStatuses = [ bookmarked: false, content: '

tooot is an open source, simple mobile client for Mastodon. Focusing on your connections while being able to explore the Fediverse.

', - reblog: null, application: { name: 'tooot', website: 'https://tooot.app' @@ -144,14 +194,30 @@ const demoStatuses = [ username: 'tooot📱', acct: 'tooot@xmflsct.com', display_name: 'tooot📱', - avatar_static: - 'https://avatars.githubusercontent.com/u/77554750?s=200&v=4' + avatar: 'https://avatars.githubusercontent.com/u/77554750?s=200&v=4', + avatar_static: 'https://avatars.githubusercontent.com/u/77554750?s=200&v=4', + url: '', + header: '', + header_static: '', + locked: false, + discoverable: false, + created_at: new Date().toISOString(), + last_status_at: new Date().toISOString(), + statuses_count: 1, + followers_count: 1, + following_count: 1, + fields: [], + bot: false }, media_attachments: [], - mentions: [] + mentions: [], + tags: [], + emojis: [], + pinned: false }, { id: '5', + uri: 'https://example.com', created_at: '2021-01-24T07:50:00.901Z', sensitive: false, visibility: 'public', @@ -164,7 +230,6 @@ const demoStatuses = [ bookmarked: false, content: '

- tooot supports multiple accounts
- tooot supports browsing external instance
- tooot aims to support multiple languages

', - reblog: null, application: { name: 'tooot', website: 'https://tooot.app' @@ -174,12 +239,27 @@ const demoStatuses = [ username: 'tooot📱', acct: 'tooot@xmflsct.com', display_name: 'tooot📱', - avatar_static: - 'https://avatars.githubusercontent.com/u/77554750?s=200&v=4' + avatar: 'https://avatars.githubusercontent.com/u/77554750?s=200&v=4', + avatar_static: 'https://avatars.githubusercontent.com/u/77554750?s=200&v=4', + url: '', + header: '', + header_static: '', + locked: false, + discoverable: false, + created_at: new Date().toISOString(), + last_status_at: new Date().toISOString(), + statuses_count: 1, + followers_count: 1, + following_count: 1, + fields: [], + bot: false }, media_attachments: [], - mentions: [] + mentions: [], + tags: [], + emojis: [], + pinned: false } ] -export default demoStatuses +export default demoStatus diff --git a/fastlane/Fastfile b/fastlane/Fastfile index c4d1f624..66d35884 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -108,6 +108,29 @@ private_lane :build_android do end end +desc "Build Android apk" +private_lane :build_android_apk do + sh("echo #{ENV["ANDROID_KEYSTORE"]} | base64 -d | tee #{File.expand_path('..', Dir.pwd)}/android/tooot.jks >/dev/null", log: false) + + prepare_playstore_android + + build_android_app( + task: 'assemble', + build_type: 'release', + project_dir: "./android", + print_command: true, + print_command_output: true, + properties: { + "android.injected.signing.store.file" => "#{File.expand_path('..', Dir.pwd)}/android/tooot.jks", + "android.injected.signing.store.password" => ENV["ANDROID_KEYSTORE_PASSWORD"], + "android.injected.signing.key.alias" => ENV["ANDROID_KEYSTORE_ALIAS"], + "android.injected.signing.key.password" => ENV["ANDROID_KEYSTORE_KEY_PASSWORD"], + } + ) + + sh "mv #{lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH]} #{File.expand_path('..', Dir.pwd)}/tooot-#{GITHUB_RELEASE}.apk" +end + lane :ios do cocoapods(clean_install: true, podfile: "./ios/Podfile") build_ios @@ -121,13 +144,15 @@ end lane :release do if ENVIRONMENT == 'release' + build_android_apk set_github_release( repository_name: GITHUB_REPO, name: GITHUB_RELEASE, tag_name: GITHUB_RELEASE, description: "No changelog provided", commitish: git_branch, - is_prerelease: false + is_prerelease: false, + upload_assets: ["#{File.expand_path('..', Dir.pwd)}/tooot-#{GITHUB_RELEASE}.apk"] ) end rocket diff --git a/fastlane/metadata/en-US/release_notes.txt b/fastlane/metadata/en-US/release_notes.txt index 12b2cab0..cf45f9e4 100644 --- a/fastlane/metadata/en-US/release_notes.txt +++ b/fastlane/metadata/en-US/release_notes.txt @@ -1,3 +1,11 @@ Enjoy toooting! This version includes following improvements and fixes: -- Fix toot attribution of favourites etc. -- Fix switching language \ No newline at end of file +- Added 🇺🇦 Slava Ukraini +- Automatic setting detected language when tooting +- Remember public timeline type selection +- Show diffing of edit history +- Allow hiding boosts and replies in home timeline +- Support toot in RTL languages +- Added notification for admins +- Pilot conversation hierarchy +- Fix whole word filter matching +- Fix tablet cannot delete toot drafts \ 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 51453bd1..95a80d31 100644 --- a/fastlane/metadata/zh-Hans/release_notes.txt +++ b/fastlane/metadata/zh-Hans/release_notes.txt @@ -1,3 +1,11 @@ toooting愉快!此版本包括以下改进和修复: -- 修复嘟文收藏等显示 -- 修复不能切换语言 \ No newline at end of file +- 增加 🇺🇦 Slava Ukraini +- 自动识别发嘟语言 +- 记住上次公共时间轴选项 +- 显示编辑历史的差异 +- 关注列表可隐藏转嘟和回复 +- 新增管理员推送通知 +- 支持嘟文右到左文字 +- 测试显示对话层级 +- 修复过滤整词功能 +- 修复平板不能删除草稿 \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 82d8d8f0..1ad566ac 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -295,23 +295,21 @@ PODS: - React-Core - react-native-blurhash (1.1.10): - React-Core - - react-native-cameraroll (5.1.0): + - react-native-cameraroll (5.2.0): - React-Core - react-native-image-picker (4.10.2): - React-Core - react-native-ios-context-menu (1.15.1): - React-Core - - react-native-language-detection (0.1.0): + - react-native-language-detection (0.2.2): - React - - react-native-live-text-image-view (0.4.0): - - React-Core - react-native-menu (0.7.2): - React - react-native-netinfo (9.3.7): - React-Core - react-native-pager-view (6.1.2): - React-Core - - react-native-paste-input (0.5.1): + - react-native-paste-input (0.5.2): - React-Core - Swime (= 3.0.6) - react-native-safe-area-context (4.4.1): @@ -428,9 +426,9 @@ PODS: - RNScreens (3.18.2): - React-Core - React-RCTImage - - RNSentry (4.10.1): + - RNSentry (4.12.0): - React-Core - - Sentry/HybridSDK (= 7.31.2) + - Sentry/HybridSDK (= 7.31.3) - RNShareMenu (6.0.0): - React - RNSVG (13.6.0): @@ -441,7 +439,7 @@ PODS: - SDWebImageWebPCoder (0.9.1): - libwebp (~> 1.0) - SDWebImage/Core (~> 5.13) - - Sentry/HybridSDK (7.31.2) + - Sentry/HybridSDK (7.31.3) - Swime (3.0.6) - Yoga (1.14.0) @@ -495,7 +493,6 @@ DEPENDENCIES: - react-native-image-picker (from `../node_modules/react-native-image-picker`) - react-native-ios-context-menu (from `../node_modules/react-native-ios-context-menu`) - react-native-language-detection (from `../node_modules/react-native-language-detection`) - - react-native-live-text-image-view (from `../node_modules/react-native-live-text-image-view`) - "react-native-menu (from `../node_modules/@react-native-menu/menu`)" - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" - react-native-pager-view (from `../node_modules/react-native-pager-view`) @@ -630,8 +627,6 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-ios-context-menu" react-native-language-detection: :path: "../node_modules/react-native-language-detection" - react-native-live-text-image-view: - :path: "../node_modules/react-native-live-text-image-view" react-native-menu: :path: "../node_modules/@react-native-menu/menu" react-native-netinfo: @@ -736,15 +731,14 @@ SPEC CHECKSUMS: React-logger: 1623c216abaa88974afce404dc8f479406bbc3a0 react-native-blur: 50c9feabacbc5f49b61337ebc32192c6be7ec3c3 react-native-blurhash: add4df9a937b4e021a24bc67a0714f13e0bd40b7 - react-native-cameraroll: a40b082318eb1ecd0336a2f29d9f74b7f2c8cae8 + react-native-cameraroll: 0ff04cc4e0ff5f19a94ff4313e5c8bc4503cd86d react-native-image-picker: bf34f3f516d139ed3e24c5f5a381a91819e349ea react-native-ios-context-menu: b170594b4448c0cd10c79e13432216bac99de1ac - react-native-language-detection: 0e43195ad014974f1b7a31b64820eff34a243f2d - react-native-live-text-image-view: 483bacfdba464162b8cf176bba555364f18b584c + react-native-language-detection: f414937fa715108ab50a6269a3de0bcb95e4ceb0 react-native-menu: 8e172cfcf0e42e92f028e7781eddf84d430cae24 react-native-netinfo: 2517ad504b3d303e90d7a431b0fcaef76d207983 react-native-pager-view: 54bed894cecebe28cede54c01038d9d1e122de43 - react-native-paste-input: 183ad7dc224e192719616f4258dde5b548627d08 + react-native-paste-input: 88709b4fd586ea8cc56ba5e2fc4cdfe90597730c react-native-safe-area-context: 99b24a0c5acd0d5dcac2b1a7f18c49ea317be99a react-native-segmented-control: 65df6cd0619b780b3843d574a72d4c7cec396097 React-perflogger: 8c79399b0500a30ee8152d0f9f11beae7fc36595 @@ -765,12 +759,12 @@ SPEC CHECKSUMS: RNGestureHandler: 62232ba8f562f7dea5ba1b3383494eb5bf97a4d3 RNReanimated: ce445c233a6ff5600223484a88ad5704945d972a RNScreens: 34cc502acf1b916c582c60003dc3089fa01dc66d - RNSentry: 3c27f3c57f16bab9835d9555add298571077e0c1 + RNSentry: 4c09f4dd9740cb9b33e94303de5b6d0dbeb0737d RNShareMenu: cb9dac548c8bf147d06f0bf07296ad51ea9f5fc3 RNSVG: 3a79c0c4992213e4f06c08e62730c5e7b9e4dc17 SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84 SDWebImageWebPCoder: 18503de6621dd2c420d680e33d46bf8e1d5169b0 - Sentry: b15765d11769852fe78c9add942f7df60ed5dbf5 + Sentry: 08884c523575ec0f6690d94ed3ccb0246a1600bf Swime: d7b2c277503b6cea317774aedc2dce05613f8b0b Yoga: 99caf8d5ab45e9d637ee6e0174ec16fbbb01bcfc diff --git a/ios/tooot.xcodeproj/project.pbxproj b/ios/tooot.xcodeproj/project.pbxproj index 6d8698dd..908f700c 100644 --- a/ios/tooot.xcodeproj/project.pbxproj +++ b/ios/tooot.xcodeproj/project.pbxproj @@ -86,6 +86,7 @@ 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 = ""; }; 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 = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; ED2971642150620600B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS12.0.sdk/System/Library/Frameworks/JavaScriptCore.framework; sourceTree = DEVELOPER_DIR; }; /* End PBXFileReference section */ @@ -298,6 +299,7 @@ sv, nl, ca, + uk, ); mainGroup = 83CBB9F61A601CBA00E9B192; productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; @@ -530,6 +532,7 @@ E63E7FF0292A828100C76FD4 /* sv */, E6217B7E293C1EBF00B1755E /* nl */, E6A4895D293C1F740047951A /* ca */, + E6D64C7A294A90840098F3AC /* uk */, ); name = InfoPlist.strings; sourceTree = ""; @@ -574,6 +577,7 @@ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_OBJC_BRIDGING_HEADER = "tooot-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_PRECOMPILE_BRIDGING_HEADER = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,6"; VERSIONING_SYSTEM = "apple-generic"; @@ -611,6 +615,7 @@ SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_OBJC_BRIDGING_HEADER = "tooot-Bridging-Header.h"; + SWIFT_PRECOMPILE_BRIDGING_HEADER = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,6"; VERSIONING_SYSTEM = "apple-generic"; @@ -781,6 +786,7 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "ShareExtension/ShareExtension-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_PRECOMPILE_BRIDGING_HEADER = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,6"; }; @@ -828,6 +834,7 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "ShareExtension/ShareExtension-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_PRECOMPILE_BRIDGING_HEADER = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,6"; }; diff --git a/ios/uk.lproj/InfoPlist.strings b/ios/uk.lproj/InfoPlist.strings new file mode 100644 index 00000000..a23b678c --- /dev/null +++ b/ios/uk.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +"NSPhotoLibraryAddUsageDescription" = "Дозвольте tooot зберігати зображення у вашій папці фотоапарата"; +"NSPhotoLibraryUsageDescription" = "Дозвольте tooot зберігати зображення у вашій папці фотоапарата"; diff --git a/package.json b/package.json index b822761c..b10cfe1c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tooot", - "version": "4.6.6", + "version": "4.7.0", "description": "tooot for Mastodon", "author": "xmflsct ", "license": "GPL-3.0-or-later", @@ -12,7 +12,7 @@ "start": "react-native start", "android": "react-native run-android", "iphone": "react-native run-ios --simulator 'iPhone 14 Pro'", - "ipad": "react-native run-ios --simulator 'iPad Pro (11-inch) (3rd generation)'", + "ipad": "react-native run-ios --simulator 'iPad Pro (11-inch) (4th generation)'", "app:build": "bundle exec fastlane", "clean": "react-native-clean-project", "postinstall": "patch-package" @@ -25,23 +25,25 @@ "@formatjs/intl-numberformat": "^8.3.3", "@formatjs/intl-pluralrules": "^5.1.8", "@formatjs/intl-relativetimeformat": "^11.1.8", - "@mattermost/react-native-paste-input": "^0.5.1", + "@mattermost/react-native-paste-input": "^0.5.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.1.0", + "@react-native-camera-roll/camera-roll": "^5.2.0", "@react-native-clipboard/clipboard": "^1.11.1", "@react-native-community/blur": "^4.3.0", "@react-native-community/netinfo": "9.3.7", "@react-native-community/segmented-control": "^2.2.2", "@react-native-menu/menu": "^0.7.2", - "@react-navigation/bottom-tabs": "^6.4.3", - "@react-navigation/native": "^6.0.16", - "@react-navigation/native-stack": "^6.9.4", - "@react-navigation/stack": "^6.3.7", + "@react-navigation/bottom-tabs": "^6.5.1", + "@react-navigation/native": "^6.1.1", + "@react-navigation/native-stack": "^6.9.6", + "@react-navigation/stack": "^6.3.9", "@reduxjs/toolkit": "^1.9.1", - "@sentry/react-native": "4.10.1", + "@sentry/react-native": "4.12.0", "@sharcoux/slider": "^6.1.1", - "axios": "^0.27.2", + "@tanstack/react-query": "^4.20.4", + "axios": "^1.2.1", + "diff": "^5.1.0", "expo": "^47.0.8", "expo-auth-session": "^3.7.3", "expo-av": "^13.0.2", @@ -59,13 +61,12 @@ "expo-store-review": "^6.0.0", "expo-video-thumbnails": "^7.0.0", "expo-web-browser": "~12.0.0", - "i18next": "^22.0.6", - "li": "^1.3.0", + "i18next": "^22.4.5", "linkify-it": "^4.0.1", "lodash": "^4.17.21", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-i18next": "^12.0.0", + "react-i18next": "^12.1.1", "react-intl": "^6.2.5", "react-native": "0.70.6", "react-native-animated-spinkit": "^1.5.2", @@ -78,8 +79,7 @@ "react-native-htmlview": "^0.16.0", "react-native-image-picker": "^4.10.2", "react-native-ios-context-menu": "^1.15.1", - "react-native-language-detection": "^0.1.0", - "react-native-live-text-image-view": "^0.4.0", + "react-native-language-detection": "^0.2.2", "react-native-pager-view": "^6.1.2", "react-native-reanimated": "^2.13.0", "react-native-reanimated-zoom": "^0.3.3", @@ -88,11 +88,11 @@ "react-native-share-menu": "^6.0.0", "react-native-svg": "^13.6.0", "react-native-swipe-list-view": "^3.2.9", - "react-native-tab-view": "^3.3.2", - "react-query": "^3.39.2", + "react-native-tab-view": "^3.3.4", "react-redux": "^8.0.5", "redux-persist": "^6.0.0", "rn-placeholder": "^3.0.3", + "rtl-detect": "^1.0.4", "valid-url": "^1.0.9", "zeego": "^0.5.0" }, @@ -102,11 +102,12 @@ "@babel/preset-react": "^7.18.6", "@babel/preset-typescript": "^7.18.6", "@expo/config": "^7.0.3", + "@types/diff": "^5.0.2", "@types/linkify-it": "^3.0.2", "@types/lodash": "^4.14.191", "@types/react": "~18.0.26", "@types/react-dom": "~18.0.9", - "@types/react-native": "~0.70.7", + "@types/react-native": "~0.70.8", "@types/react-native-base64": "^0.2.0", "@types/react-native-share-menu": "^5.0.2", "@types/react-timeago": "^4.1.3", @@ -120,6 +121,6 @@ "patch-package": "^6.5.0", "postinstall-postinstall": "^2.1.0", "react-native-clean-project": "^4.0.1", - "typescript": "^4.9.3" + "typescript": "^4.9.4" } } diff --git a/src/@types/app.d.ts b/src/@types/app.d.ts index 0db454c2..3b2b9525 100644 --- a/src/@types/app.d.ts +++ b/src/@types/app.d.ts @@ -3,13 +3,12 @@ declare namespace App { | 'Following' | 'Local' | 'LocalPublic' + | 'Trending' | 'Notifications' | 'Hashtag' | 'List' | 'Toot' - | 'Account_Default' - | 'Account_All' - | 'Account_Attachments' + | 'Account' | 'Conversations' | 'Bookmarks' | 'Favourites' diff --git a/src/@types/mastodon.d.ts b/src/@types/mastodon.d.ts index 54bc1571..699cbd16 100644 --- a/src/@types/mastodon.d.ts +++ b/src/@types/mastodon.d.ts @@ -30,6 +30,7 @@ declare namespace Mastodon { bot: boolean source?: Source suspended?: boolean + role?: Role } type Announcement = { @@ -332,24 +333,34 @@ declare namespace Mastodon { url: string } - type Notification = { - // Base - id: string - type: - | 'follow' - | 'follow_request' - | 'mention' - | 'reblog' - | 'favourite' - | 'poll' - | 'status' - | 'update' - created_at: string - account: Account - - // Others - status?: Status - } + type Notification = + | { + // Base + id: string + type: 'favourite' | 'mention' | 'poll' | 'reblog' | 'status' | 'update' + created_at: string + account: Account + status: Status + report: undefined + } + | { + // Base + id: string + type: 'follow' | 'follow_request' | 'admin.sign_up' + created_at: string + account: Account + status: undefined + report: undefined + } + | { + // Base + id: string + type: 'admin.report' + created_at: string + account: Account + status: undefined + report: Report + } type Poll = { // Base @@ -384,6 +395,9 @@ declare namespace Mastodon { mention: boolean poll: boolean status: boolean + update: boolean + 'admin.sign_up': boolean + 'admin.report': boolean } server_key: string } @@ -403,12 +417,37 @@ declare namespace Mastodon { note: string } + type Report = { + id: string + action_taken: boolean + action_taken_at?: string + category: 'spam' | 'violation' | 'other' + comment: string + forwarded: boolean + created_at: string + status_ids?: string[] + rule_ids?: string[] + target_account: Account + } + type Results = { accounts?: Account[] statuses?: Status[] hashtags?: Tag[] } + type Role = { + // Added since 4.0 + id: string + name: string + color: string + position: number + permissions: string + highlighted: boolean + created_at: string + updated_at: string + } + type Status = { // Base id: string @@ -479,25 +518,4 @@ declare namespace Mastodon { history: { day: string; accounts: string; uses: string }[] following: boolean // Since v4.0 } - - type WebSocketStream = - | 'user' - | 'public' - | 'public:local' - | 'hashtag' - | 'hashtag:local' - | 'list' - | 'direct' - type WebSocket = - | { - stream: WebSocketStream[] - event: 'update' - payload: string // Status - } - | { stream: WebSocketStream[]; event: 'delete'; payload: Status['id'] } - | { - stream: WebSocketStream[] - event: 'notification' - payload: string // Notification - } } diff --git a/src/@types/untyped.d.ts b/src/@types/untyped.d.ts index e3b7bc1a..ad27df89 100644 --- a/src/@types/untyped.d.ts +++ b/src/@types/untyped.d.ts @@ -1,9 +1,9 @@ declare module 'gl-react-blurhash' declare module 'htmlparser2-without-node-native' -declare module 'li' declare module 'react-native-feather' declare module 'react-native-htmlview' declare module 'react-native-toast-message' +declare module 'rtl-detect' declare module '@helpers/features' { const features: { feature: string; version: number; reference?: string }[] diff --git a/src/App.tsx b/src/App.tsx index 92dcc448..5d77d55a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -23,7 +23,7 @@ import { LogBox, Platform } from 'react-native' import { GestureHandlerRootView } from 'react-native-gesture-handler' import { SafeAreaProvider } from 'react-native-safe-area-context' import { enableFreeze } from 'react-native-screens' -import { QueryClientProvider } from 'react-query' +import { QueryClientProvider } from '@tanstack/react-query' import { Provider } from 'react-redux' import { PersistGate } from 'redux-persist/integration/react' diff --git a/src/Screens.tsx b/src/Screens.tsx index f5686550..e7430d1f 100644 --- a/src/Screens.tsx +++ b/src/Screens.tsx @@ -135,10 +135,7 @@ const Screens: React.FC = ({ localCorrupt }) => { instance => paths[0] === `@${instance.account.acct}@${instance.uri}` ) if (instanceIndex !== -1 && instanceActive !== instanceIndex) { - initQuery({ - instance: instances[instanceIndex], - prefetch: { enabled: true } - }) + initQuery({ instance: instances[instanceIndex] }) } } } diff --git a/src/api/general.ts b/src/api/general.ts index 81e993f5..f13e698f 100644 --- a/src/api/general.ts +++ b/src/api/general.ts @@ -1,5 +1,5 @@ import axios from 'axios' -import { ctx, handleError, userAgent } from './helpers' +import { ctx, handleError, PagedResponse, userAgent } from './helpers' export type Params = { method: 'get' | 'post' | 'put' | 'delete' @@ -19,7 +19,7 @@ const apiGeneral = async ({ params, headers, body -}: Params): Promise<{ body: T }> => { +}: Params): Promise> => { console.log( ctx.bgGreen.bold(' API general ') + ' ' + @@ -47,9 +47,27 @@ const apiGeneral = async ({ ...(body && { data: body }) }) .then(response => { - return Promise.resolve({ - body: response.data - }) + let links: { + prev?: { id: string; isOffset: boolean } + next?: { id: string; isOffset: boolean } + } = {} + + if (response.headers?.link) { + const linksParsed = response.headers.link.matchAll( + new RegExp('[?&](.*?_id|offset)=(.*?)>; *rel="(.*?)"', 'gi') + ) + for (const link of linksParsed) { + switch (link[3]) { + case 'prev': + links.prev = { id: link[2], isOffset: link[1].includes('offset') } + break + case 'next': + links.next = { id: link[2], isOffset: link[1].includes('offset') } + break + } + } + } + return Promise.resolve({ body: response.data, links }) }) .catch(handleError()) } diff --git a/src/api/helpers/index.ts b/src/api/helpers/index.ts index 55871b18..65013f79 100644 --- a/src/api/helpers/index.ts +++ b/src/api/helpers/index.ts @@ -63,4 +63,10 @@ const handleError = } } +type LinkFormat = { id: string; isOffset: boolean } +export type PagedResponse = { + body: T + links: { prev?: LinkFormat; next?: LinkFormat } +} + export { ctx, handleError, userAgent } diff --git a/src/api/instance.ts b/src/api/instance.ts index a178a42c..d6446e8f 100644 --- a/src/api/instance.ts +++ b/src/api/instance.ts @@ -1,7 +1,6 @@ import { RootState } from '@root/store' import axios, { AxiosRequestConfig } from 'axios' -import li from 'li' -import { ctx, handleError, userAgent } from './helpers' +import { ctx, handleError, PagedResponse, userAgent } from './helpers' export type Params = { method: 'get' | 'post' | 'put' | 'delete' | 'patch' @@ -15,11 +14,6 @@ export type Params = { extras?: Omit } -export type InstanceResponse = { - body: T - links: { prev?: string; next?: string } -} - const apiInstance = async ({ method, version = 'v1', @@ -28,7 +22,7 @@ const apiInstance = async ({ headers, body, extras -}: Params): Promise> => { +}: Params): Promise> => { const { store } = require('@root/store') const state = store.getState() as RootState const instanceActive = state.instances.instances.findIndex(instance => instance.active) @@ -74,17 +68,27 @@ const apiInstance = async ({ ...extras }) .then(response => { - let prev - let next + let links: { + prev?: { id: string; isOffset: boolean } + next?: { id: string; isOffset: boolean } + } = {} + if (response.headers?.link) { - const headersLinks = li.parse(response.headers?.link) - prev = headersLinks.prev?.match(/_id=([0-9]*)/)?.[1] - next = headersLinks.next?.match(/_id=([0-9]*)/)?.[1] + const linksParsed = response.headers.link.matchAll( + new RegExp('[?&](.*?_id|offset)=(.*?)>; *rel="(.*?)"', 'gi') + ) + for (const link of linksParsed) { + switch (link[3]) { + case 'prev': + links.prev = { id: link[2], isOffset: link[1].includes('offset') } + break + case 'next': + links.next = { id: link[2], isOffset: link[1].includes('offset') } + break + } + } } - return Promise.resolve({ - body: response.data, - links: { prev, next } - }) + return Promise.resolve({ body: response.data, links }) }) .catch(handleError()) } diff --git a/src/components/AccountButton.tsx b/src/components/AccountButton.tsx index cddce028..21ef8288 100644 --- a/src/components/AccountButton.tsx +++ b/src/components/AccountButton.tsx @@ -12,11 +12,7 @@ interface Props { additionalActions?: () => void } -const AccountButton: React.FC = ({ - instance, - selected = false, - additionalActions -}) => { +const AccountButton: React.FC = ({ instance, selected = false, additionalActions }) => { const navigation = useNavigation() return ( @@ -27,12 +23,10 @@ const AccountButton: React.FC = ({ marginBottom: StyleConstants.Spacing.M, marginRight: StyleConstants.Spacing.M }} - content={`@${instance.account.acct}@${instance.uri}${ - selected ? ' ✓' : '' - }`} + content={`@${instance.account.acct}@${instance.uri}${selected ? ' ✓' : ''}`} onPress={() => { haptics('Light') - initQuery({ instance, prefetch: { enabled: true } }) + initQuery({ instance }) navigation.goBack() if (additionalActions) { additionalActions() diff --git a/src/components/GracefullyImage.tsx b/src/components/GracefullyImage.tsx index 5afcbff6..f4c393e7 100644 --- a/src/components/GracefullyImage.tsx +++ b/src/components/GracefullyImage.tsx @@ -4,15 +4,13 @@ import React, { useMemo, useState } from 'react' import { AccessibilityProps, Image, - ImageStyle, - Platform, Pressable, StyleProp, StyleSheet, View, ViewStyle } from 'react-native' -import FastImage from 'react-native-fast-image' +import FastImage, { ImageStyle } from 'react-native-fast-image' import { Blurhash } from 'react-native-blurhash' // blurhas -> if blurhash, show before any loading succeed @@ -97,30 +95,17 @@ const GracefullyImage = ({ {...(onPress ? (hidden ? { disabled: true } : { onPress }) : { disabled: true })} > {uri.preview && !imageLoaded ? ( - ) : null} - {Platform.OS === 'ios' ? ( - - ) : ( - - )} + {blurhashView} ) diff --git a/src/components/Hashtag.tsx b/src/components/Hashtag.tsx index 2d05edc9..1402d7ac 100644 --- a/src/components/Hashtag.tsx +++ b/src/components/Hashtag.tsx @@ -3,8 +3,8 @@ import { StackNavigationProp } from '@react-navigation/stack' import { TabLocalStackParamList } from '@utils/navigation/navigators' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' -import React, { useCallback, useState } from 'react' -import { Dimensions, Pressable } from 'react-native' +import React, { PropsWithChildren, useCallback, useState } from 'react' +import { Dimensions, Pressable, View } from 'react-native' import Sparkline from './Sparkline' import CustomText from './Text' @@ -13,7 +13,11 @@ export interface Props { onPress?: () => void } -const ComponentHashtag: React.FC = ({ hashtag, onPress: customOnPress }) => { +const ComponentHashtag: React.FC = ({ + hashtag, + onPress: customOnPress, + children +}) => { const { colors } = useTheme() const navigation = useNavigation>() @@ -31,15 +35,11 @@ const ComponentHashtag: React.FC = ({ hashtag, onPress: customOnPress }) style={{ flex: 1, flexDirection: 'row', + alignItems: 'center', justifyContent: 'space-between', padding }} onPress={customOnPress || onPress} - onLayout={({ - nativeEvent: { - layout: { height } - } - }) => setHeight(height - padding * 2 - 1)} > = ({ hashtag, onPress: customOnPress }) > #{hashtag.name} - parseInt(h.uses)).reverse()} - width={width} - height={height} - /> + setHeight(height)} + > + parseInt(h.uses)).reverse()} + width={width} + height={height} + margin={children ? StyleConstants.Spacing.S : undefined} + /> + {children} + ) } diff --git a/src/components/Header/Center.tsx b/src/components/Header/Center.tsx index 2a700fab..d61e3bfa 100644 --- a/src/components/Header/Center.tsx +++ b/src/components/Header/Center.tsx @@ -1,6 +1,4 @@ -import Icon from '@components/Icon' import CustomText from '@components/Text' -import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import React from 'react' import { View } from 'react-native' @@ -9,16 +7,10 @@ export interface Props { content?: string inverted?: boolean onPress?: () => void - dropdown?: boolean } // Used for Android mostly -const HeaderCenter: React.FC = ({ - content, - inverted = false, - onPress, - dropdown = false -}) => { +const HeaderCenter: React.FC = ({ content, inverted = false, onPress }) => { const { colors } = useTheme() return ( @@ -33,13 +25,6 @@ const HeaderCenter: React.FC = ({ children={content} {...(onPress && { onPress })} /> - ) } diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index 735620ed..37c7e61e 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -11,6 +11,7 @@ export interface Props { fill?: string strokeWidth?: number style?: StyleProp + crossOut?: boolean } const Icon: React.FC = ({ @@ -20,7 +21,8 @@ const Icon: React.FC = ({ color, fill, strokeWidth = 2, - style + style, + crossOut = false }) => { return ( = ({ fill, strokeWidth })} + {crossOut ? ( + + ) : null} ) } diff --git a/src/components/Instance/Auth.tsx b/src/components/Instance/Auth.tsx deleted file mode 100644 index 1a72e2b5..00000000 --- a/src/components/Instance/Auth.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import browserPackage from '@helpers/browserPackage' -import { useNavigation } from '@react-navigation/native' -import { useAppDispatch } from '@root/store' -import { InstanceLatest } from '@utils/migrations/instances/migration' -import { TabMeStackNavigationProp } from '@utils/navigation/navigators' -import addInstance from '@utils/slices/instances/add' -import { checkInstanceFeature } from '@utils/slices/instancesSlice' -import * as AuthSession from 'expo-auth-session' -import React, { useEffect } from 'react' -import { useQueryClient } from 'react-query' -import { useSelector } from 'react-redux' - -export interface Props { - instanceDomain: string - // Domain can be different than uri - instance: Mastodon.Instance - appData: InstanceLatest['appData'] - goBack?: boolean -} - -const InstanceAuth = React.memo( - ({ instanceDomain, instance, appData, goBack }: Props) => { - const redirectUri = AuthSession.makeRedirectUri({ - native: 'tooot://instance-auth', - useProxy: false - }) - - const navigation = useNavigation>() - const queryClient = useQueryClient() - const dispatch = useAppDispatch() - - const deprecateAuthFollow = useSelector(checkInstanceFeature('deprecate_auth_follow')) - const [request, response, promptAsync] = AuthSession.useAuthRequest( - { - clientId: appData.clientId, - clientSecret: appData.clientSecret, - scopes: deprecateAuthFollow - ? ['read', 'write', 'push'] - : ['read', 'write', 'follow', 'push'], - redirectUri - }, - { - authorizationEndpoint: `https://${instanceDomain}/oauth/authorize` - } - ) - useEffect(() => { - ;(async () => { - if (request?.clientId) { - await promptAsync({ browserPackage: await browserPackage() }).catch(e => console.log(e)) - } - })() - }, [request]) - useEffect(() => { - ;(async () => { - if (response?.type === 'success') { - const { accessToken } = await AuthSession.exchangeCodeAsync( - { - clientId: appData.clientId, - clientSecret: appData.clientSecret, - scopes: ['read', 'write', 'follow', 'push'], - redirectUri, - code: response.params.code, - extraParams: { - grant_type: 'authorization_code' - } - }, - { - tokenEndpoint: `https://${instanceDomain}/oauth/token` - } - ) - queryClient.clear() - dispatch( - addInstance({ - domain: instanceDomain, - token: accessToken, - instance, - appData - }) - ) - goBack && navigation.goBack() - } - })() - }, [response]) - - return <> - }, - () => true -) - -export default InstanceAuth diff --git a/src/components/Instance.tsx b/src/components/Instance/index.tsx similarity index 74% rename from src/components/Instance.tsx rename to src/components/Instance/index.tsx index b0039b37..e21c49de 100644 --- a/src/components/Instance.tsx +++ b/src/components/Instance/index.tsx @@ -1,22 +1,27 @@ import Button from '@components/Button' import Icon from '@components/Icon' import browserPackage from '@helpers/browserPackage' -import { useAppsQuery } from '@utils/queryHooks/apps' +import { redirectUri, useAppsMutation } from '@utils/queryHooks/apps' import { useInstanceQuery } from '@utils/queryHooks/instance' -import { getInstances } from '@utils/slices/instancesSlice' +import { checkInstanceFeature, getInstances } from '@utils/slices/instancesSlice' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' +import * as AuthSession from 'expo-auth-session' import * as WebBrowser from 'expo-web-browser' import { debounce } from 'lodash' -import React, { RefObject, useCallback, useMemo, useState } from 'react' +import React, { RefObject, useCallback, useState } from 'react' import { Trans, useTranslation } from 'react-i18next' import { Alert, Image, KeyboardAvoidingView, Platform, TextInput, View } from 'react-native' import { ScrollView } from 'react-native-gesture-handler' import { useSelector } from 'react-redux' import { Placeholder } from 'rn-placeholder' -import InstanceAuth from './Instance/Auth' -import InstanceInfo from './Instance/Info' -import CustomText from './Text' +import InstanceInfo from './Info' +import CustomText from '../Text' +import { useNavigation } from '@react-navigation/native' +import { TabMeStackNavigationProp } from '@utils/navigation/navigators' +import queryClient from '@helpers/queryClient' +import { useAppDispatch } from '@root/store' +import addInstance from '@utils/slices/instances/add' export interface Props { scrollViewRef?: RefObject @@ -31,30 +36,64 @@ const ComponentInstance: React.FC = ({ }) => { const { t } = useTranslation('componentInstance') const { colors, mode } = useTheme() + const navigation = useNavigation>() + const [domain, setDomain] = useState('') + + const dispatch = useAppDispatch() const instances = useSelector(getInstances, () => true) - const [domain, setDomain] = useState() - const instanceQuery = useInstanceQuery({ domain, options: { enabled: !!domain, retry: false } }) - const appsQuery = useAppsQuery({ - domain, - options: { enabled: false, retry: false } - }) - const onChangeText = useCallback( - debounce( - text => { - setDomain(text.replace(/^http(s)?\:\/\//i, '')) - appsQuery.remove() - }, - 1000, - { trailing: true } - ), - [] - ) + const deprecateAuthFollow = useSelector(checkInstanceFeature('deprecate_auth_follow')) + + const appsMutation = useAppsMutation({ + retry: false, + onSuccess: async (data, variables) => { + const clientId = data.client_id + const clientSecret = data.client_secret + + const discovery = { authorizationEndpoint: `https://${domain}/oauth/authorize` } + + const request = new AuthSession.AuthRequest({ + clientId, + clientSecret, + scopes: deprecateAuthFollow + ? ['read', 'write', 'push'] + : ['read', 'write', 'follow', 'push'], + redirectUri + }) + await request.makeAuthUrlAsync(discovery) + + const promptResult = await request.promptAsync(discovery) + + if (promptResult?.type === 'success') { + const { accessToken } = await AuthSession.exchangeCodeAsync( + { + clientId, + clientSecret, + scopes: ['read', 'write', 'follow', 'push'], + redirectUri, + code: promptResult.params.code, + extraParams: { grant_type: 'authorization_code' } + }, + { tokenEndpoint: `https://${variables.domain}/oauth/token` } + ) + queryClient.clear() + dispatch( + addInstance({ + domain, + token: accessToken, + instance: instanceQuery.data!, + appData: { clientId, clientSecret } + }) + ) + goBack && navigation.goBack() + } + } + }) const processUpdate = useCallback(() => { if (domain) { @@ -66,39 +105,15 @@ const ComponentInstance: React.FC = ({ }, { text: t('common:buttons.continue'), - onPress: () => { - appsQuery.refetch() - } + onPress: () => appsMutation.mutate({ domain }) } ]) } else { - appsQuery.refetch() + appsMutation.mutate({ domain }) } } }, [domain]) - const requestAuth = useMemo(() => { - if ( - domain && - instanceQuery.data?.uri && - appsQuery.data?.client_id && - appsQuery.data.client_secret - ) { - return ( - - ) - } - }, [domain, instanceQuery.data, appsQuery.data]) - return ( = ({ borderBottomWidth: 1, ...StyleConstants.FontStyle.M, color: colors.primaryDefault, - borderBottomColor: instanceQuery.isError ? colors.red : colors.border + borderBottomColor: instanceQuery.isError ? colors.red : colors.border, + ...(Platform.OS === 'android' && { paddingRight: 0 }) }} editable={false} defaultValue='https://' @@ -143,9 +159,12 @@ const ComponentInstance: React.FC = ({ ...StyleConstants.FontStyle.M, marginRight: StyleConstants.Spacing.M, color: colors.primaryDefault, - borderBottomColor: instanceQuery.isError ? colors.red : colors.border + borderBottomColor: instanceQuery.isError ? colors.red : colors.border, + ...(Platform.OS === 'android' && { paddingLeft: 0 }) }} - onChangeText={onChangeText} + onChangeText={debounce(text => setDomain(text.replace(/^http(s)?\:\/\//i, '')), 1000, { + trailing: true + })} autoCapitalize='none' clearButtonMode='never' keyboardType='url' @@ -176,7 +195,7 @@ const ComponentInstance: React.FC = ({ content={t('server.button')} onPress={processUpdate} disabled={!instanceQuery.data?.uri} - loading={instanceQuery.isFetching || appsQuery.isFetching} + loading={instanceQuery.isFetching || appsMutation.isLoading} /> @@ -276,8 +295,6 @@ const ComponentInstance: React.FC = ({ - - {requestAuth} ) } diff --git a/src/components/Menu/Row.tsx b/src/components/Menu/Row.tsx index 3953e5c9..2bfe1cdd 100644 --- a/src/components/Menu/Row.tsx +++ b/src/components/Menu/Row.tsx @@ -5,7 +5,7 @@ import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import { ColorDefinitions } from '@utils/styles/themes' import React, { useMemo } from 'react' -import { Text, View } from 'react-native' +import { View } from 'react-native' import { Flow } from 'react-native-animated-spinkit' import { State, Switch, TapGestureHandler } from 'react-native-gesture-handler' @@ -65,7 +65,6 @@ const MenuRow: React.FC = ({ > { - if (typeof iconBack !== 'string') return // Let icon back handles the gesture if (nativeEvent.state === State.ACTIVE && !loading) { if (screenReaderEnabled && switchOnValueChange) { switchOnValueChange() @@ -86,9 +85,10 @@ const MenuRow: React.FC = ({ > {iconFront && ( diff --git a/src/components/Parse/Emojis.tsx b/src/components/Parse/Emojis.tsx index 6e8df38b..d9e1e88b 100644 --- a/src/components/Parse/Emojis.tsx +++ b/src/components/Parse/Emojis.tsx @@ -5,7 +5,7 @@ import { StyleConstants } from '@utils/styles/constants' import { adaptiveScale } from '@utils/styles/scaling' import { useTheme } from '@utils/styles/ThemeManager' import React, { useMemo } from 'react' -import { Platform, StyleSheet } from 'react-native' +import { Platform, StyleSheet, TextStyle } from 'react-native' import FastImage from 'react-native-fast-image' import { useSelector } from 'react-redux' import validUrl from 'valid-url' @@ -18,16 +18,11 @@ export interface Props { size?: 'S' | 'M' | 'L' adaptiveSize?: boolean fontBold?: boolean + style?: TextStyle } const ParseEmojis = React.memo( - ({ - content, - emojis, - size = 'M', - adaptiveSize = false, - fontBold = false - }: Props) => { + ({ content, emojis, size = 'M', adaptiveSize = false, fontBold = false, style }: Props) => { const { reduceMotionEnabled } = useAccessibility() const adaptiveFontsize = useSelector(getSettingsFontsize) @@ -51,22 +46,13 @@ const ParseEmojis = React.memo( image: { width: adaptedFontsize, height: adaptedFontsize, - ...(Platform.OS === 'ios' - ? { - transform: [{ translateY: -2 }] - } - : { - transform: [{ translateY: 1 }] - }) + ...(Platform.OS === 'android' && { transform: [{ translateY: 2 }] }) } }) }, [theme, adaptiveFontsize]) return ( - + {emojis ? ( content .split(regexEmoji) @@ -78,11 +64,7 @@ const ParseEmojis = React.memo( return emojiShortcode === `:${emoji.shortcode}:` }) if (emojiIndex === -1) { - return ( - - {emojiShortcode} - - ) + return {emojiShortcode} } else { const uri = reduceMotionEnabled ? emojis[emojiIndex].static_url diff --git a/src/components/Parse/HTML.tsx b/src/components/Parse/HTML.tsx index a2615163..e651a66e 100644 --- a/src/components/Parse/HTML.tsx +++ b/src/components/Parse/HTML.tsx @@ -10,9 +10,10 @@ import { StyleConstants } from '@utils/styles/constants' import layoutAnimation from '@utils/styles/layoutAnimation' import { adaptiveScale } from '@utils/styles/scaling' import { useTheme } from '@utils/styles/ThemeManager' +import { isEqual } from 'lodash' import React, { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' -import { Platform, Pressable, View } from 'react-native' +import { Platform, Pressable, TextStyleIOS, View } from 'react-native' import HTMLView from 'react-native-htmlview' import { useSelector } from 'react-redux' @@ -133,13 +134,8 @@ const renderNode = ({ name='ExternalLink' size={adaptedFontsize} style={{ - ...(Platform.OS === 'ios' - ? { - transform: [{ translateY: -2 }] - } - : { - transform: [{ translateY: 1 }] - }) + marginLeft: StyleConstants.Spacing.XS, + ...(Platform.OS === 'android' && { transform: [{ translateY: 2 }] }) }} /> ) : null} @@ -158,6 +154,7 @@ const renderNode = ({ export interface Props { content: string size?: 'S' | 'M' | 'L' + textStyles?: TextStyleIOS adaptiveSize?: boolean emojis?: Mastodon.Emoji[] mentions?: Mastodon.Mention[] @@ -175,6 +172,7 @@ const ParseHTML = React.memo( ({ content, size = 'M', + textStyles, adaptiveSize = false, emojis, mentions, @@ -200,7 +198,7 @@ const ParseHTML = React.memo( const navigation = useNavigation>() const route = useRoute() const { colors, theme } = useTheme() - const { t, i18n } = useTranslation('componentParse') + const { t } = useTranslation('componentParse') if (!expandHint) { expandHint = t('HTML.defaultHint') } @@ -298,6 +296,7 @@ const ParseHTML = React.memo( } }} style={{ + ...textStyles, height: numberOfLines === 1 && !expanded ? 0 : undefined }} numberOfLines={ @@ -308,7 +307,7 @@ const ParseHTML = React.memo( ) }, - [theme, i18n.language] + [theme] ) return ( @@ -320,7 +319,7 @@ const ParseHTML = React.memo( /> ) }, - (prev, next) => prev.content === next.content + (prev, next) => prev.content === next.content && isEqual(prev.emojis, next.emojis) ) export default ParseHTML diff --git a/src/components/Relationship/Incoming.tsx b/src/components/Relationship/Incoming.tsx index d6eb633c..837345f6 100644 --- a/src/components/Relationship/Incoming.tsx +++ b/src/components/Relationship/Incoming.tsx @@ -8,7 +8,7 @@ import { useTheme } from '@utils/styles/ThemeManager' import React from 'react' import { useTranslation } from 'react-i18next' import { StyleSheet, View } from 'react-native' -import { useQueryClient } from 'react-query' +import { useQueryClient } from '@tanstack/react-query' export interface Props { id: Mastodon.Account['id'] diff --git a/src/components/Relationship/Outgoing.tsx b/src/components/Relationship/Outgoing.tsx index b551d670..70a643e0 100644 --- a/src/components/Relationship/Outgoing.tsx +++ b/src/components/Relationship/Outgoing.tsx @@ -10,7 +10,7 @@ import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { useTheme } from '@utils/styles/ThemeManager' import React from 'react' import { useTranslation } from 'react-i18next' -import { useQueryClient } from 'react-query' +import { useQueryClient } from '@tanstack/react-query' export interface Props { id: Mastodon.Account['id'] @@ -30,8 +30,8 @@ const RelationshipOutgoing = React.memo( haptics('Success') queryClient.setQueryData(queryKeyRelationship, [res]) if (action === 'block') { - const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Following' }] - queryClient.invalidateQueries(queryKey) + const queryKey = ['Timeline', { page: 'Following' }] + queryClient.invalidateQueries({ queryKey, exact: false }) } }, onError: (err: any, { payload: { action } }) => { diff --git a/src/components/Sparkline.tsx b/src/components/Sparkline.tsx index 21792069..707f28e7 100644 --- a/src/components/Sparkline.tsx +++ b/src/components/Sparkline.tsx @@ -1,7 +1,6 @@ import { useTheme } from '@utils/styles/ThemeManager' import { maxBy, minBy } from 'lodash' import React from 'react' -import { Platform } from 'react-native' import Svg, { G, Path } from 'react-native-svg' export interface Props { @@ -69,7 +68,7 @@ const Sparkline: React.FC = ({ data, width, height, margin = 0 }) => { const fillPoints = linePoints.concat(closePolyPoints) return ( - + > queryKey: QueryKeyTimeline + queryOptions?: Omit< + UseInfiniteQueryOptions, + 'notifyOnChangeProps' | 'getNextPageParam' | 'getPreviousPageParam' | 'select' | 'onSuccess' + > disableRefresh?: boolean disableInfinity?: boolean - lookback?: Extract - customProps: Partial> & - Pick, 'renderItem'> + customProps: Partial> & Pick, 'renderItem'> } const Timeline: React.FC = ({ flRef: customFLRef, queryKey, + queryOptions, disableRefresh = false, disableInfinity = false, customProps }) => { const { colors } = useTheme() - const { - data, - refetch, - isFetching, - isLoading, - fetchNextPage, - isFetchingNextPage - } = useTimelineQuery({ - ...queryKey[1], - options: { - notifyOnChangeProps: Platform.select({ - ios: ['dataUpdatedAt', 'isFetching'], - android: ['dataUpdatedAt', 'isFetching', 'isLoading'] - }), - getNextPageParam: lastPage => - lastPage?.links?.next && { - max_id: lastPage.links.next - } - } - }) + const { data, refetch, isFetching, isLoading, fetchNextPage, isFetchingNextPage } = + useTimelineQuery({ + ...queryKey[1], + options: { + ...queryOptions, + notifyOnChangeProps: Platform.select({ + ios: ['dataUpdatedAt', 'isFetching'], + android: ['dataUpdatedAt', 'isFetching', 'isLoading'] + }), + getNextPageParam: lastPage => + lastPage?.links?.next && { + ...(lastPage.links.next.isOffset + ? { offset: lastPage.links.next.id } + : { max_id: lastPage.links.next.id }) + } + } + }) - const flattenData = data?.pages - ? data.pages?.flatMap(page => [...page.body]) - : [] + const flattenData = data?.pages ? data.pages?.flatMap(page => [...page.body]) : [] const onEndReached = useCallback( () => !disableInfinity && !isFetchingNextPage && fetchNextPage(), @@ -134,10 +127,7 @@ const Timeline: React.FC = ({ onEndReached={onEndReached} onEndReachedThreshold={0.75} ListFooterComponent={ - + } ListEmptyComponent={} ItemSeparatorComponent={({ leadingItem }) => @@ -145,9 +135,7 @@ const Timeline: React.FC = ({ ) : ( ) } diff --git a/src/components/Timeline/Conversation.tsx b/src/components/Timeline/Conversation.tsx index b3311a28..8e07d2c0 100644 --- a/src/components/Timeline/Conversation.tsx +++ b/src/components/Timeline/Conversation.tsx @@ -8,7 +8,7 @@ import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import React, { useCallback } from 'react' import { Pressable, View } from 'react-native' -import { useMutation, useQueryClient } from 'react-query' +import { useMutation, useQueryClient } from '@tanstack/react-query' import TimelineActions from './Shared/Actions' import TimelineContent from './Shared/Content' import StatusContext from './Shared/Context' diff --git a/src/components/Timeline/Default.tsx b/src/components/Timeline/Default.tsx index cf57785b..cb3a8f0d 100644 --- a/src/components/Timeline/Default.tsx +++ b/src/components/Timeline/Default.tsx @@ -34,6 +34,7 @@ export interface Props { highlighted?: boolean disableDetails?: boolean disableOnPress?: boolean + isConversation?: boolean } // When the poll is long @@ -43,7 +44,8 @@ const TimelineDefault: React.FC = ({ rootQueryKey, highlighted = false, disableDetails = false, - disableOnPress = false + disableOnPress = false, + isConversation = false }) => { const { colors } = useTheme() const navigation = useNavigation>() @@ -69,9 +71,12 @@ const TimelineDefault: React.FC = ({ } const mainStyle: StyleProp = { - padding: StyleConstants.Spacing.Global.PagePadding, + flex: 1, + padding: disableDetails + ? StyleConstants.Spacing.Global.PagePadding / 1.5 + : StyleConstants.Spacing.Global.PagePadding, backgroundColor: colors.backgroundDefault, - paddingBottom: disableDetails ? StyleConstants.Spacing.Global.PagePadding : 0 + paddingBottom: disableDetails ? StyleConstants.Spacing.Global.PagePadding / 1.5 : 0 } const main = () => ( <> @@ -81,7 +86,13 @@ const TimelineDefault: React.FC = ({ ) : null} - + @@ -89,7 +100,11 @@ const TimelineDefault: React.FC = ({ @@ -125,8 +140,10 @@ const TimelineDefault: React.FC = ({ spoilerHidden, copiableContent, highlighted, + inThread: queryKey?.[1].page === 'Toot', disableDetails, - disableOnPress + disableOnPress, + isConversation }} > {disableOnPress ? ( diff --git a/src/components/Timeline/Footer.tsx b/src/components/Timeline/Footer.tsx index ab4e1934..4ac3f2af 100644 --- a/src/components/Timeline/Footer.tsx +++ b/src/components/Timeline/Footer.tsx @@ -21,7 +21,11 @@ const TimelineFooter = React.memo( enabled: !disableInfinity, notifyOnChangeProps: ['hasNextPage'], getNextPageParam: lastPage => - lastPage?.links?.next && { max_id: lastPage.links.next } + lastPage?.links?.next && { + ...(lastPage.links.next.isOffset + ? { offset: lastPage.links.next.id } + : { max_id: lastPage.links.next.id }) + } } }) @@ -43,11 +47,7 @@ const TimelineFooter = React.memo( + ]} /> diff --git a/src/components/Timeline/Notifications.tsx b/src/components/Timeline/Notifications.tsx index 04204965..7eff27d1 100644 --- a/src/components/Timeline/Notifications.tsx +++ b/src/components/Timeline/Notifications.tsx @@ -28,18 +28,18 @@ import TimelineHeaderAndroid from './Shared/HeaderAndroid' export interface Props { notification: Mastodon.Notification queryKey: QueryKeyTimeline - highlighted?: boolean } -const TimelineNotifications: React.FC = ({ - notification, - queryKey, - highlighted = false -}) => { +const TimelineNotifications: React.FC = ({ notification, queryKey }) => { const instanceAccount = useSelector(getInstanceAccount, () => true) const status = notification.status?.reblog ? notification.status.reblog : notification.status - const account = notification.status ? notification.status.account : notification.account + const account = + notification.type === 'admin.report' + ? notification.report.target_account + : notification.status + ? notification.status.account + : notification.account const ownAccount = notification.account?.id === instanceAccount?.id const [spoilerExpanded, setSpoilerExpanded] = useState( instanceAccount.preferences['reading:expand:spoilers'] || false @@ -91,7 +91,8 @@ const TimelineNotifications: React.FC = ({ notification.type === 'follow' || notification.type === 'follow_request' || notification.type === 'mention' || - notification.type === 'status' + notification.type === 'status' || + notification.type === 'admin.sign_up' ? 1 : 0.5 }} @@ -102,13 +103,11 @@ const TimelineNotifications: React.FC = ({ {notification.status ? ( - - + + @@ -138,8 +137,7 @@ const TimelineNotifications: React.FC = ({ status, ownAccount, spoilerHidden, - copiableContent, - highlighted + copiableContent }} > diff --git a/src/components/Timeline/Refresh.tsx b/src/components/Timeline/Refresh.tsx index 38cfdcdd..9ac45e95 100644 --- a/src/components/Timeline/Refresh.tsx +++ b/src/components/Timeline/Refresh.tsx @@ -1,10 +1,6 @@ import haptics from '@components/haptics' import Icon from '@components/Icon' -import { - QueryKeyTimeline, - TimelineData, - useTimelineQuery -} from '@utils/queryHooks/timeline' +import { QueryKeyTimeline, TimelineData, useTimelineQuery } from '@utils/queryHooks/timeline' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import React, { RefObject, useCallback, useRef, useState } from 'react' @@ -20,7 +16,7 @@ import Animated, { useSharedValue, withTiming } from 'react-native-reanimated' -import { InfiniteData, useQueryClient } from 'react-query' +import { InfiniteData, useQueryClient } from '@tanstack/react-query' export interface Props { flRef: RefObject> @@ -31,14 +27,8 @@ export interface Props { } const CONTAINER_HEIGHT = StyleConstants.Spacing.M * 2.5 -export const SEPARATION_Y_1 = -( - CONTAINER_HEIGHT / 2 + - StyleConstants.Font.Size.S / 2 -) -export const SEPARATION_Y_2 = -( - CONTAINER_HEIGHT * 1.5 + - StyleConstants.Font.Size.S / 2 -) +export const SEPARATION_Y_1 = -(CONTAINER_HEIGHT / 2 + StyleConstants.Font.Size.S / 2) +export const SEPARATION_Y_2 = -(CONTAINER_HEIGHT * 1.5 + StyleConstants.Font.Size.S / 2) const TimelineRefresh: React.FC = ({ flRef, @@ -57,87 +47,77 @@ const TimelineRefresh: React.FC = ({ const fetchingLatestIndex = useRef(0) const refetchActive = useRef(false) - const { - refetch, - isFetching, - isLoading, - fetchPreviousPage, - hasPreviousPage, - isFetchingNextPage - } = useTimelineQuery({ - ...queryKey[1], - options: { - getPreviousPageParam: firstPage => - firstPage?.links?.prev && { - min_id: firstPage.links.prev, - // https://github.com/facebook/react-native/issues/25239#issuecomment-731100372 - limit: '3' + const { refetch, isFetching, isLoading, fetchPreviousPage, hasPreviousPage, isFetchingNextPage } = + useTimelineQuery({ + ...queryKey[1], + options: { + getPreviousPageParam: firstPage => + firstPage?.links?.prev && { + ...(firstPage.links.prev.isOffset + ? { offset: firstPage.links.prev.id } + : { max_id: firstPage.links.prev.id }), + // https://github.com/facebook/react-native/issues/25239#issuecomment-731100372 + limit: '3' + }, + select: data => { + if (refetchActive.current) { + data.pageParams = [data.pageParams[0]] + data.pages = [data.pages[0]] + refetchActive.current = false + } + return data }, - select: data => { - if (refetchActive.current) { - data.pageParams = [data.pageParams[0]] - data.pages = [data.pages[0]] - refetchActive.current = false - } - return data - }, - onSuccess: () => { - if (fetchingLatestIndex.current > 0) { - if (fetchingLatestIndex.current > 5) { - clearFirstPage() - fetchingLatestIndex.current = 0 - } else { - if (hasPreviousPage) { - fetchPreviousPage() - fetchingLatestIndex.current++ - } else { + onSuccess: () => { + if (fetchingLatestIndex.current > 0) { + if (fetchingLatestIndex.current > 5) { clearFirstPage() fetchingLatestIndex.current = 0 + } else { + if (hasPreviousPage) { + fetchPreviousPage() + fetchingLatestIndex.current++ + } else { + clearFirstPage() + fetchingLatestIndex.current = 0 + } } } } } - } - }) + }) const { t } = useTranslation('componentTimeline') const { colors } = useTheme() const queryClient = useQueryClient() const clearFirstPage = () => { - queryClient.setQueryData | undefined>( - queryKey, - data => { - if (data?.pages[0] && data.pages[0].body.length === 0) { - return { - pages: data.pages.slice(1), - pageParams: data.pageParams.slice(1) - } - } else { - return data + queryClient.setQueryData | undefined>(queryKey, data => { + if (data?.pages[0] && data.pages[0].body.length === 0) { + return { + pages: data.pages.slice(1), + pageParams: data.pageParams.slice(1) } + } else { + return data } - ) + }) } const prepareRefetch = () => { refetchActive.current = true - queryClient.setQueryData | undefined>( - queryKey, - data => { - if (data) { - data.pageParams = [undefined] - const newFirstPage: TimelineData = { body: [] } - for (let page of data.pages) { - // @ts-ignore - newFirstPage.body.push(...page.body) - if (newFirstPage.body.length > 10) break - } - data.pages = [newFirstPage] + queryClient.setQueryData | undefined>(queryKey, data => { + if (data) { + data.pageParams = [undefined] + const newFirstPage: TimelineData = { body: [] } + for (let page of data.pages) { + // @ts-ignore + newFirstPage.body.push(...page.body) + if (newFirstPage.body.length > 10) break } - - return data + data.pages = [newFirstPage] } - ) + + return data + }) } const callRefetch = async () => { await refetch() @@ -161,10 +141,7 @@ const TimelineRefresh: React.FC = ({ ] })) const arrowTop = useAnimatedStyle(() => ({ - marginTop: - scrollY.value < SEPARATION_Y_2 - ? withTiming(CONTAINER_HEIGHT) - : withTiming(0) + marginTop: scrollY.value < SEPARATION_Y_2 ? withTiming(CONTAINER_HEIGHT) : withTiming(0) })) const arrowStage = useSharedValue(0) @@ -241,8 +218,7 @@ const TimelineRefresh: React.FC = ({ const headerPadding = useAnimatedStyle( () => ({ paddingTop: - fetchingLatestIndex.current !== 0 || - (isFetching && !isLoading && !isFetchingNextPage) + fetchingLatestIndex.current !== 0 || (isFetching && !isLoading && !isFetchingNextPage) ? withTiming(StyleConstants.Spacing.M * 2.5) : withTiming(0) }), @@ -254,10 +230,7 @@ const TimelineRefresh: React.FC = ({ {isFetching ? ( - + ) : ( <> diff --git a/src/components/Timeline/Shared/Actioned.tsx b/src/components/Timeline/Shared/Actioned.tsx index 8d3dcb26..8e73de69 100644 --- a/src/components/Timeline/Shared/Actioned.tsx +++ b/src/components/Timeline/Shared/Actioned.tsx @@ -28,7 +28,12 @@ const TimelineActioned: React.FC = ({ action, isNotification, ...rest }) const iconColor = colors.primaryDefault const content = (content: string) => ( - + ) const onPress = () => navigation.push('Tab-Shared-Account', { account }) @@ -145,6 +150,30 @@ const TimelineActioned: React.FC = ({ action, isNotification, ...rest }) {content(t('shared.actioned.update'))} ) + case 'admin.sign_up': + return ( + <> + + {content(t('shared.actioned.admin.sign_up', { name: `@${account.acct}` }))} + + ) + case 'admin.report': + return ( + <> + + {content(t('shared.actioned.admin.report', { name: `@${account.acct}` }))} + + ) default: return <> } diff --git a/src/components/Timeline/Shared/Actions.tsx b/src/components/Timeline/Shared/Actions.tsx index 0991ee89..a803dd45 100644 --- a/src/components/Timeline/Shared/Actions.tsx +++ b/src/components/Timeline/Shared/Actions.tsx @@ -17,7 +17,7 @@ import { uniqBy } from 'lodash' import React, { useCallback, useContext, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { Pressable, StyleSheet, View } from 'react-native' -import { useQueryClient } from 'react-query' +import { useQueryClient } from '@tanstack/react-query' import { useSelector } from 'react-redux' import StatusContext from './Context' diff --git a/src/components/Timeline/Shared/Attachment.tsx b/src/components/Timeline/Shared/Attachment.tsx index 4ff5ab84..174894cc 100644 --- a/src/components/Timeline/Shared/Attachment.tsx +++ b/src/components/Timeline/Shared/Attachment.tsx @@ -45,6 +45,21 @@ const TimelineAttachment = () => { } const [sensitiveShown, setSensitiveShown] = useState(defaultSensitive()) + // const testHorizontal: Mastodon.Attachment[] = Array(2).fill({ + // id: Math.random().toString(), + // type: 'image', + // url: 'https://images.unsplash.com/photo-1670870764013-f0e36aa376b0?w=1000', + // preview_url: 'https://images.unsplash.com/photo-1543968996-ee822b8176ba?w=300', + // meta: { original: { width: 1000, height: 625 } } + // }) + // const testVertical: Mastodon.Attachment[] = Array(7).fill({ + // id: Math.random().toString(), + // type: 'image', + // url: 'https://images.unsplash.com/photo-1670842587871-326b95acbc8c?w=1000', + // preview_url: 'https://images.unsplash.com/photo-1670833288990-64b2f4ef7290?w=300', + // meta: { original: { width: 987, height: 1480 } } + // }) + // @ts-ignore const imageUrls: RootStackParamList['Screen-ImagesViewer']['imageUrls'] = status.media_attachments .map(attachment => { diff --git a/src/components/Timeline/Shared/Attachment/Audio.tsx b/src/components/Timeline/Shared/Attachment/Audio.tsx index 83ec4f82..9e4cc767 100644 --- a/src/components/Timeline/Shared/Attachment/Audio.tsx +++ b/src/components/Timeline/Shared/Attachment/Audio.tsx @@ -8,7 +8,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react' import { AppState, AppStateStatus, StyleSheet, View } from 'react-native' import { Blurhash } from 'react-native-blurhash' import AttachmentAltText from './AltText' -import attachmentAspectRatio from './aspectRatio' +import { aspectRatio } from './dimensions' export interface Props { total: number @@ -64,7 +64,7 @@ const AttachmentAudio: React.FC = ({ total, index, sensitiveShown, audio styles.base, { backgroundColor: colors.disabled, - aspectRatio: attachmentAspectRatio({ total, index }) + aspectRatio: aspectRatio({ total, index, ...audio.meta?.original }) } ]} > diff --git a/src/components/Timeline/Shared/Attachment/Image.tsx b/src/components/Timeline/Shared/Attachment/Image.tsx index 814d41b2..1069a36a 100644 --- a/src/components/Timeline/Shared/Attachment/Image.tsx +++ b/src/components/Timeline/Shared/Attachment/Image.tsx @@ -1,9 +1,10 @@ import GracefullyImage from '@components/GracefullyImage' import { StyleConstants } from '@utils/styles/constants' +import { useTheme } from '@utils/styles/ThemeManager' import React from 'react' import { View } from 'react-native' import AttachmentAltText from './AltText' -import attachmentAspectRatio from './aspectRatio' +import { aspectRatio } from './dimensions' export interface Props { total: number @@ -20,6 +21,8 @@ const AttachmentImage = ({ image, navigateToImagesViewer }: Props) => { + const { colors } = useTheme() + return ( - ) diff --git a/src/components/Timeline/Shared/Attachment/Unsupported.tsx b/src/components/Timeline/Shared/Attachment/Unsupported.tsx index f1c7f347..7fa9d89d 100644 --- a/src/components/Timeline/Shared/Attachment/Unsupported.tsx +++ b/src/components/Timeline/Shared/Attachment/Unsupported.tsx @@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next' import { View } from 'react-native' import { Blurhash } from 'react-native-blurhash' import AttachmentAltText from './AltText' -import attachmentAspectRatio from './aspectRatio' +import { aspectRatio } from './dimensions' export interface Props { total: number @@ -29,7 +29,7 @@ const AttachmentUnsupported: React.FC = ({ total, index, sensitiveShown, padding: StyleConstants.Spacing.XS / 2, justifyContent: 'center', alignItems: 'center', - aspectRatio: attachmentAspectRatio({ total, index }) + aspectRatio: aspectRatio({ total, index, ...attachment.meta?.original }) }} > {attachment.blurhash ? ( diff --git a/src/components/Timeline/Shared/Attachment/Video.tsx b/src/components/Timeline/Shared/Attachment/Video.tsx index 518a5b78..78e53ac4 100644 --- a/src/components/Timeline/Shared/Attachment/Video.tsx +++ b/src/components/Timeline/Shared/Attachment/Video.tsx @@ -4,9 +4,10 @@ import { ResizeMode, Video, VideoFullscreenUpdate } from 'expo-av' import React, { useRef, useState } from 'react' import { Pressable, View } from 'react-native' import { Blurhash } from 'react-native-blurhash' -import attachmentAspectRatio from './aspectRatio' import AttachmentAltText from './AltText' import { Platform } from 'expo-modules-core' +import { useAccessibility } from '@utils/accessibility/AccessibilityManager' +import { aspectRatio } from './dimensions' export interface Props { total: number @@ -23,6 +24,8 @@ const AttachmentVideo: React.FC = ({ video, gifv = false }) => { + const { reduceMotionEnabled } = useAccessibility() + const videoPlayer = useRef

Це демо дмух: smiling_face_with_smiling_eyes:. Ви можете вибрати з декількох варіантів знизу.

Це налаштування впливає лише на основний вміст дмухів, але не на інші розміри шрифту.

", + "sizes": { + "S": "S", + "M": "М - за замовчуванням", + "L": "L", + "XL": "XL", + "XXL": "XXL" + } + }, + "listAccounts": { + "heading": "Керувати користувачами", + "error": "Видалити користувача зі списку", + "empty": "Користувача не додано в цей список" + }, + "listEdit": { + "heading": "Редагувати деталі списку", + "title": "Назва", + "repliesPolicy": { + "heading": "Показати відповіді від:", + "options": { + "none": "Ніхто", + "list": "Користувачів списку", + "followed": "Будь-який користувач, за яким ви стежите" + } + } + }, + "listDelete": { + "heading": "Видалити список", + "confirm": { + "title": "Видалити список \"{{list}}\"?", + "message": "Цю дію не можна скасувати." + } + }, + "profile": { + "feedback": { + "succeed": "{{type}} оновлено", + "failed": "{{type}} оновлення не вдалося, будь ласка, спробуйте ще раз" + }, + "root": { + "name": { + "title": "Ім'я для відображення" + }, + "avatar": { + "title": "Аватар", + "description": "Буде зменшено до 400x400px" + }, + "header": { + "title": "Банер", + "description": "Буде зменшено до 1500x500px" + }, + "note": { + "title": "Опис" + }, + "fields": { + "title": "Метадані", + "total_one": "{{count}} поле", + "total_other": "{{count}} полей" + }, + "visibility": { + "title": "Видимість дмухів", + "options": { + "public": "Публічний", + "unlisted": "Приватний", + "private": "Тільки для підписників" + } + }, + "sensitive": { + "title": "Розміщення чутливої інформації" + }, + "lock": { + "title": "Закрити акаунт", + "description": "Потребує вас вручну затвердити підписників" + }, + "bot": { + "title": "Акаунт бота", + "description": "Цей акаунт виконує автоматичні дії та може не відстежуватися" + } + }, + "fields": { + "group": "Група {{index}}", + "label": "Мітка", + "content": "Контент" + }, + "mediaSelectionFailed": "Не вдалося виконати обробку зображення. Будь ласка, спробуйте ще раз." + }, + "push": { + "notAvailable": "Ваш телефон не підтримує push-сповіщення tooot", + "enable": { + "direct": "Увімкнути push сповіщення", + "settings": "Включити в налаштуваннях" + }, + "missingServerKey": { + "message": "Сервер неправильно налаштовано для push", + "description": "Будь ласка, зверніться до адміністратора сервера для налаштування підтримки push" + }, + "global": { + "heading": "Увімкнути для {{acct}}", + "description": "Повідомлення перенаправляються через сервер tooot" + }, + "decode": { + "heading": "Показати деталі повідомлення", + "description": "Повідомлення, які передаються через сервер tooot, зашифровані, але ви можете декодувати повідомлення на сервері. Наш вихідний код сервера є відкритим вихідним кодом і не має політики журналу." + }, + "default": { + "heading": "За замовчуванням" + }, + "follow": { + "heading": "Новий підписник(-ця)" + }, + "follow_request": { + "heading": "Запит на підписку" + }, + "favourite": { + "heading": "Улюблене" + }, + "reblog": { + "heading": "Передмухнув" + }, + "mention": { + "heading": "згадав (-ла) вас" + }, + "poll": { + "heading": "Опитування успішно оновлено" + }, + "status": { + "heading": "Дмух від підписників" + }, + "admin.sign_up": { + "heading": "Адмін: реєстрація" + }, + "admin.report": { + "heading": "Адмін: звіти" + }, + "howitworks": "Дізнатися, як працює маршрутизація" + }, + "root": { + "announcements": { + "content": { + "unread": "{{amount}} непрочитаних", + "read": "Усі прочитані", + "empty": "Відсутньо" + } + }, + "push": { + "content_true": "Увімкнено", + "content_false": "Вимкнено" + }, + "update": { + "title": "Оновити до останньої версії" + }, + "logout": { + "button": "Вийти", + "alert": { + "title": "Виходиш з системи?", + "message": "Після виходу з системи необхідно повторно увійти в неї", + "buttons": { + "logout": "Вийти" + } + } + } + }, + "settings": { + "fontsize": { + "heading": "$t(me.stacks.fontSize.name)", + "content": { + "S": "$t(me.fontSize.sizes.S)", + "M": "$t(me.fontSize.sizes.M)", + "L": "$t(me.fontSize.sizes.L)", + "XL": "$t(me.fontSize.sizes.XL)", + "XXL": "$t(me.fontSize.sizes.XXL)" + } + }, + "language": { + "heading": "$t(me.stacks.language.name)" + }, + "theme": { + "heading": "Зовнішній вигляд", + "options": { + "auto": "За системними налаштуваннями", + "light": "Світла тема", + "dark": "Темний режим" + } + }, + "darkTheme": { + "heading": "Темна тема", + "options": { + "lighter": "Світліше", + "darker": "Темніше" + } + }, + "browser": { + "heading": "Відкривання посилань", + "options": { + "internal": "Всередині додатку", + "external": "Використовувати системний браузер" + } + }, + "staticEmoji": { + "heading": "Використовувати статичні емодзі", + "description": "Якщо ви стикаєтеся з частими збоями в роботі програми при перегляді списку емодзі, ви можете спробувати використовувати статичні емодзі." + }, + "feedback": { + "heading": "Запропонувати ідею" + }, + "support": { + "heading": "Підтримати tooot" + }, + "contact": { + "heading": "Зв'язатись з tooot" + }, + "version": "Версія: {{version}}", + "instanceVersion": "Версія Mastodon: v{{version}}" + }, + "switch": { + "existing": "Вибрати з авторизованих", + "new": "Увійти до інстансу" + } + }, + "shared": { + "account": { + "actions": { + "accessibilityLabel": "Дії для користувача {{user}}", + "accessibilityHint": "Ви можете приглушити, заблокувати, чи кинути репорт на цього користувача" + }, + "followed_by": " слідує за вами", + "moved": "Користувач перемістився", + "created_at": "Приєднався: {{date}}", + "summary": { + "statuses_count": "{{count}} дмухів", + "following_count": "$t(shared.users.accounts.following)", + "followers_count": "$t(shared.users.accounts.followers)" + }, + "toots": { + "default": "Дмухи", + "all": "Дмухи та відповіді" + }, + "suspended": "Обліковий запис призупинено модераторами вашого інстансу" + }, + "accountInLists": { + "name": "Списки @{{username}}", + "inLists": "У списку", + "notInLists": "Інші списки" + }, + "attachments": { + "name": "<0 /><1> медіа" + }, + "hashtag": { + "follow": "Підписатися", + "unfollow": "Відписатися" + }, + "history": { + "name": "Редагувати історію" + }, + "search": { + "header": { + "prefix": "Пошук", + "placeholder": "кого?.." + }, + "empty": { + "general": "Введіть ключове слово для пошуку $t(screenTabs:shared.search.sections.accounts)$t(screenTabs:shared.search.sections.hashtags) або $t(screenTabs:shared.search.sections.statuses)", + "advanced": { + "header": "Розширений пошук", + "example": { + "account": "$t(shared.search.header.prefix) $t(shared.search.sections.accounts)", + "hashtag": "$t(shared.search.header.prefix) $t(shared.search.sections.hashtags)", + "statusLink": "$t(shared.search.header.prefix) $t(shared.search.sections.statuses)", + "accountLink": "$t(shared.search.header.prefix) $t(shared.search.sections.accounts)" + } + }, + "trending": { + "tags": "Популярні теги" + } + }, + "sections": { + "accounts": "Користувач", + "hashtags": "Ґештег", + "statuses": "Дмух" + }, + "notFound": "Не вдалося знайти {{searchTerm}} пов'язаний з {{type}}" + }, + "toot": { + "name": "Обговорення" + }, + "users": { + "accounts": { + "following": "Підписки {{count}}", + "followers": "{{count}} підписників" + }, + "statuses": { + "reblogged_by": "{{count}} передмухів", + "favourited_by": "{{count}} улюблених" + }, + "resultIncomplete": "Результати з віддаленого інстанса неповні" + } + } +} \ No newline at end of file diff --git a/src/i18n/vi/common.json b/src/i18n/vi/common.json index 421903ba..517903aa 100644 --- a/src/i18n/vi/common.json +++ b/src/i18n/vi/common.json @@ -5,6 +5,7 @@ "cancel": "Hủy bỏ", "discard": "Bỏ qua", "continue": "Tiếp tục", + "create": "Tạo", "delete": "Xóa", "done": "Xong" }, diff --git a/src/i18n/vi/components/instance.json b/src/i18n/vi/components/instance.json index b03df3c8..acca9aa1 100644 --- a/src/i18n/vi/components/instance.json +++ b/src/i18n/vi/components/instance.json @@ -1,7 +1,7 @@ { "server": { "textInput": { - "placeholder": "Máy chủ" + "placeholder": "Địa chỉ máy chủ" }, "button": "Đăng nhập", "information": { diff --git a/src/i18n/vi/components/timeline.json b/src/i18n/vi/components/timeline.json index 3dba5244..dc0be9f4 100644 --- a/src/i18n/vi/components/timeline.json +++ b/src/i18n/vi/components/timeline.json @@ -30,7 +30,9 @@ "default": "{{name}} đăng lại", "notification": "{{name}} đăng lại tút của bạn" }, - "update": "Đăng lại đã được sửa" + "update": "Đăng lại đã được sửa", + "admin.sign_up": "{{name}} tham gia máy chủ", + "admin.report": "{{name}} báo cáo:" }, "actions": { "reply": { @@ -52,7 +54,8 @@ "bookmarked": { "accessibilityLabel": "Lưu tút này", "function": "Lưu tút" - } + }, + "openReport": "Mở báo cáo" }, "actionsUsers": { "reblogged_by": { diff --git a/src/i18n/vi/screens/actions.json b/src/i18n/vi/screens/actions.json index 516c6077..5551f751 100644 --- a/src/i18n/vi/screens/actions.json +++ b/src/i18n/vi/screens/actions.json @@ -2,19 +2,6 @@ "content": { "altText": { "heading": "Văn bản thay thế" - }, - "notificationsFilter": { - "heading": "Những kiểu thông báo cho phép", - "content": { - "follow": "$t(screenTabs:me.push.follow.heading)", - "follow_request": "Yêu cầu theo dõi", - "favourite": "$t(screenTabs:me.push.favourite.heading)", - "reblog": "$t(screenTabs:me.push.reblog.heading)", - "mention": "$t(screenTabs:me.push.mention.heading)", - "poll": "$t(screenTabs:me.push.poll.heading)", - "status": "Tút từ người đã theo dõi", - "update": "Đăng lại đã được sửa" - } } } } \ No newline at end of file diff --git a/src/i18n/vi/screens/compose.json b/src/i18n/vi/screens/compose.json index 83b5901c..a2d38e04 100644 --- a/src/i18n/vi/screens/compose.json +++ b/src/i18n/vi/screens/compose.json @@ -1,13 +1,11 @@ { "heading": { "left": { - "button": "Hủy bỏ", "alert": { "title": "Không đăng nữa?", "buttons": { "save": "Lưu bản nháp", - "delete": "Xóa bản nháp", - "cancel": "Hủy bỏ" + "delete": "Xóa bản nháp" } } }, diff --git a/src/i18n/vi/screens/tabs.json b/src/i18n/vi/screens/tabs.json index b198254e..342857ef 100644 --- a/src/i18n/vi/screens/tabs.json +++ b/src/i18n/vi/screens/tabs.json @@ -1,20 +1,21 @@ { "tabs": { "local": { - "name": "Đang theo dõi" + "name": "Đang theo dõi", + "options": { + "showBoosts": "Hiện lượt đăng lại", + "showReplies": "Hiện lượt trả lời" + } }, "public": { - "name": "", "segments": { - "left": "Liên hợp", - "right": "Máy chủ" + "federated": "Liên hợp", + "local": "Máy chủ", + "trending": "Xu hướng" } }, "notifications": { "name": "Thông báo" - }, - "me": { - "name": "Tôi" } }, "common": { @@ -24,9 +25,22 @@ } }, "notifications": { - "filter": { + "filters": { "accessibilityLabel": "Lọc", - "accessibilityHint": "Lọc các dạng thông báo" + "accessibilityHint": "Lọc các dạng thông báo", + "title": "Hiện thông báo", + "options": { + "follow": "$t(screenTabs:me.push.follow.heading)", + "follow_request": "Yêu cầu theo dõi", + "favourite": "$t(screenTabs:me.push.favourite.heading)", + "reblog": "$t(screenTabs:me.push.reblog.heading)", + "mention": "$t(screenTabs:me.push.mention.heading)", + "poll": "$t(screenTabs:me.push.poll.heading)", + "status": "Tút từ người đã theo dõi", + "update": "Đăng lại đã được sửa", + "admin.sign_up": "$t(screenTabs:me.push.admin.sign_up.heading)", + "admin.report": "$t(screenTabs:me.push.admin.report.heading)" + } } }, "me": { @@ -40,6 +54,9 @@ "favourites": { "name": "Đã thích" }, + "followedTags": { + "name": "Hashtag theo dõi" + }, "fontSize": { "name": "Cỡ chữ" }, @@ -53,7 +70,7 @@ "name": "Người trong danh sách: {{list}}" }, "listAdd": { - "name": "Thêm một danh sách" + "name": "Tạo danh sách" }, "listEdit": { "name": "Sửa danh sách" @@ -179,8 +196,8 @@ "settings": "Bật trong cài đặt" }, "missingServerKey": { - "message": "", - "description": "" + "message": "Máy chủ định cấu hình sai để nhận thông báo đẩy", + "description": "Vui lòng liên hệ với quản trị viên máy chủ của bạn để định cấu hình thông báo đẩy" }, "global": { "heading": "Bật cho {{acct}}", @@ -214,6 +231,12 @@ "status": { "heading": "Tút từ người đã theo dõi" }, + "admin.sign_up": { + "heading": "Admin: đăng ký" + }, + "admin.report": { + "heading": "Admin: báo cáo" + }, "howitworks": "Tìm hiểu cách truyền" }, "root": { @@ -225,10 +248,8 @@ } }, "push": { - "content": { - "enabled": "Đã bật", - "disabled": "Đã tắt" - } + "content_true": "Bật", + "content_false": "Tắt" }, "update": { "title": "Cập nhật phiên bản mới" @@ -290,9 +311,6 @@ "support": { "heading": "Ủng hộ tooot" }, - "review": { - "heading": "Đánh giá tooot" - }, "contact": { "heading": "Liên hệ tooot" }, @@ -377,7 +395,8 @@ "statuses": { "reblogged_by": "{{count}} đăng lại", "favourited_by": "{{count}} thích" - } + }, + "resultIncomplete": "Kết quả từ máy chủ khác luôn không đầy đủ" } } } \ No newline at end of file diff --git a/src/i18n/zh-Hans/common.json b/src/i18n/zh-Hans/common.json index a3c7dd6d..7e8894a1 100644 --- a/src/i18n/zh-Hans/common.json +++ b/src/i18n/zh-Hans/common.json @@ -5,6 +5,7 @@ "cancel": "取消", "discard": "丢弃", "continue": "继续", + "create": "创建", "delete": "删除", "done": "完成" }, diff --git a/src/i18n/zh-Hans/components/instance.json b/src/i18n/zh-Hans/components/instance.json index abf40b9b..4fdc13f7 100644 --- a/src/i18n/zh-Hans/components/instance.json +++ b/src/i18n/zh-Hans/components/instance.json @@ -1,7 +1,7 @@ { "server": { "textInput": { - "placeholder": "输入社区服务器地址" + "placeholder": "实例域名" }, "button": "登录", "information": { diff --git a/src/i18n/zh-Hans/components/timeline.json b/src/i18n/zh-Hans/components/timeline.json index 6725dc0d..404ad280 100644 --- a/src/i18n/zh-Hans/components/timeline.json +++ b/src/i18n/zh-Hans/components/timeline.json @@ -30,7 +30,9 @@ "default": "{{name}} 转嘟了", "notification": "{{name}} 转嘟了你的嘟文" }, - "update": "转嘟已被编辑" + "update": "转嘟已被编辑", + "admin.sign_up": "{{name}}加入了实例", + "admin.report": "{{name}}举报:" }, "actions": { "reply": { @@ -52,7 +54,8 @@ "bookmarked": { "accessibilityLabel": "添加此嘟文至书签列表", "function": "喜欢嘟文" - } + }, + "openReport": "打开举报" }, "actionsUsers": { "reblogged_by": { diff --git a/src/i18n/zh-Hans/screens/actions.json b/src/i18n/zh-Hans/screens/actions.json index 56c57a99..3e66a95c 100644 --- a/src/i18n/zh-Hans/screens/actions.json +++ b/src/i18n/zh-Hans/screens/actions.json @@ -2,19 +2,6 @@ "content": { "altText": { "heading": "替代文本" - }, - "notificationsFilter": { - "heading": "显示通知", - "content": { - "follow": "$t(screenTabs:me.push.follow.heading)", - "follow_request": "关注请求", - "favourite": "$t(screenTabs:me.push.favourite.heading)", - "reblog": "$t(screenTabs:me.push.reblog.heading)", - "mention": "$t(screenTabs:me.push.mention.heading)", - "poll": "$t(screenTabs:me.push.poll.heading)", - "status": "订阅用户的嘟文", - "update": "转嘟被编辑" - } } } } \ No newline at end of file diff --git a/src/i18n/zh-Hans/screens/compose.json b/src/i18n/zh-Hans/screens/compose.json index ba2d5dc3..cbe69118 100644 --- a/src/i18n/zh-Hans/screens/compose.json +++ b/src/i18n/zh-Hans/screens/compose.json @@ -1,13 +1,11 @@ { "heading": { "left": { - "button": "退出编辑", "alert": { "title": "确认退出编辑?", "buttons": { "save": "保存草稿", - "delete": "删除草稿", - "cancel": "继续编辑" + "delete": "删除草稿" } } }, diff --git a/src/i18n/zh-Hans/screens/tabs.json b/src/i18n/zh-Hans/screens/tabs.json index bfbd2a44..6f46a372 100644 --- a/src/i18n/zh-Hans/screens/tabs.json +++ b/src/i18n/zh-Hans/screens/tabs.json @@ -1,20 +1,21 @@ { "tabs": { "local": { - "name": "我的关注" + "name": "我的关注", + "options": { + "showBoosts": "显示转嘟", + "showReplies": "显示回复" + } }, "public": { - "name": "", "segments": { - "left": "跨站嘟嘟", - "right": "本站嘟嘟" + "federated": "跨站", + "local": "本站", + "trending": "热门" } }, "notifications": { "name": "通知" - }, - "me": { - "name": "关于我" } }, "common": { @@ -24,9 +25,22 @@ } }, "notifications": { - "filter": { + "filters": { "accessibilityLabel": "筛选", - "accessibilityHint": "筛选显示的通知类型" + "accessibilityHint": "筛选显示的通知类型", + "title": "显示通知", + "options": { + "follow": "$t(screenTabs:me.push.follow.heading)", + "follow_request": "关注请求", + "favourite": "$t(screenTabs:me.push.favourite.heading)", + "reblog": "$t(screenTabs:me.push.reblog.heading)", + "mention": "$t(screenTabs:me.push.mention.heading)", + "poll": "$t(screenTabs:me.push.poll.heading)", + "status": "订阅用户的嘟文", + "update": "转嘟被编辑", + "admin.sign_up": "$t(screenTabs:me.push.admin.sign_up.heading)", + "admin.report": "$t(screenTabs:me.push.admin.report.heading)" + } } }, "me": { @@ -40,6 +54,9 @@ "favourites": { "name": "喜欢" }, + "followedTags": { + "name": "关注的话题标签" + }, "fontSize": { "name": "嘟文字号" }, @@ -53,7 +70,7 @@ "name": "列表中的用户:{{list}}" }, "listAdd": { - "name": "添加列表" + "name": "创建列表" }, "listEdit": { "name": "编辑列表详情" @@ -214,6 +231,12 @@ "status": { "heading": "订阅用户的嘟文" }, + "admin.sign_up": { + "heading": "管理员:用户注册" + }, + "admin.report": { + "heading": "管理员:报告" + }, "howitworks": "了解通知消息转发如何工作" }, "root": { @@ -225,10 +248,8 @@ } }, "push": { - "content": { - "enabled": "已启用", - "disabled": "未启用" - } + "content_true": "已启用", + "content_false": "未启用" }, "update": { "title": "更新至最新版本" @@ -290,9 +311,6 @@ "support": { "heading": "赞助 tooot 开发" }, - "review": { - "heading": "给 tooot 打分" - }, "contact": { "heading": "联系 tooot" }, @@ -377,7 +395,8 @@ "statuses": { "reblogged_by": "{{count}} 人转嘟", "favourited_by": "{{count}} 人喜欢" - } + }, + "resultIncomplete": "来自远程实例的结果不完整" } } } \ No newline at end of file diff --git a/src/i18n/zh-Hant/common.json b/src/i18n/zh-Hant/common.json index 6132e49a..12e5bc66 100644 --- a/src/i18n/zh-Hant/common.json +++ b/src/i18n/zh-Hant/common.json @@ -5,6 +5,7 @@ "cancel": "取消", "discard": "不儲存", "continue": "繼續", + "create": "建立", "delete": "刪除", "done": "完成" }, diff --git a/src/i18n/zh-Hant/components/timeline.json b/src/i18n/zh-Hant/components/timeline.json index a483f163..f26ff232 100644 --- a/src/i18n/zh-Hant/components/timeline.json +++ b/src/i18n/zh-Hant/components/timeline.json @@ -30,7 +30,9 @@ "default": "{{name}} 轉嘟了", "notification": "{{name}} 轉嘟了您的嘟文" }, - "update": "轉嘟已編輯" + "update": "轉嘟已編輯", + "admin.sign_up": "", + "admin.report": "" }, "actions": { "reply": { @@ -52,7 +54,8 @@ "bookmarked": { "accessibilityLabel": "將嘟文加入書籤", "function": "加入書籤" - } + }, + "openReport": "" }, "actionsUsers": { "reblogged_by": { diff --git a/src/i18n/zh-Hant/screens/actions.json b/src/i18n/zh-Hant/screens/actions.json index d5187c30..0ae1bdd9 100644 --- a/src/i18n/zh-Hant/screens/actions.json +++ b/src/i18n/zh-Hant/screens/actions.json @@ -2,19 +2,6 @@ "content": { "altText": { "heading": "替代文字" - }, - "notificationsFilter": { - "heading": "顯示通知類型", - "content": { - "follow": "$t(screenTabs:me.push.follow.heading)", - "follow_request": "跟隨請求", - "favourite": "$t(screenTabs:me.push.favourite.heading)", - "reblog": "$t(screenTabs:me.push.reblog.heading)", - "mention": "$t(screenTabs:me.push.mention.heading)", - "poll": "$t(screenTabs:me.push.poll.heading)", - "status": "訂閱使用者的嘟文", - "update": "轉嘟被編輯" - } } } } \ No newline at end of file diff --git a/src/i18n/zh-Hant/screens/compose.json b/src/i18n/zh-Hant/screens/compose.json index 4e374c29..78d25b7f 100644 --- a/src/i18n/zh-Hant/screens/compose.json +++ b/src/i18n/zh-Hant/screens/compose.json @@ -1,13 +1,11 @@ { "heading": { "left": { - "button": "取消", "alert": { "title": "確認取消編輯?", "buttons": { "save": "儲存草稿", - "delete": "刪除草稿", - "cancel": "取消" + "delete": "刪除草稿" } } }, diff --git a/src/i18n/zh-Hant/screens/tabs.json b/src/i18n/zh-Hant/screens/tabs.json index 5b715fdb..04f2cd86 100644 --- a/src/i18n/zh-Hant/screens/tabs.json +++ b/src/i18n/zh-Hant/screens/tabs.json @@ -1,20 +1,21 @@ { "tabs": { "local": { - "name": "跟隨中" + "name": "跟隨中", + "options": { + "showBoosts": "顯示轉嘟", + "showReplies": "顯示回覆" + } }, "public": { - "name": "", "segments": { - "left": "聯邦宇宙", - "right": "本站" + "federated": "聯邦", + "local": "本站", + "trending": "熱門" } }, "notifications": { "name": "通知" - }, - "me": { - "name": "關於我" } }, "common": { @@ -24,9 +25,22 @@ } }, "notifications": { - "filter": { + "filters": { "accessibilityLabel": "過濾器", - "accessibilityHint": "過濾顯示的通知類型" + "accessibilityHint": "過濾顯示的通知類型", + "title": "顯示通知", + "options": { + "follow": "$t(screenTabs:me.push.follow.heading)", + "follow_request": "跟隨請求", + "favourite": "$t(screenTabs:me.push.favourite.heading)", + "reblog": "$t(screenTabs:me.push.reblog.heading)", + "mention": "$t(screenTabs:me.push.mention.heading)", + "poll": "$t(screenTabs:me.push.poll.heading)", + "status": "訂閱使用者的嘟文", + "update": "轉嘟已編輯", + "admin.sign_up": "$t(screenTabs:me.push.admin.sign_up.heading)", + "admin.report": "$t(screenTabs:me.push.admin.report.heading)" + } } }, "me": { @@ -40,6 +54,9 @@ "favourites": { "name": "最愛" }, + "followedTags": { + "name": "跟隨主題標籤" + }, "fontSize": { "name": "嘟文字體大小" }, @@ -53,7 +70,7 @@ "name": "使用者在 {{list}} 列表中" }, "listAdd": { - "name": "新增列表" + "name": "建立列表" }, "listEdit": { "name": "編輯列表詳細資料" @@ -179,8 +196,8 @@ "settings": "在設定中啟用" }, "missingServerKey": { - "message": "", - "description": "" + "message": "伺服器推播設定不正確", + "description": "請聯絡您的伺服器管理員啟用推播設定" }, "global": { "heading": "啟用 {{acct}}", @@ -214,6 +231,12 @@ "status": { "heading": "訂閱使用者的嘟文" }, + "admin.sign_up": { + "heading": "管理員:註冊" + }, + "admin.report": { + "heading": "管理員:回報" + }, "howitworks": "了解通知訊息轉發如何工作" }, "root": { @@ -225,10 +248,8 @@ } }, "push": { - "content": { - "enabled": "啟用", - "disabled": "關閉" - } + "content_true": "啟用", + "content_false": "關閉" }, "update": { "title": "已更新到最新版本" @@ -290,9 +311,6 @@ "support": { "heading": "贊助 tooot 開發" }, - "review": { - "heading": "給 tooot 評分" - }, "contact": { "heading": "聯繫 tooot" }, @@ -377,7 +395,8 @@ "statuses": { "reblogged_by": "{{count}} 人轉嘟", "favourited_by": "{{count}} 人喜歡" - } + }, + "resultIncomplete": "" } } } \ No newline at end of file diff --git a/src/screens/Actions.tsx b/src/screens/Actions.tsx index 54626356..0a8b3c3a 100644 --- a/src/screens/Actions.tsx +++ b/src/screens/Actions.tsx @@ -15,7 +15,6 @@ import Animated, { } from 'react-native-reanimated' import { useSafeAreaInsets } from 'react-native-safe-area-context' import ActionsAltText from './Actions/AltText' -import ActionsNotificationsFilter from './Actions/NotificationsFilter' const ScreenActions = ({ route: { params }, @@ -53,8 +52,6 @@ const ScreenActions = ({ const actions = () => { switch (params.type) { - case 'notifications_filter': - return case 'alt_text': return } diff --git a/src/screens/Actions/NotificationsFilter.tsx b/src/screens/Actions/NotificationsFilter.tsx deleted file mode 100644 index 928bb5ae..00000000 --- a/src/screens/Actions/NotificationsFilter.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import Button from '@components/Button' -import MenuContainer from '@components/Menu/Container' -import MenuHeader from '@components/Menu/Header' -import MenuRow from '@components/Menu/Row' -import { useNavigation } from '@react-navigation/native' -import { - checkInstanceFeature, - getInstanceNotificationsFilter, - updateInstanceNotificationsFilter -} from '@utils/slices/instancesSlice' -import { StyleConstants } from '@utils/styles/constants' -import React, { useMemo } from 'react' -import { useTranslation } from 'react-i18next' -import { useSelector } from 'react-redux' -import { useQueryClient } from 'react-query' -import { QueryKeyTimeline } from '@utils/queryHooks/timeline' -import { useAppDispatch } from '@root/store' - -const ActionsNotificationsFilter: React.FC = () => { - const navigation = useNavigation() - const { t } = useTranslation('screenActions') - - const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Notifications' }] - const queryClient = useQueryClient() - - const dispatch = useAppDispatch() - const instanceNotificationsFilter = useSelector( - getInstanceNotificationsFilter - ) - - if (!instanceNotificationsFilter) { - navigation.goBack() - return null - } - - const hasTypeStatus = useSelector( - checkInstanceFeature('notification_type_status') - ) - const hasTypeUpdate = useSelector( - checkInstanceFeature('notification_type_update') - ) - const options = useMemo(() => { - return ( - instanceNotificationsFilter && - ( - [ - 'follow', - 'follow_request', - 'favourite', - 'reblog', - 'mention', - 'poll', - 'status', - 'update' - ] as [ - 'follow', - 'follow_request', - 'favourite', - 'reblog', - 'mention', - 'poll', - 'status', - 'update' - ] - ) - .filter(type => { - switch (type) { - case 'status': - return hasTypeStatus - case 'update': - return hasTypeUpdate - default: - return true - } - }) - .map(type => ( - - dispatch( - updateInstanceNotificationsFilter({ - ...instanceNotificationsFilter, - [type]: !instanceNotificationsFilter[type] - }) - ) - } - /> - )) - ) - }, [instanceNotificationsFilter, hasTypeStatus, hasTypeUpdate]) - - return ( - <> - - - {options} - -