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

Merge pull request #559 from tooot-app/main

Release v4.7.0
This commit is contained in:
xmflsct
2022-12-16 00:26:36 +01:00
committed by GitHub
327 changed files with 5667 additions and 4151 deletions

View File

@ -1,3 +0,0 @@
{
"javascript.inlayHints.functionLikeReturnTypes.enabled": false
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

BIN
demo/screenshots/Tab-Me.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

View File

@ -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:
'<p>Would you like to try out this simple, beautiful and open-source mobile app for Mastodon? 😊</p>',
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:
'<p>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.</p>',
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: '<p></p>',
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:
'<p>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.</p>',
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:
'<p>tooot is an open source, simple mobile client for Mastodon. Focusing on your connections while being able to explore the Fediverse.</p>',
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:
'<p>- tooot supports multiple accounts<br />- tooot supports browsing external instance<br />- tooot aims to support multiple languages</p>',
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

View File

@ -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

View File

@ -1,3 +1,11 @@
Enjoy toooting! This version includes following improvements and fixes:
- Fix toot attribution of favourites etc.
- Fix switching language
- 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

View File

@ -1,3 +1,11 @@
toooting愉快此版本包括以下改进和修复
- 修复嘟文收藏等显示
- 修复不能切换语言
- 增加 🇺🇦 Slava Ukraini
- 自动识别发嘟语言
- 记住上次公共时间轴选项
- 显示编辑历史的差异
- 关注列表可隐藏转嘟和回复
- 新增管理员推送通知
- 支持嘟文右到左文字
- 测试显示对话层级
- 修复过滤整词功能
- 修复平板不能删除草稿

View File

@ -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

View File

@ -86,6 +86,7 @@
E69EBACE28DF28560057EDEC /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/InfoPlist.strings; sourceTree = "<group>"; };
E6A4895D293C1F740047951A /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/InfoPlist.strings; sourceTree = "<group>"; };
E6C8B26628F5F9FC0062CF2E /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = "<group>"; };
E6D64C7A294A90840098F3AC /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/InfoPlist.strings; sourceTree = "<group>"; };
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 = "<group>";
@ -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";
};

View File

@ -0,0 +1,2 @@
"NSPhotoLibraryAddUsageDescription" = "Дозвольте tooot зберігати зображення у вашій папці фотоапарата";
"NSPhotoLibraryUsageDescription" = "Дозвольте tooot зберігати зображення у вашій папці фотоапарата";

View File

@ -1,6 +1,6 @@
{
"name": "tooot",
"version": "4.6.6",
"version": "4.7.0",
"description": "tooot for Mastodon",
"author": "xmflsct <me@xmflsct.com>",
"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"
}
}

5
src/@types/app.d.ts vendored
View File

@ -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'

View File

@ -30,6 +30,7 @@ declare namespace Mastodon {
bot: boolean
source?: Source
suspended?: boolean
role?: Role
}
type Announcement = {
@ -332,23 +333,33 @@ declare namespace Mastodon {
url: string
}
type Notification = {
type Notification =
| {
// Base
id: string
type:
| 'follow'
| 'follow_request'
| 'mention'
| 'reblog'
| 'favourite'
| 'poll'
| 'status'
| 'update'
type: 'favourite' | 'mention' | 'poll' | 'reblog' | 'status' | 'update'
created_at: string
account: Account
// Others
status?: Status
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 = {
@ -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
}
}

View File

@ -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 }[]

View File

@ -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'

View File

@ -135,10 +135,7 @@ const Screens: React.FC<Props> = ({ 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] })
}
}
}

View File

@ -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 <T = unknown>({
params,
headers,
body
}: Params): Promise<{ body: T }> => {
}: Params): Promise<PagedResponse<T>> => {
console.log(
ctx.bgGreen.bold(' API general ') +
' ' +
@ -47,9 +47,27 @@ const apiGeneral = async <T = unknown>({
...(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())
}

View File

@ -63,4 +63,10 @@ const handleError =
}
}
type LinkFormat = { id: string; isOffset: boolean }
export type PagedResponse<T = unknown> = {
body: T
links: { prev?: LinkFormat; next?: LinkFormat }
}
export { ctx, handleError, userAgent }

View File

@ -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<AxiosRequestConfig, 'method' | 'url' | 'params' | 'headers' | 'data'>
}
export type InstanceResponse<T = unknown> = {
body: T
links: { prev?: string; next?: string }
}
const apiInstance = async <T = unknown>({
method,
version = 'v1',
@ -28,7 +22,7 @@ const apiInstance = async <T = unknown>({
headers,
body,
extras
}: Params): Promise<InstanceResponse<T>> => {
}: Params): Promise<PagedResponse<T>> => {
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 <T = unknown>({
...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())
}

View File

@ -12,11 +12,7 @@ interface Props {
additionalActions?: () => void
}
const AccountButton: React.FC<Props> = ({
instance,
selected = false,
additionalActions
}) => {
const AccountButton: React.FC<Props> = ({ instance, selected = false, additionalActions }) => {
const navigation = useNavigation()
return (
@ -27,12 +23,10 @@ const AccountButton: React.FC<Props> = ({
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()

View File

@ -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 ? (
<Image
fadeDuration={0}
<FastImage
source={{ uri: uri.preview }}
style={[styles.placeholder, { backgroundColor: colors.shimmerDefault }]}
/>
) : null}
{Platform.OS === 'ios' ? (
<Image
fadeDuration={0}
source={source}
style={[{ flex: 1 }, imageStyle]}
onLoad={onLoad}
onError={onError}
/>
) : (
<FastImage
fadeDuration={0}
source={source}
// @ts-ignore
style={[{ flex: 1 }, imageStyle]}
onLoad={onLoad}
onError={onError}
/>
)}
{blurhashView}
</Pressable>
)

View File

@ -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<Props> = ({ hashtag, onPress: customOnPress }) => {
const ComponentHashtag: React.FC<PropsWithChildren & Props> = ({
hashtag,
onPress: customOnPress,
children
}) => {
const { colors } = useTheme()
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
@ -31,15 +35,11 @@ const ComponentHashtag: React.FC<Props> = ({ 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)}
>
<CustomText
fontStyle='M'
@ -52,11 +52,22 @@ const ComponentHashtag: React.FC<Props> = ({ hashtag, onPress: customOnPress })
>
#{hashtag.name}
</CustomText>
<View
style={{ flexDirection: 'row', alignItems: 'center' }}
onLayout={({
nativeEvent: {
layout: { height }
}
}) => setHeight(height)}
>
<Sparkline
data={hashtag.history.map(h => parseInt(h.uses)).reverse()}
width={width}
height={height}
margin={children ? StyleConstants.Spacing.S : undefined}
/>
{children}
</View>
</Pressable>
)
}

View File

@ -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<Props> = ({
content,
inverted = false,
onPress,
dropdown = false
}) => {
const HeaderCenter: React.FC<Props> = ({ content, inverted = false, onPress }) => {
const { colors } = useTheme()
return (
@ -33,13 +25,6 @@ const HeaderCenter: React.FC<Props> = ({
children={content}
{...(onPress && { onPress })}
/>
<Icon
name='ChevronDown'
size={StyleConstants.Font.Size.M}
color={colors.primaryDefault}
style={{ marginLeft: StyleConstants.Spacing.XS, opacity: dropdown ? undefined : 0 }}
strokeWidth={3}
/>
</View>
)
}

View File

@ -11,6 +11,7 @@ export interface Props {
fill?: string
strokeWidth?: number
style?: StyleProp<ViewStyle>
crossOut?: boolean
}
const Icon: React.FC<Props> = ({
@ -20,7 +21,8 @@ const Icon: React.FC<Props> = ({
color,
fill,
strokeWidth = 2,
style
style,
crossOut = false
}) => {
return (
<View
@ -42,6 +44,17 @@ const Icon: React.FC<Props> = ({
fill,
strokeWidth
})}
{crossOut ? (
<View
style={{
position: 'absolute',
transform: [{ rotate: '45deg' }],
width: size * 1.35,
borderBottomColor: color,
borderBottomWidth: strokeWidth
}}
/>
) : null}
</View>
)
}

View File

@ -1,90 +0,0 @@
import browserPackage from '@helpers/browserPackage'
import { useNavigation } from '@react-navigation/native'
import { useAppDispatch } from '@root/store'
import { InstanceLatest } from '@utils/migrations/instances/migration'
import { TabMeStackNavigationProp } from '@utils/navigation/navigators'
import addInstance from '@utils/slices/instances/add'
import { checkInstanceFeature } from '@utils/slices/instancesSlice'
import * as AuthSession from 'expo-auth-session'
import React, { useEffect } from 'react'
import { useQueryClient } from 'react-query'
import { useSelector } from 'react-redux'
export interface Props {
instanceDomain: string
// Domain can be different than uri
instance: Mastodon.Instance
appData: InstanceLatest['appData']
goBack?: boolean
}
const InstanceAuth = React.memo(
({ instanceDomain, instance, appData, goBack }: Props) => {
const redirectUri = AuthSession.makeRedirectUri({
native: 'tooot://instance-auth',
useProxy: false
})
const navigation = useNavigation<TabMeStackNavigationProp<'Tab-Me-Root' | 'Tab-Me-Switch'>>()
const queryClient = useQueryClient()
const dispatch = useAppDispatch()
const deprecateAuthFollow = useSelector(checkInstanceFeature('deprecate_auth_follow'))
const [request, response, promptAsync] = AuthSession.useAuthRequest(
{
clientId: appData.clientId,
clientSecret: appData.clientSecret,
scopes: deprecateAuthFollow
? ['read', 'write', 'push']
: ['read', 'write', 'follow', 'push'],
redirectUri
},
{
authorizationEndpoint: `https://${instanceDomain}/oauth/authorize`
}
)
useEffect(() => {
;(async () => {
if (request?.clientId) {
await promptAsync({ browserPackage: await browserPackage() }).catch(e => console.log(e))
}
})()
}, [request])
useEffect(() => {
;(async () => {
if (response?.type === 'success') {
const { accessToken } = await AuthSession.exchangeCodeAsync(
{
clientId: appData.clientId,
clientSecret: appData.clientSecret,
scopes: ['read', 'write', 'follow', 'push'],
redirectUri,
code: response.params.code,
extraParams: {
grant_type: 'authorization_code'
}
},
{
tokenEndpoint: `https://${instanceDomain}/oauth/token`
}
)
queryClient.clear()
dispatch(
addInstance({
domain: instanceDomain,
token: accessToken,
instance,
appData
})
)
goBack && navigation.goBack()
}
})()
}, [response])
return <></>
},
() => true
)
export default InstanceAuth

View File

@ -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<ScrollView>
@ -31,30 +36,64 @@ const ComponentInstance: React.FC<Props> = ({
}) => {
const { t } = useTranslation('componentInstance')
const { colors, mode } = useTheme()
const navigation = useNavigation<TabMeStackNavigationProp<'Tab-Me-Root' | 'Tab-Me-Switch'>>()
const [domain, setDomain] = useState<string>('')
const dispatch = useAppDispatch()
const instances = useSelector(getInstances, () => true)
const [domain, setDomain] = useState<string>()
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()
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' }
},
1000,
{ trailing: true }
),
[]
{ 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<Props> = ({
},
{
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 (
<InstanceAuth
key={Math.random()}
instanceDomain={domain}
instance={instanceQuery.data}
appData={{
clientId: appsQuery.data.client_id,
clientSecret: appsQuery.data.client_secret
}}
goBack={goBack}
/>
)
}
}, [domain, instanceQuery.data, appsQuery.data])
return (
<KeyboardAvoidingView
style={{ flex: 1 }}
@ -131,7 +146,8 @@ const ComponentInstance: React.FC<Props> = ({
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<Props> = ({
...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<Props> = ({
content={t('server.button')}
onPress={processUpdate}
disabled={!instanceQuery.data?.uri}
loading={instanceQuery.isFetching || appsQuery.isFetching}
loading={instanceQuery.isFetching || appsMutation.isLoading}
/>
</View>
@ -276,8 +295,6 @@ const ComponentInstance: React.FC<Props> = ({
</View>
</View>
</View>
{requestAuth}
</KeyboardAvoidingView>
)
}

View File

@ -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<Props> = ({
>
<TapGestureHandler
onHandlerStateChange={async ({ nativeEvent }) => {
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<Props> = ({
>
<View
style={{
flex: 3,
flexShrink: 3,
flexDirection: 'row',
alignItems: 'center'
alignItems: 'center',
marginRight: StyleConstants.Spacing.M
}}
>
{iconFront && (

View File

@ -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 (
<CustomText
style={styles.text}
fontWeight={fontBold ? 'Bold' : undefined}
>
<CustomText style={[styles.text, style]} fontWeight={fontBold ? 'Bold' : undefined}>
{emojis ? (
content
.split(regexEmoji)
@ -78,11 +64,7 @@ const ParseEmojis = React.memo(
return emojiShortcode === `:${emoji.shortcode}:`
})
if (emojiIndex === -1) {
return (
<CustomText key={emojiShortcode + i}>
{emojiShortcode}
</CustomText>
)
return <CustomText key={emojiShortcode + i}>{emojiShortcode}</CustomText>
} else {
const uri = reduceMotionEnabled
? emojis[emojiIndex].static_url

View File

@ -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<StackNavigationProp<TabLocalStackParamList>>()
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(
</View>
)
},
[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

View File

@ -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']

View File

@ -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<Mastodon.Relationship[]>(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 } }) => {

View File

@ -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<Props> = ({ data, width, height, margin = 0 }) => {
const fillPoints = linePoints.concat(closePolyPoints)
return (
<Svg height={Platform.OS !== 'android' ? 'auto' : 24} width={width}>
<Svg height={height} width={width} style={{ marginRight: margin }}>
<G>
<Path d={'M' + fillPoints.join(' ')} fill={colors.blue} fillOpacity={0.1} />
<Path

View File

@ -1,68 +1,61 @@
import ComponentSeparator from '@components/Separator'
import { useScrollToTop } from '@react-navigation/native'
import { UseInfiniteQueryOptions } from '@tanstack/react-query'
import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
import { getInstanceActive } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { RefObject, useCallback, useRef } from 'react'
import { FlatList, FlatListProps, Platform, RefreshControl } from 'react-native'
import Animated, {
useAnimatedScrollHandler,
useSharedValue
} from 'react-native-reanimated'
import Animated, { useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated'
import { useSelector } from 'react-redux'
import TimelineEmpty from './Timeline/Empty'
import TimelineFooter from './Timeline/Footer'
import TimelineRefresh, {
SEPARATION_Y_1,
SEPARATION_Y_2
} from './Timeline/Refresh'
import TimelineRefresh, { SEPARATION_Y_1, SEPARATION_Y_2 } from './Timeline/Refresh'
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList)
export interface Props {
flRef?: RefObject<FlatList<any>>
queryKey: QueryKeyTimeline
queryOptions?: Omit<
UseInfiniteQueryOptions<any>,
'notifyOnChangeProps' | 'getNextPageParam' | 'getPreviousPageParam' | 'select' | 'onSuccess'
>
disableRefresh?: boolean
disableInfinity?: boolean
lookback?: Extract<App.Pages, 'Following' | 'Local' | 'LocalPublic'>
customProps: Partial<FlatListProps<any>> &
Pick<FlatListProps<any>, 'renderItem'>
customProps: Partial<FlatListProps<any>> & Pick<FlatListProps<any>, 'renderItem'>
}
const Timeline: React.FC<Props> = ({
flRef: customFLRef,
queryKey,
queryOptions,
disableRefresh = false,
disableInfinity = false,
customProps
}) => {
const { colors } = useTheme()
const {
data,
refetch,
isFetching,
isLoading,
fetchNextPage,
isFetchingNextPage
} = useTimelineQuery({
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 && {
max_id: 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<Props> = ({
onEndReached={onEndReached}
onEndReachedThreshold={0.75}
ListFooterComponent={
<TimelineFooter
queryKey={queryKey}
disableInfinity={disableInfinity}
/>
<TimelineFooter queryKey={queryKey} disableInfinity={disableInfinity} />
}
ListEmptyComponent={<TimelineEmpty queryKey={queryKey} />}
ItemSeparatorComponent={({ leadingItem }) =>
@ -145,9 +135,7 @@ const Timeline: React.FC<Props> = ({
<ComponentSeparator extraMarginLeft={0} />
) : (
<ComponentSeparator
extraMarginLeft={
StyleConstants.Avatar.M + StyleConstants.Spacing.S
}
extraMarginLeft={StyleConstants.Avatar.M + StyleConstants.Spacing.S}
/>
)
}

View File

@ -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'

View File

@ -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<Props> = ({
rootQueryKey,
highlighted = false,
disableDetails = false,
disableOnPress = false
disableOnPress = false,
isConversation = false
}) => {
const { colors } = useTheme()
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
@ -69,9 +71,12 @@ const TimelineDefault: React.FC<Props> = ({
}
const mainStyle: StyleProp<ViewStyle> = {
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<Props> = ({
<TimelineActioned action='pinned' />
) : null}
<View style={{ flex: 1, width: '100%', flexDirection: 'row' }}>
<View
style={{
flex: 1,
flexDirection: 'row',
...(disableDetails && { alignItems: 'flex-start', overflow: 'hidden' })
}}
>
<TimelineAvatar />
<TimelineHeaderDefault />
</View>
@ -89,7 +100,11 @@ const TimelineDefault: React.FC<Props> = ({
<View
style={{
paddingTop: highlighted ? StyleConstants.Spacing.S : 0,
paddingLeft: highlighted ? 0 : StyleConstants.Avatar.M + StyleConstants.Spacing.S
paddingLeft: highlighted
? 0
: (disableDetails ? StyleConstants.Avatar.XS : StyleConstants.Avatar.M) +
StyleConstants.Spacing.S,
...(disableDetails && { marginTop: -StyleConstants.Spacing.S })
}}
>
<TimelineContent setSpoilerExpanded={setSpoilerExpanded} />
@ -125,8 +140,10 @@ const TimelineDefault: React.FC<Props> = ({
spoilerHidden,
copiableContent,
highlighted,
inThread: queryKey?.[1].page === 'Toot',
disableDetails,
disableOnPress
disableOnPress,
isConversation
}}
>
{disableOnPress ? (

View File

@ -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(
<Trans
i18nKey='componentTimeline:end.message'
components={[
<Icon
name='Coffee'
size={StyleConstants.Font.Size.S}
color={colors.secondary}
/>
<Icon name='Coffee' size={StyleConstants.Font.Size.S} color={colors.secondary} />
]}
/>
</CustomText>

View File

@ -28,18 +28,18 @@ import TimelineHeaderAndroid from './Shared/HeaderAndroid'
export interface Props {
notification: Mastodon.Notification
queryKey: QueryKeyTimeline
highlighted?: boolean
}
const TimelineNotifications: React.FC<Props> = ({
notification,
queryKey,
highlighted = false
}) => {
const TimelineNotifications: React.FC<Props> = ({ 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<Props> = ({
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<Props> = ({
</View>
{notification.status ? (
<View
style={{
paddingTop: highlighted ? StyleConstants.Spacing.S : 0,
paddingLeft: highlighted ? 0 : StyleConstants.Avatar.M + StyleConstants.Spacing.S
}}
>
<TimelineContent setSpoilerExpanded={setSpoilerExpanded} />
<View style={{ paddingLeft: StyleConstants.Avatar.M + StyleConstants.Spacing.S }}>
<TimelineContent
notificationOwnToot={['favourite', 'reblog'].includes(notification.type)}
setSpoilerExpanded={setSpoilerExpanded}
/>
<TimelinePoll />
<TimelineAttachment />
<TimelineCard />
@ -138,8 +137,7 @@ const TimelineNotifications: React.FC<Props> = ({
status,
ownAccount,
spoilerHidden,
copiableContent,
highlighted
copiableContent
}}
>
<ContextMenu.Root>

View File

@ -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<FlatList<any>>
@ -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<Props> = ({
flRef,
@ -57,19 +47,15 @@ const TimelineRefresh: React.FC<Props> = ({
const fetchingLatestIndex = useRef(0)
const refetchActive = useRef(false)
const {
refetch,
isFetching,
isLoading,
fetchPreviousPage,
hasPreviousPage,
isFetchingNextPage
} = useTimelineQuery({
const { refetch, isFetching, isLoading, fetchPreviousPage, hasPreviousPage, isFetchingNextPage } =
useTimelineQuery({
...queryKey[1],
options: {
getPreviousPageParam: firstPage =>
firstPage?.links?.prev && {
min_id: 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'
},
@ -105,9 +91,7 @@ const TimelineRefresh: React.FC<Props> = ({
const queryClient = useQueryClient()
const clearFirstPage = () => {
queryClient.setQueryData<InfiniteData<TimelineData> | undefined>(
queryKey,
data => {
queryClient.setQueryData<InfiniteData<TimelineData> | undefined>(queryKey, data => {
if (data?.pages[0] && data.pages[0].body.length === 0) {
return {
pages: data.pages.slice(1),
@ -116,14 +100,11 @@ const TimelineRefresh: React.FC<Props> = ({
} else {
return data
}
}
)
})
}
const prepareRefetch = () => {
refetchActive.current = true
queryClient.setQueryData<InfiniteData<TimelineData> | undefined>(
queryKey,
data => {
queryClient.setQueryData<InfiniteData<TimelineData> | undefined>(queryKey, data => {
if (data) {
data.pageParams = [undefined]
const newFirstPage: TimelineData = { body: [] }
@ -136,8 +117,7 @@ const TimelineRefresh: React.FC<Props> = ({
}
return data
}
)
})
}
const callRefetch = async () => {
await refetch()
@ -161,10 +141,7 @@ const TimelineRefresh: React.FC<Props> = ({
]
}))
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<Props> = ({
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<Props> = ({
<View style={styles.base}>
{isFetching ? (
<View style={styles.container2}>
<Circle
size={StyleConstants.Font.Size.L}
color={colors.secondary}
/>
<Circle size={StyleConstants.Font.Size.L} color={colors.secondary} />
</View>
) : (
<>

View File

@ -28,7 +28,12 @@ const TimelineActioned: React.FC<Props> = ({ action, isNotification, ...rest })
const iconColor = colors.primaryDefault
const content = (content: string) => (
<ParseEmojis content={content} emojis={account.emojis} size='S' />
<ParseEmojis
content={content}
emojis={account.emojis}
size='S'
style={{ color: action === 'admin.report' ? colors.red : colors.primaryDefault }}
/>
)
const onPress = () => navigation.push('Tab-Shared-Account', { account })
@ -145,6 +150,30 @@ const TimelineActioned: React.FC<Props> = ({ action, isNotification, ...rest })
{content(t('shared.actioned.update'))}
</>
)
case 'admin.sign_up':
return (
<>
<Icon
name='Users'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
{content(t('shared.actioned.admin.sign_up', { name: `@${account.acct}` }))}
</>
)
case 'admin.report':
return (
<>
<Icon
name='AlertOctagon'
size={StyleConstants.Font.Size.S}
color={colors.red}
style={styles.icon}
/>
{content(t('shared.actioned.admin.report', { name: `@${account.acct}` }))}
</>
)
default:
return <></>
}

View File

@ -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'

View File

@ -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 => {

View File

@ -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<Props> = ({ total, index, sensitiveShown, audio
styles.base,
{
backgroundColor: colors.disabled,
aspectRatio: attachmentAspectRatio({ total, index })
aspectRatio: aspectRatio({ total, index, ...audio.meta?.original })
}
]}
>

View File

@ -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 (
<View
style={{
@ -28,21 +31,16 @@ const AttachmentImage = ({
padding: StyleConstants.Spacing.XS / 2
}}
>
<View style={{ flex: 1, backgroundColor: colors.shimmerDefault }}>
<GracefullyImage
accessibilityLabel={image.description}
hidden={sensitiveShown}
uri={{ original: image.preview_url, remote: image.remote_url }}
blurhash={image.blurhash}
onPress={() => navigateToImagesViewer(image.id)}
style={{
aspectRatio:
total > 1 || !image.meta?.original?.width || !image.meta?.original?.height
? attachmentAspectRatio({ total, index })
: image.meta.original.height / image.meta.original.width > 1
? 1
: image.meta.original.width / image.meta.original.height
}}
style={{ aspectRatio: aspectRatio({ total, index, ...image.meta?.original }) }}
/>
</View>
<AttachmentAltText sensitiveShown={sensitiveShown} text={image.description} />
</View>
)

View File

@ -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<Props> = ({ 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 ? (

View File

@ -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<Props> = ({
video,
gifv = false
}) => {
const { reduceMotionEnabled } = useAccessibility()
const videoPlayer = useRef<Video>(null)
const [videoLoading, setVideoLoading] = useState(false)
const [videoLoaded, setVideoLoaded] = useState(false)
@ -46,7 +49,7 @@ const AttachmentVideo: React.FC<Props> = ({
flex: 1,
flexBasis: '50%',
padding: StyleConstants.Spacing.XS / 2,
aspectRatio: attachmentAspectRatio({ total, index })
aspectRatio: aspectRatio({ total, index, ...video.meta?.original })
}}
>
<Video
@ -57,7 +60,7 @@ const AttachmentVideo: React.FC<Props> = ({
resizeMode={videoResizeMode}
{...(gifv
? {
shouldPlay: true,
shouldPlay: reduceMotionEnabled ? false : true,
isMuted: true,
isLooping: true,
source: { uri: video.url }
@ -70,10 +73,10 @@ const AttachmentVideo: React.FC<Props> = ({
onFullscreenUpdate={event => {
if (event.fullscreenUpdate === VideoFullscreenUpdate.PLAYER_DID_DISMISS) {
Platform.OS === 'android' && setVideoResizeMode(ResizeMode.COVER)
if (!gifv) {
videoPlayer.current?.pauseAsync()
} else {
if (gifv && !reduceMotionEnabled) {
videoPlayer.current?.playAsync()
} else {
videoPlayer.current?.pauseAsync()
}
}
}}
@ -103,7 +106,7 @@ const AttachmentVideo: React.FC<Props> = ({
video.blurhash ? (
<Blurhash blurhash={video.blurhash} style={{ width: '100%', height: '100%' }} />
) : null
) : !gifv ? (
) : !gifv || (gifv && reduceMotionEnabled) ? (
<Button
round
overlay

View File

@ -1,25 +0,0 @@
const attachmentAspectRatio = ({
total,
index
}: {
total: number
index?: number
}) => {
switch (total) {
case 1:
case 4:
return 16 / 9
case 2:
return 8 / 9
case 3:
if (index === 2) {
return 32 / 9
} else {
return 16 / 9
}
default:
return 16 / 9
}
}
export default attachmentAspectRatio

View File

@ -0,0 +1,38 @@
export const aspectRatio = ({
total,
index,
width,
height
}: {
total: number
index?: number
width?: number
height?: number
}): number => {
const cropTooTall = (height || 1) / (width || 1) > 3 / 2 ? 2 / 3 : (width || 1) / (height || 1)
const isEven = total % 2 == 0
if (total > 5) {
switch (isEven) {
case true:
return total / 2 / 2
case false:
if ((index || -2) + 1 == total) {
return Math.ceil(total / 2)
} else {
return Math.ceil(total / 2) / 2
}
}
} else {
switch (isEven) {
case true:
return cropTooTall
case false:
if ((index || -2) + 1 == total) {
return cropTooTall * 2
} else {
return cropTooTall
}
}
}
}

View File

@ -12,7 +12,8 @@ export interface Props {
}
const TimelineAvatar: React.FC<Props> = ({ account }) => {
const { status, highlighted, disableOnPress } = useContext(StatusContext)
const { status, highlighted, disableDetails, disableOnPress, isConversation } =
useContext(StatusContext)
const actualAccount = account || status?.account
if (!actualAccount) return null
@ -33,14 +34,22 @@ const TimelineAvatar: React.FC<Props> = ({ account }) => {
!disableOnPress && navigation.push('Tab-Shared-Account', { account: actualAccount })
}
uri={{ original: actualAccount.avatar, static: actualAccount.avatar_static }}
dimension={{
dimension={
disableDetails || isConversation
? {
width: StyleConstants.Avatar.XS,
height: StyleConstants.Avatar.XS
}
: {
width: StyleConstants.Avatar.M,
height: StyleConstants.Avatar.M
}}
}
}
style={{
borderRadius: StyleConstants.Avatar.M,
overflow: 'hidden',
marginRight: StyleConstants.Spacing.S
marginRight: StyleConstants.Spacing.S,
marginLeft: isConversation ? StyleConstants.Avatar.M - StyleConstants.Avatar.XS : undefined
}}
/>
)

View File

@ -10,7 +10,7 @@ import { useStatusQuery } from '@utils/queryHooks/status'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext, useEffect, useState } from 'react'
import { Pressable, StyleSheet, Text, View } from 'react-native'
import { Pressable, StyleSheet, View } from 'react-native'
import { Circle } from 'react-native-animated-spinkit'
import TimelineDefault from '../Default'
import StatusContext from './Context'
@ -187,7 +187,10 @@ const TimelineCard: React.FC = () => {
style={{
flex: 1,
flexDirection: 'row',
minHeight: isAccount && foundAccount ? undefined : StyleConstants.Font.LineHeight.M * 5,
minHeight:
(isStatus && foundStatus) || (isAccount && foundAccount)
? undefined
: StyleConstants.Font.LineHeight.M * 5,
marginTop: StyleConstants.Spacing.M,
borderWidth: StyleSheet.hairlineWidth,
borderRadius: StyleConstants.Spacing.S,

View File

@ -2,15 +2,18 @@ import { ParseHTML } from '@components/Parse'
import { getInstanceAccount } from '@utils/slices/instancesSlice'
import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next'
import { Platform } from 'react-native'
import { useSelector } from 'react-redux'
import { isRtlLang } from 'rtl-detect'
import StatusContext from './Context'
export interface Props {
notificationOwnToot?: boolean
setSpoilerExpanded?: React.Dispatch<React.SetStateAction<boolean>>
}
const TimelineContent: React.FC<Props> = ({ setSpoilerExpanded }) => {
const { status, highlighted, disableDetails } = useContext(StatusContext)
const TimelineContent: React.FC<Props> = ({ notificationOwnToot = false, setSpoilerExpanded }) => {
const { status, highlighted, inThread, disableDetails } = useContext(StatusContext)
if (!status || typeof status.content !== 'string' || !status.content.length) return null
const { t } = useTranslation('componentTimeline')
@ -30,6 +33,11 @@ const TimelineContent: React.FC<Props> = ({ setSpoilerExpanded }) => {
numberOfLines={999}
highlighted={highlighted}
disableDetails={disableDetails}
textStyles={
Platform.OS === 'ios' && status.language && isRtlLang(status.language)
? { writingDirection: 'rtl' }
: undefined
}
/>
<ParseHTML
content={status.content}
@ -38,11 +46,22 @@ const TimelineContent: React.FC<Props> = ({ setSpoilerExpanded }) => {
emojis={status.emojis}
mentions={status.mentions}
tags={status.tags}
numberOfLines={instanceAccount.preferences['reading:expand:spoilers'] ? 999 : 1}
numberOfLines={
instanceAccount.preferences['reading:expand:spoilers'] || inThread
? notificationOwnToot
? 2
: 999
: 1
}
expandHint={t('shared.content.expandHint')}
setSpoilerExpanded={setSpoilerExpanded}
highlighted={highlighted}
disableDetails={disableDetails}
textStyles={
Platform.OS === 'ios' && status.language && isRtlLang(status.language)
? { writingDirection: 'rtl' }
: undefined
}
/>
</>
) : (
@ -53,8 +72,13 @@ const TimelineContent: React.FC<Props> = ({ setSpoilerExpanded }) => {
emojis={status.emojis}
mentions={status.mentions}
tags={status.tags}
numberOfLines={highlighted ? 999 : undefined}
numberOfLines={highlighted || inThread ? 999 : notificationOwnToot ? 2 : undefined}
disableDetails={disableDetails}
textStyles={
Platform.OS === 'ios' && status.language && isRtlLang(status.language)
? { writingDirection: 'rtl' }
: undefined
}
/>
)}
</>

View File

@ -16,8 +16,10 @@ type ContextType = {
}>
highlighted?: boolean
inThread?: boolean
disableDetails?: boolean
disableOnPress?: boolean
isConversation?: boolean
}
const StatusContext = createContext<ContextType>({} as ContextType)

View File

@ -37,7 +37,7 @@ const TimelineFeedback = () => {
onPress={() =>
navigation.push('Tab-Shared-Users', {
reference: 'statuses',
id: status.id,
status,
type: 'reblogged_by',
count: status.reblogs_count
})
@ -59,7 +59,7 @@ const TimelineFeedback = () => {
onPress={() =>
navigation.push('Tab-Shared-Users', {
reference: 'statuses',
id: status.id,
status,
type: 'favourited_by',
count: status.favourites_count
})

View File

@ -45,6 +45,7 @@ export const shouldFilter = ({
status: Mastodon.Status
queryKey: QueryKeyTimeline
}): string | null => {
const page = queryKey[1]
const instance = getInstance(store.getState())
const ownAccount = getInstanceAccount(store.getState())?.id === status.account?.id
@ -72,12 +73,12 @@ export const shouldFilter = ({
const escapedPhrase = filter.phrase.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
switch (filter.whole_word) {
case true:
if (new RegExp(`\\B${escapedPhrase}\\b`).test(rawContent)) {
if (new RegExp(`\\b${escapedPhrase}\\b`, 'i').test(rawContent)) {
shouldFilter = filter.phrase
}
break
case false:
if (new RegExp(escapedPhrase).test(rawContent)) {
if (new RegExp(escapedPhrase, 'i').test(rawContent)) {
shouldFilter = filter.phrase
}
break
@ -93,11 +94,11 @@ export const shouldFilter = ({
}
}
switch (queryKey[1].page) {
switch (page.page) {
case 'Following':
case 'Local':
case 'List':
case 'Account_Default':
case 'Account':
if (filter.context.includes('home')) {
checkFilter(filter)
}

View File

@ -1,5 +1,4 @@
import menuAccount from '@components/contextMenu/account'
import menuInstance from '@components/contextMenu/instance'
import menuShare from '@components/contextMenu/share'
import menuStatus from '@components/contextMenu/status'
import Icon from '@components/Icon'
@ -31,7 +30,6 @@ const TimelineHeaderAndroid: React.FC = () => {
queryKey
})
const mStatus = menuStatus({ status, queryKey, rootQueryKey })
const mInstance = menuInstance({ status, queryKey, rootQueryKey })
return (
<View style={{ position: 'absolute', top: 0, right: 0 }}>
@ -53,7 +51,6 @@ const TimelineHeaderAndroid: React.FC = () => {
{mGroup.map(menu => (
<DropdownMenu.Item key={menu.key} {...menu.item}>
<DropdownMenu.ItemTitle children={menu.title} />
<DropdownMenu.ItemIcon iosIconName={menu.icon} />
</DropdownMenu.Item>
))}
</DropdownMenu.Group>
@ -64,7 +61,6 @@ const TimelineHeaderAndroid: React.FC = () => {
{mGroup.map(menu => (
<DropdownMenu.Item key={menu.key} {...menu.item}>
<DropdownMenu.ItemTitle children={menu.title} />
<DropdownMenu.ItemIcon iosIconName={menu.icon} />
</DropdownMenu.Item>
))}
</DropdownMenu.Group>
@ -75,18 +71,6 @@ const TimelineHeaderAndroid: React.FC = () => {
{mGroup.map(menu => (
<DropdownMenu.Item key={menu.key} {...menu.item}>
<DropdownMenu.ItemTitle children={menu.title} />
<DropdownMenu.ItemIcon iosIconName={menu.icon} />
</DropdownMenu.Item>
))}
</DropdownMenu.Group>
))}
{mInstance.map((mGroup, index) => (
<DropdownMenu.Group key={index}>
{mGroup.map(menu => (
<DropdownMenu.Item key={menu.key} {...menu.item}>
<DropdownMenu.ItemTitle children={menu.title} />
<DropdownMenu.ItemIcon iosIconName={menu.icon} />
</DropdownMenu.Item>
))}
</DropdownMenu.Group>

View File

@ -8,7 +8,7 @@ import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next'
import { Pressable, View } from 'react-native'
import { useQueryClient } from 'react-query'
import { useQueryClient } from '@tanstack/react-query'
import StatusContext from './Context'
import HeaderSharedCreated from './HeaderShared/Created'
import HeaderSharedMuted from './HeaderShared/Muted'

View File

@ -1,5 +1,4 @@
import menuAccount from '@components/contextMenu/account'
import menuInstance from '@components/contextMenu/instance'
import menuShare from '@components/contextMenu/share'
import menuStatus from '@components/contextMenu/status'
import Icon from '@components/Icon'
@ -38,18 +37,23 @@ const TimelineHeaderDefault: React.FC = () => {
queryKey
})
const mStatus = menuStatus({ status, queryKey, rootQueryKey })
const mInstance = menuInstance({ status, queryKey, rootQueryKey })
return (
<View style={{ flex: 1, flexDirection: 'row' }}>
<View style={{ flex: 7 }}>
<View
style={{
flex: 7,
...(disableDetails && { flexDirection: 'row' })
}}
>
<HeaderSharedAccount account={status.account} />
<View
style={{
flexDirection: 'row',
alignItems: 'center',
marginTop: StyleConstants.Spacing.XS,
marginBottom: StyleConstants.Spacing.S
...(disableDetails
? { marginLeft: StyleConstants.Spacing.S }
: { marginTop: StyleConstants.Spacing.XS, marginBottom: StyleConstants.Spacing.S })
}}
>
<HeaderSharedCreated
@ -110,17 +114,6 @@ const TimelineHeaderDefault: React.FC = () => {
))}
</DropdownMenu.Group>
))}
{mInstance.map((mGroup, index) => (
<DropdownMenu.Group key={index}>
{mGroup.map(menu => (
<DropdownMenu.Item key={menu.key} {...menu.item}>
<DropdownMenu.ItemTitle children={menu.title} />
<DropdownMenu.ItemIcon iosIconName={menu.icon} />
</DropdownMenu.Item>
))}
</DropdownMenu.Group>
))}
</DropdownMenu.Content>
</DropdownMenu.Root>
</Pressable>

View File

@ -1,13 +1,19 @@
import Button from '@components/Button'
import menuAccount from '@components/contextMenu/account'
import menuInstance from '@components/contextMenu/instance'
import menuShare from '@components/contextMenu/share'
import menuStatus from '@components/contextMenu/status'
import Icon from '@components/Icon'
import { RelationshipIncoming, RelationshipOutgoing } from '@components/Relationship'
import browserPackage from '@helpers/browserPackage'
import { getInstanceUrl } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import * as WebBrowser from 'expo-web-browser'
import React, { useContext, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Platform, Pressable, View } from 'react-native'
import { useSelector } from 'react-redux'
import * as DropdownMenu from 'zeego/dropdown-menu'
import StatusContext from './Context'
import HeaderSharedAccount from './HeaderShared/Account'
@ -21,6 +27,7 @@ export type Props = {
}
const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
const { t } = useTranslation('componentTimeline')
const { queryKey, status } = useContext(StatusContext)
const { colors } = useTheme()
@ -40,12 +47,32 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
const mStatus = menuStatus({ status, queryKey })
const mInstance = menuInstance({ status, queryKey })
const url = useSelector(getInstanceUrl)
const actions = () => {
switch (notification.type) {
case 'follow':
return <RelationshipOutgoing id={notification.account.id} />
case 'follow_request':
return <RelationshipIncoming id={notification.account.id} />
case 'admin.report':
return (
<Button
type='text'
content={t('shared.actions.openReport')}
onPress={async () =>
WebBrowser.openAuthSessionAsync(
`https://${url}/admin/reports/${notification.report.id}`,
'tooot://tooot',
{
browserPackage: await browserPackage(),
dismissButtonStyle: 'done',
readerMode: false
}
)
}
/>
)
default:
if (status) {
return (
@ -118,12 +145,25 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
<View style={{ flex: 1, flexDirection: 'row' }}>
<View
style={{
flex: notification.type === 'follow' || notification.type === 'follow_request' ? 1 : 4
flex:
notification.type === 'follow' ||
notification.type === 'follow_request' ||
notification.type === 'admin.report'
? 1
: 4
}}
>
<HeaderSharedAccount
account={notification.status ? notification.status.account : notification.account}
{...((notification.type === 'follow' || notification.type === 'follow_request') && {
account={
notification.type === 'admin.report'
? notification.report.target_account
: notification.status
? notification.status.account
: notification.account
}
{...((notification.type === 'follow' ||
notification.type === 'follow_request' ||
notification.type === 'admin.report') && {
withoutName: true
})}
/>
@ -151,7 +191,9 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
<View
style={[
{ marginLeft: StyleConstants.Spacing.M },
notification.type === 'follow' || notification.type === 'follow_request'
notification.type === 'follow' ||
notification.type === 'follow_request' ||
notification.type === 'admin.report'
? { flexShrink: 1 }
: { flex: 1 }
]}

View File

@ -16,7 +16,7 @@ import { maxBy } from 'lodash'
import React, { useCallback, useContext, useMemo, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { Pressable, View } from 'react-native'
import { useQueryClient } from 'react-query'
import { useQueryClient } from '@tanstack/react-query'
import StatusContext from './Context'
const TimelinePoll: React.FC = () => {
@ -33,7 +33,7 @@ const TimelinePoll: React.FC = () => {
const poll = status.poll
const { colors, theme } = useTheme()
const { t, i18n } = useTranslation('componentTimeline')
const { t } = useTranslation('componentTimeline')
const [allOptions, setAllOptions] = useState(new Array(status.poll.options.length).fill(false))
@ -127,7 +127,7 @@ const TimelinePoll: React.FC = () => {
)
}
}
}, [theme, i18n.language, poll.expired, poll.voted, allOptions, mutation.isLoading])
}, [theme, poll.expired, poll.voted, allOptions, mutation.isLoading])
const isSelected = useCallback(
(index: number): string =>

View File

@ -1,5 +1,6 @@
import { ParseHTML } from '@components/Parse'
import CustomText from '@components/Text'
import detectLanguage from '@helpers/detectLanguage'
import getLanguage from '@helpers/getLanguage'
import { useTranslateQuery } from '@utils/queryHooks/translate'
import { StyleConstants } from '@utils/styles/constants'
@ -7,18 +8,18 @@ import { useTheme } from '@utils/styles/ThemeManager'
import * as Localization from 'expo-localization'
import React, { useContext, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Pressable } from 'react-native'
import { Platform, Pressable } from 'react-native'
import { Circle } from 'react-native-animated-spinkit'
import detectLanguage from 'react-native-language-detection'
import StatusContext from './Context'
const TimelineTranslate = () => {
const { status, highlighted } = useContext(StatusContext)
const { status, highlighted, copiableContent } = useContext(StatusContext)
if (!status || !highlighted) return null
const { t } = useTranslation('componentTimeline')
const { colors } = useTheme()
const backupTextProcessing = (): string[] => {
const text = status.spoiler_text ? [status.spoiler_text, status.content] : [status.content]
for (const i in text) {
@ -31,14 +32,20 @@ const TimelineTranslate = () => {
.replace(/#.*? /gi, ' ')
.replace(/http(s):\/\/.*? /gi, ' ')
}
return text
}
const text = copiableContent?.current.content
? [copiableContent?.current.content]
: backupTextProcessing()
const [detectedLanguage, setDetectedLanguage] = useState<string>('')
const [detectedLanguage, setDetectedLanguage] = useState<{
language: string
confidence: number
}>({ language: status.language || '', confidence: 0 })
useEffect(() => {
const detect = async () => {
const result = await detectLanguage(text.join(`\n\n`)).catch(() => {
// No need to log language detection failure
})
result?.detected && setDetectedLanguage(result.detected.slice(0, 2))
const result = await detectLanguage(text.join('\n\n'))
result && setDetectedLanguage(result)
}
detect()
}, [])
@ -49,21 +56,37 @@ const TimelineTranslate = () => {
: settingsLanguage || Localization.locale || 'en'
const [enabled, setEnabled] = useState(false)
const { refetch, data, isLoading, isSuccess, isError } = useTranslateQuery({
source: detectedLanguage,
const { refetch, data, isFetching, isSuccess, isError } = useTranslateQuery({
source: detectedLanguage.language,
target: targetLanguage,
text,
options: { enabled }
})
const devView = () => {
return __DEV__ ? (
<CustomText fontStyle='S' style={{ color: colors.secondary }}>{` Source: ${
detectedLanguage?.language
}; Confidence: ${
detectedLanguage?.confidence.toString().slice(0, 5) || 'null'
}; Target: ${targetLanguage}`}</CustomText>
) : null
}
if (!detectedLanguage) {
return null
return devView()
}
if (Localization.locale.slice(0, 2).includes(detectedLanguage)) {
return null
if (
Platform.OS === 'ios' &&
Localization.locale.slice(0, 2).includes(detectedLanguage.language.slice(0, 2))
) {
return devView()
}
if (settingsLanguage?.slice(0, 2).includes(detectedLanguage)) {
return null
if (
Platform.OS === 'android' &&
settingsLanguage?.slice(0, 2).includes(detectedLanguage.language.slice(0, 2))
) {
return devView()
}
return (
@ -88,7 +111,7 @@ const TimelineTranslate = () => {
<CustomText
fontStyle='M'
style={{
color: isLoading || isSuccess ? colors.secondary : isError ? colors.red : colors.blue
color: isFetching || isSuccess ? colors.secondary : isError ? colors.red : colors.blue
}}
>
{isError
@ -102,10 +125,7 @@ const TimelineTranslate = () => {
})
: t('shared.translate.default')}
</CustomText>
<CustomText>
{__DEV__ ? ` Source: ${detectedLanguage}; Target: ${targetLanguage}` : undefined}
</CustomText>
{isLoading ? (
{isFetching ? (
<Circle
size={StyleConstants.Font.Size.M}
color={colors.disabled}
@ -113,6 +133,7 @@ const TimelineTranslate = () => {
/>
) : null}
</Pressable>
{devView()}
{data && data.error === undefined
? data.text.map((d, i) => <ParseHTML key={i} content={d} size={'M'} numberOfLines={999} />)
: null}

View File

@ -17,7 +17,7 @@ import { getInstanceAccount } from '@utils/slices/instancesSlice'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Platform } from 'react-native'
import { useQueryClient } from 'react-query'
import { useQueryClient } from '@tanstack/react-query'
import { useSelector } from 'react-redux'
const menuAccount = ({
@ -50,7 +50,7 @@ const menuAccount = ({
setEnabled(true)
}
}, [openChange, enabled])
const { data, isFetching } = useRelationshipQuery({ id: account.id, options: { enabled } })
const { data, isFetched } = useRelationshipQuery({ id: account.id, options: { enabled } })
const queryClient = useQueryClient()
const timelineMutation = useTimelineMutation({
@ -99,8 +99,8 @@ const menuAccount = ({
haptics('Success')
queryClient.setQueryData<Mastodon.Relationship[]>(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 } }) => {
@ -131,7 +131,7 @@ const menuAccount = ({
type: 'outgoing',
payload: { action: 'follow', state: !data?.requested ? data.following : true }
}),
disabled: !data || isFetching,
disabled: !data || !isFetched,
destructive: false,
hidden: false
},
@ -152,9 +152,9 @@ const menuAccount = ({
key: 'account-list',
item: {
onSelect: () => navigation.navigate('Tab-Shared-Account-In-Lists', { account }),
disabled: Platform.OS !== 'android' ? !data || isFetching : false,
disabled: Platform.OS !== 'android' ? !data || !isFetched : false,
destructive: false,
hidden: isFetching ? false : !data?.following
hidden: !isFetched || !data?.following
},
title: t('account.inLists'),
icon: 'checklist'
@ -169,7 +169,7 @@ const menuAccount = ({
id: account.id,
payload: { property: 'mute', currentValue: data?.muting }
}),
disabled: Platform.OS !== 'android' ? !data || isFetching : false,
disabled: Platform.OS !== 'android' ? !data || !isFetched : false,
destructive: false,
hidden: false
},
@ -192,7 +192,7 @@ const menuAccount = ({
id: account.id,
payload: { property: 'block', currentValue: data?.blocking }
}),
disabled: Platform.OS !== 'android' ? !data || isFetching : false,
disabled: Platform.OS !== 'android' ? !data || !isFetched : false,
destructive: !data?.blocking,
hidden: false
},

View File

@ -3,7 +3,7 @@ import { QueryKeyTimeline, useTimelineMutation } from '@utils/queryHooks/timelin
import { getInstanceUrl } from '@utils/slices/instancesSlice'
import { useTranslation } from 'react-i18next'
import { Alert } from 'react-native'
import { useQueryClient } from 'react-query'
import { useQueryClient } from '@tanstack/react-query'
import { useSelector } from 'react-redux'
const menuInstance = ({

View File

@ -12,7 +12,7 @@ import { checkInstanceFeature, getInstanceAccount } from '@utils/slices/instance
import { useTheme } from '@utils/styles/ThemeManager'
import { useTranslation } from 'react-i18next'
import { Alert } from 'react-native'
import { useQueryClient } from 'react-query'
import { useQueryClient } from '@tanstack/react-query'
import { useSelector } from 'react-redux'
const menuStatus = ({

View File

@ -22,10 +22,7 @@ const mediaSelector = async ({
indicateMaximum = false,
showActionSheetWithOptions
}: Props): Promise<Asset[]> => {
const _maximum =
maximum ||
getInstanceConfigurationStatusMaxAttachments(store.getState()) ||
4
const _maximum = maximum || getInstanceConfigurationStatusMaxAttachments(store.getState()) || 4
const options = () => {
switch (mediaType) {
@ -33,7 +30,7 @@ const mediaSelector = async ({
return [
i18next.t(
'componentMediaSelector:options.image',
indicateMaximum ? { context: 'max', max: _maximum } : undefined
indicateMaximum ? { context: 'max', max: _maximum } : {}
),
i18next.t('common:buttons.cancel')
]
@ -41,7 +38,7 @@ const mediaSelector = async ({
return [
i18next.t(
'componentMediaSelector:options.video',
indicateMaximum ? { context: 'max', max: 1 } : undefined
indicateMaximum ? { context: 'max', max: 1 } : {}
),
i18next.t('common:buttons.cancel')
]
@ -49,11 +46,11 @@ const mediaSelector = async ({
return [
i18next.t(
'componentMediaSelector:options.image',
indicateMaximum ? { context: 'max', max: _maximum } : undefined
indicateMaximum ? { context: 'max', max: _maximum } : {}
),
i18next.t(
'componentMediaSelector:options.video',
indicateMaximum ? { context: 'max', max: 1 } : undefined
indicateMaximum ? { context: 'max', max: 1 } : {}
),
i18next.t('common:buttons.cancel')
]

View File

@ -0,0 +1,10 @@
import detect from 'react-native-language-detection'
const detectLanguage = async (
text: string
): Promise<{ language: string; confidence: number } | null> => {
const possibleLanguages = await detect(text).catch(() => {})
return possibleLanguages ? possibleLanguages.filter(lang => lang.confidence > 0.5)?.[0] : null
}
export default detectLanguage

View File

@ -1,37 +1,42 @@
[
{
"feature": "notification_type_status",
"version": 3.3
},
{
"feature": "account_return_suspended",
"version": 3.3,
"reference": "https://github.com/mastodon/mastodon/releases/tag/v3.3.0"
"version": 3.3
},
{
"feature": "edit_post",
"version": 3.5,
"reference": "https://github.com/mastodon/mastodon/releases/tag/v3.5.0"
"version": 3.5
},
{
"feature": "deprecate_auth_follow",
"version": 3.5,
"reference": "https://github.com/mastodon/mastodon/releases/tag/v3.5.0"
"version": 3.5
},
{
"feature": "notification_type_update",
"version": 3.5,
"reference": "https://github.com/mastodon/mastodon/releases/tag/v3.5.0"
"version": 3.5
},
{
"feature": "notification_type_admin_signup",
"version": 3.5
},
{
"feature": "notification_types_positive_filter",
"version": 3.5,
"reference": "https://github.com/mastodon/mastodon/releases/tag/v3.5.0"
"version": 3.5
},
{
"feature": "trends_new_path",
"version": 3.5,
"reference": "https://github.com/mastodon/mastodon/releases/tag/v3.5.0"
"version": 3.5
},
{
"feature": "follow_tags",
"version": 4.0,
"reference": "https://github.com/mastodon/mastodon/releases/tag/v4.0.0"
"version": 4.0
},
{
"feature": "notification_type_admin_report",
"version": 4.0
}
]

View File

@ -0,0 +1,9 @@
export const PERMISSION_MANAGE_REPORTS = 0x0000000000000010
export const PERMISSION_MANAGE_USERS = 0x0000000000000400
export const checkPermission = (permission: number, permissions?: string | number): boolean =>
permissions
? !!(
(typeof permissions === 'string' ? parseInt(permissions || '0') : permissions) & permission
)
: false

View File

@ -1,5 +1,5 @@
import { QueryClient } from 'react-query'
import { QueryClient } from '@tanstack/react-query'
const queryClient = new QueryClient()
const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 1000 * 60 * 5 } } })
export default queryClient

Some files were not shown because too many files have changed in this diff Show More