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', id: '1',
uri: 'https://example.com',
created_at: new Date().toISOString(), created_at: new Date().toISOString(),
sensitive: false, sensitive: false,
visibility: 'public', visibility: 'public',
@ -13,7 +14,6 @@ const demoStatuses = [
bookmarked: false, bookmarked: false,
content: content:
'<p>Would you like to try out this simple, beautiful and open-source mobile app for Mastodon? 😊</p>', '<p>Would you like to try out this simple, beautiful and open-source mobile app for Mastodon? 😊</p>',
reblog: null,
application: { application: {
name: 'tooot', name: 'tooot',
website: 'https://tooot.app' website: 'https://tooot.app'
@ -23,19 +23,31 @@ const demoStatuses = [
username: 'tooot📱', username: 'tooot📱',
acct: 'tooot@xmflsct.com', acct: 'tooot@xmflsct.com',
display_name: 'tooot📱', display_name: 'tooot📱',
avatar_static: avatar: 'https://avatars.githubusercontent.com/u/77554750?s=200&v=4',
'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: [], media_attachments: [],
poll: { poll: {
id: '1', id: '1',
expires_at: new Date().setDate(new Date().getDate() + 5), expires_at: new Date().setDate(new Date().getDate() + 5).toString(),
expired: false, expired: false,
multiple: false, multiple: false,
votes_count: 10, votes_count: 10,
voters_count: null, voters_count: 2,
voted: false, voted: false,
own_votes: null, own_votes: undefined,
options: [ options: [
{ {
title: 'I would love to!', title: 'I would love to!',
@ -48,11 +60,15 @@ const demoStatuses = [
], ],
emojis: [] emojis: []
}, },
mentions: [] mentions: [],
tags: [],
emojis: [],
pinned: false
}, },
{ {
id: '2', 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, sensitive: false,
spoiler_text: '', spoiler_text: '',
visibility: 'public', visibility: 'public',
@ -65,18 +81,26 @@ const demoStatuses = [
bookmarked: false, bookmarked: false,
content: 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>', '<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' },
application: {
name: 'Web',
website: null
},
account: { account: {
id: '1000', id: '1000',
username: 'Mastodon', username: 'Mastodon',
acct: 'mastodon', acct: 'mastodon',
display_name: 'Mastodon', display_name: 'Mastodon',
avatar_static: avatar: 'https://mastodon.social/apple-touch-icon.png',
'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: [], media_attachments: [],
card: { card: {
@ -85,18 +109,31 @@ const demoStatuses = [
description: 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!', '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', type: 'link',
image: image: 'https://mastodon.social/apple-touch-icon.png',
'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', 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: '', spoiler_text: '',
visibility: 'public', visibility: 'public',
replies_count: 2, replies_count: 2,
reblogs_count: null, reblogs_count: 1,
favourites_count: 3, favourites_count: 3,
favourited: false, favourited: false,
reblogged: false, reblogged: false,
@ -104,24 +141,38 @@ const demoStatuses = [
bookmarked: true, bookmarked: true,
content: 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>', '<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' },
application: {
name: 'Web',
website: null
},
account: { account: {
id: '1001', id: '1001',
username: 'Fediverse', username: 'Fediverse',
acct: 'fediverse', acct: 'fediverse',
display_name: '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: 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: [], media_attachments: [],
mentions: [] mentions: [],
tags: [],
emojis: [],
pinned: false
}, },
{ {
id: '4', id: '4',
uri: 'https://example.com',
created_at: '2021-01-24T08:50:00.901Z', created_at: '2021-01-24T08:50:00.901Z',
sensitive: false, sensitive: false,
visibility: 'public', visibility: 'public',
@ -134,7 +185,6 @@ const demoStatuses = [
bookmarked: false, bookmarked: false,
content: content:
'<p>tooot is an open source, simple mobile client for Mastodon. Focusing on your connections while being able to explore the Fediverse.</p>', '<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: { application: {
name: 'tooot', name: 'tooot',
website: 'https://tooot.app' website: 'https://tooot.app'
@ -144,14 +194,30 @@ const demoStatuses = [
username: 'tooot📱', username: 'tooot📱',
acct: 'tooot@xmflsct.com', acct: 'tooot@xmflsct.com',
display_name: 'tooot📱', display_name: 'tooot📱',
avatar_static: avatar: 'https://avatars.githubusercontent.com/u/77554750?s=200&v=4',
'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: [], media_attachments: [],
mentions: [] mentions: [],
tags: [],
emojis: [],
pinned: false
}, },
{ {
id: '5', id: '5',
uri: 'https://example.com',
created_at: '2021-01-24T07:50:00.901Z', created_at: '2021-01-24T07:50:00.901Z',
sensitive: false, sensitive: false,
visibility: 'public', visibility: 'public',
@ -164,7 +230,6 @@ const demoStatuses = [
bookmarked: false, bookmarked: false,
content: content:
'<p>- tooot supports multiple accounts<br />- tooot supports browsing external instance<br />- tooot aims to support multiple languages</p>', '<p>- tooot supports multiple accounts<br />- tooot supports browsing external instance<br />- tooot aims to support multiple languages</p>',
reblog: null,
application: { application: {
name: 'tooot', name: 'tooot',
website: 'https://tooot.app' website: 'https://tooot.app'
@ -174,12 +239,27 @@ const demoStatuses = [
username: 'tooot📱', username: 'tooot📱',
acct: 'tooot@xmflsct.com', acct: 'tooot@xmflsct.com',
display_name: 'tooot📱', display_name: 'tooot📱',
avatar_static: avatar: 'https://avatars.githubusercontent.com/u/77554750?s=200&v=4',
'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: [], 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
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 lane :ios do
cocoapods(clean_install: true, podfile: "./ios/Podfile") cocoapods(clean_install: true, podfile: "./ios/Podfile")
build_ios build_ios
@ -121,13 +144,15 @@ end
lane :release do lane :release do
if ENVIRONMENT == 'release' if ENVIRONMENT == 'release'
build_android_apk
set_github_release( set_github_release(
repository_name: GITHUB_REPO, repository_name: GITHUB_REPO,
name: GITHUB_RELEASE, name: GITHUB_RELEASE,
tag_name: GITHUB_RELEASE, tag_name: GITHUB_RELEASE,
description: "No changelog provided", description: "No changelog provided",
commitish: git_branch, commitish: git_branch,
is_prerelease: false is_prerelease: false,
upload_assets: ["#{File.expand_path('..', Dir.pwd)}/tooot-#{GITHUB_RELEASE}.apk"]
) )
end end
rocket rocket

View File

@ -1,3 +1,11 @@
Enjoy toooting! This version includes following improvements and fixes: Enjoy toooting! This version includes following improvements and fixes:
- Fix toot attribution of favourites etc. - Added 🇺🇦 Slava Ukraini
- Fix switching language - 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愉快此版本包括以下改进和修复 toooting愉快此版本包括以下改进和修复
- 修复嘟文收藏等显示 - 增加 🇺🇦 Slava Ukraini
- 修复不能切换语言 - 自动识别发嘟语言
- 记住上次公共时间轴选项
- 显示编辑历史的差异
- 关注列表可隐藏转嘟和回复
- 新增管理员推送通知
- 支持嘟文右到左文字
- 测试显示对话层级
- 修复过滤整词功能
- 修复平板不能删除草稿

View File

@ -295,23 +295,21 @@ PODS:
- React-Core - React-Core
- react-native-blurhash (1.1.10): - react-native-blurhash (1.1.10):
- React-Core - React-Core
- react-native-cameraroll (5.1.0): - react-native-cameraroll (5.2.0):
- React-Core - React-Core
- react-native-image-picker (4.10.2): - react-native-image-picker (4.10.2):
- React-Core - React-Core
- react-native-ios-context-menu (1.15.1): - react-native-ios-context-menu (1.15.1):
- React-Core - React-Core
- react-native-language-detection (0.1.0): - react-native-language-detection (0.2.2):
- React - React
- react-native-live-text-image-view (0.4.0):
- React-Core
- react-native-menu (0.7.2): - react-native-menu (0.7.2):
- React - React
- react-native-netinfo (9.3.7): - react-native-netinfo (9.3.7):
- React-Core - React-Core
- react-native-pager-view (6.1.2): - react-native-pager-view (6.1.2):
- React-Core - React-Core
- react-native-paste-input (0.5.1): - react-native-paste-input (0.5.2):
- React-Core - React-Core
- Swime (= 3.0.6) - Swime (= 3.0.6)
- react-native-safe-area-context (4.4.1): - react-native-safe-area-context (4.4.1):
@ -428,9 +426,9 @@ PODS:
- RNScreens (3.18.2): - RNScreens (3.18.2):
- React-Core - React-Core
- React-RCTImage - React-RCTImage
- RNSentry (4.10.1): - RNSentry (4.12.0):
- React-Core - React-Core
- Sentry/HybridSDK (= 7.31.2) - Sentry/HybridSDK (= 7.31.3)
- RNShareMenu (6.0.0): - RNShareMenu (6.0.0):
- React - React
- RNSVG (13.6.0): - RNSVG (13.6.0):
@ -441,7 +439,7 @@ PODS:
- SDWebImageWebPCoder (0.9.1): - SDWebImageWebPCoder (0.9.1):
- libwebp (~> 1.0) - libwebp (~> 1.0)
- SDWebImage/Core (~> 5.13) - SDWebImage/Core (~> 5.13)
- Sentry/HybridSDK (7.31.2) - Sentry/HybridSDK (7.31.3)
- Swime (3.0.6) - Swime (3.0.6)
- Yoga (1.14.0) - Yoga (1.14.0)
@ -495,7 +493,6 @@ DEPENDENCIES:
- react-native-image-picker (from `../node_modules/react-native-image-picker`) - 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-ios-context-menu (from `../node_modules/react-native-ios-context-menu`)
- react-native-language-detection (from `../node_modules/react-native-language-detection`) - 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-menu (from `../node_modules/@react-native-menu/menu`)"
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
- react-native-pager-view (from `../node_modules/react-native-pager-view`) - 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" :path: "../node_modules/react-native-ios-context-menu"
react-native-language-detection: react-native-language-detection:
:path: "../node_modules/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: react-native-menu:
:path: "../node_modules/@react-native-menu/menu" :path: "../node_modules/@react-native-menu/menu"
react-native-netinfo: react-native-netinfo:
@ -736,15 +731,14 @@ SPEC CHECKSUMS:
React-logger: 1623c216abaa88974afce404dc8f479406bbc3a0 React-logger: 1623c216abaa88974afce404dc8f479406bbc3a0
react-native-blur: 50c9feabacbc5f49b61337ebc32192c6be7ec3c3 react-native-blur: 50c9feabacbc5f49b61337ebc32192c6be7ec3c3
react-native-blurhash: add4df9a937b4e021a24bc67a0714f13e0bd40b7 react-native-blurhash: add4df9a937b4e021a24bc67a0714f13e0bd40b7
react-native-cameraroll: a40b082318eb1ecd0336a2f29d9f74b7f2c8cae8 react-native-cameraroll: 0ff04cc4e0ff5f19a94ff4313e5c8bc4503cd86d
react-native-image-picker: bf34f3f516d139ed3e24c5f5a381a91819e349ea react-native-image-picker: bf34f3f516d139ed3e24c5f5a381a91819e349ea
react-native-ios-context-menu: b170594b4448c0cd10c79e13432216bac99de1ac react-native-ios-context-menu: b170594b4448c0cd10c79e13432216bac99de1ac
react-native-language-detection: 0e43195ad014974f1b7a31b64820eff34a243f2d react-native-language-detection: f414937fa715108ab50a6269a3de0bcb95e4ceb0
react-native-live-text-image-view: 483bacfdba464162b8cf176bba555364f18b584c
react-native-menu: 8e172cfcf0e42e92f028e7781eddf84d430cae24 react-native-menu: 8e172cfcf0e42e92f028e7781eddf84d430cae24
react-native-netinfo: 2517ad504b3d303e90d7a431b0fcaef76d207983 react-native-netinfo: 2517ad504b3d303e90d7a431b0fcaef76d207983
react-native-pager-view: 54bed894cecebe28cede54c01038d9d1e122de43 react-native-pager-view: 54bed894cecebe28cede54c01038d9d1e122de43
react-native-paste-input: 183ad7dc224e192719616f4258dde5b548627d08 react-native-paste-input: 88709b4fd586ea8cc56ba5e2fc4cdfe90597730c
react-native-safe-area-context: 99b24a0c5acd0d5dcac2b1a7f18c49ea317be99a react-native-safe-area-context: 99b24a0c5acd0d5dcac2b1a7f18c49ea317be99a
react-native-segmented-control: 65df6cd0619b780b3843d574a72d4c7cec396097 react-native-segmented-control: 65df6cd0619b780b3843d574a72d4c7cec396097
React-perflogger: 8c79399b0500a30ee8152d0f9f11beae7fc36595 React-perflogger: 8c79399b0500a30ee8152d0f9f11beae7fc36595
@ -765,12 +759,12 @@ SPEC CHECKSUMS:
RNGestureHandler: 62232ba8f562f7dea5ba1b3383494eb5bf97a4d3 RNGestureHandler: 62232ba8f562f7dea5ba1b3383494eb5bf97a4d3
RNReanimated: ce445c233a6ff5600223484a88ad5704945d972a RNReanimated: ce445c233a6ff5600223484a88ad5704945d972a
RNScreens: 34cc502acf1b916c582c60003dc3089fa01dc66d RNScreens: 34cc502acf1b916c582c60003dc3089fa01dc66d
RNSentry: 3c27f3c57f16bab9835d9555add298571077e0c1 RNSentry: 4c09f4dd9740cb9b33e94303de5b6d0dbeb0737d
RNShareMenu: cb9dac548c8bf147d06f0bf07296ad51ea9f5fc3 RNShareMenu: cb9dac548c8bf147d06f0bf07296ad51ea9f5fc3
RNSVG: 3a79c0c4992213e4f06c08e62730c5e7b9e4dc17 RNSVG: 3a79c0c4992213e4f06c08e62730c5e7b9e4dc17
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84 SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
SDWebImageWebPCoder: 18503de6621dd2c420d680e33d46bf8e1d5169b0 SDWebImageWebPCoder: 18503de6621dd2c420d680e33d46bf8e1d5169b0
Sentry: b15765d11769852fe78c9add942f7df60ed5dbf5 Sentry: 08884c523575ec0f6690d94ed3ccb0246a1600bf
Swime: d7b2c277503b6cea317774aedc2dce05613f8b0b Swime: d7b2c277503b6cea317774aedc2dce05613f8b0b
Yoga: 99caf8d5ab45e9d637ee6e0174ec16fbbb01bcfc 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>"; }; 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>"; }; 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>"; }; 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; }; 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; }; 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 */ /* End PBXFileReference section */
@ -298,6 +299,7 @@
sv, sv,
nl, nl,
ca, ca,
uk,
); );
mainGroup = 83CBB9F61A601CBA00E9B192; mainGroup = 83CBB9F61A601CBA00E9B192;
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
@ -530,6 +532,7 @@
E63E7FF0292A828100C76FD4 /* sv */, E63E7FF0292A828100C76FD4 /* sv */,
E6217B7E293C1EBF00B1755E /* nl */, E6217B7E293C1EBF00B1755E /* nl */,
E6A4895D293C1F740047951A /* ca */, E6A4895D293C1F740047951A /* ca */,
E6D64C7A294A90840098F3AC /* uk */,
); );
name = InfoPlist.strings; name = InfoPlist.strings;
sourceTree = "<group>"; sourceTree = "<group>";
@ -574,6 +577,7 @@
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
SWIFT_OBJC_BRIDGING_HEADER = "tooot-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "tooot-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_PRECOMPILE_BRIDGING_HEADER = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,6"; TARGETED_DEVICE_FAMILY = "1,2,6";
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
@ -611,6 +615,7 @@
SUPPORTS_MACCATALYST = YES; SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
SWIFT_OBJC_BRIDGING_HEADER = "tooot-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "tooot-Bridging-Header.h";
SWIFT_PRECOMPILE_BRIDGING_HEADER = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,6"; TARGETED_DEVICE_FAMILY = "1,2,6";
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
@ -781,6 +786,7 @@
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "ShareExtension/ShareExtension-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "ShareExtension/ShareExtension-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_PRECOMPILE_BRIDGING_HEADER = NO;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,6"; TARGETED_DEVICE_FAMILY = "1,2,6";
}; };
@ -828,6 +834,7 @@
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "ShareExtension/ShareExtension-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "ShareExtension/ShareExtension-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_PRECOMPILE_BRIDGING_HEADER = NO;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,6"; TARGETED_DEVICE_FAMILY = "1,2,6";
}; };

View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "tooot", "name": "tooot",
"version": "4.6.6", "version": "4.7.0",
"description": "tooot for Mastodon", "description": "tooot for Mastodon",
"author": "xmflsct <me@xmflsct.com>", "author": "xmflsct <me@xmflsct.com>",
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
@ -12,7 +12,7 @@
"start": "react-native start", "start": "react-native start",
"android": "react-native run-android", "android": "react-native run-android",
"iphone": "react-native run-ios --simulator 'iPhone 14 Pro'", "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", "app:build": "bundle exec fastlane",
"clean": "react-native-clean-project", "clean": "react-native-clean-project",
"postinstall": "patch-package" "postinstall": "patch-package"
@ -25,23 +25,25 @@
"@formatjs/intl-numberformat": "^8.3.3", "@formatjs/intl-numberformat": "^8.3.3",
"@formatjs/intl-pluralrules": "^5.1.8", "@formatjs/intl-pluralrules": "^5.1.8",
"@formatjs/intl-relativetimeformat": "^11.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", "@neverdull-agency/expo-unlimited-secure-store": "^1.0.10",
"@react-native-async-storage/async-storage": "~1.17.11", "@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-clipboard/clipboard": "^1.11.1",
"@react-native-community/blur": "^4.3.0", "@react-native-community/blur": "^4.3.0",
"@react-native-community/netinfo": "9.3.7", "@react-native-community/netinfo": "9.3.7",
"@react-native-community/segmented-control": "^2.2.2", "@react-native-community/segmented-control": "^2.2.2",
"@react-native-menu/menu": "^0.7.2", "@react-native-menu/menu": "^0.7.2",
"@react-navigation/bottom-tabs": "^6.4.3", "@react-navigation/bottom-tabs": "^6.5.1",
"@react-navigation/native": "^6.0.16", "@react-navigation/native": "^6.1.1",
"@react-navigation/native-stack": "^6.9.4", "@react-navigation/native-stack": "^6.9.6",
"@react-navigation/stack": "^6.3.7", "@react-navigation/stack": "^6.3.9",
"@reduxjs/toolkit": "^1.9.1", "@reduxjs/toolkit": "^1.9.1",
"@sentry/react-native": "4.10.1", "@sentry/react-native": "4.12.0",
"@sharcoux/slider": "^6.1.1", "@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": "^47.0.8",
"expo-auth-session": "^3.7.3", "expo-auth-session": "^3.7.3",
"expo-av": "^13.0.2", "expo-av": "^13.0.2",
@ -59,13 +61,12 @@
"expo-store-review": "^6.0.0", "expo-store-review": "^6.0.0",
"expo-video-thumbnails": "^7.0.0", "expo-video-thumbnails": "^7.0.0",
"expo-web-browser": "~12.0.0", "expo-web-browser": "~12.0.0",
"i18next": "^22.0.6", "i18next": "^22.4.5",
"li": "^1.3.0",
"linkify-it": "^4.0.1", "linkify-it": "^4.0.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-i18next": "^12.0.0", "react-i18next": "^12.1.1",
"react-intl": "^6.2.5", "react-intl": "^6.2.5",
"react-native": "0.70.6", "react-native": "0.70.6",
"react-native-animated-spinkit": "^1.5.2", "react-native-animated-spinkit": "^1.5.2",
@ -78,8 +79,7 @@
"react-native-htmlview": "^0.16.0", "react-native-htmlview": "^0.16.0",
"react-native-image-picker": "^4.10.2", "react-native-image-picker": "^4.10.2",
"react-native-ios-context-menu": "^1.15.1", "react-native-ios-context-menu": "^1.15.1",
"react-native-language-detection": "^0.1.0", "react-native-language-detection": "^0.2.2",
"react-native-live-text-image-view": "^0.4.0",
"react-native-pager-view": "^6.1.2", "react-native-pager-view": "^6.1.2",
"react-native-reanimated": "^2.13.0", "react-native-reanimated": "^2.13.0",
"react-native-reanimated-zoom": "^0.3.3", "react-native-reanimated-zoom": "^0.3.3",
@ -88,11 +88,11 @@
"react-native-share-menu": "^6.0.0", "react-native-share-menu": "^6.0.0",
"react-native-svg": "^13.6.0", "react-native-svg": "^13.6.0",
"react-native-swipe-list-view": "^3.2.9", "react-native-swipe-list-view": "^3.2.9",
"react-native-tab-view": "^3.3.2", "react-native-tab-view": "^3.3.4",
"react-query": "^3.39.2",
"react-redux": "^8.0.5", "react-redux": "^8.0.5",
"redux-persist": "^6.0.0", "redux-persist": "^6.0.0",
"rn-placeholder": "^3.0.3", "rn-placeholder": "^3.0.3",
"rtl-detect": "^1.0.4",
"valid-url": "^1.0.9", "valid-url": "^1.0.9",
"zeego": "^0.5.0" "zeego": "^0.5.0"
}, },
@ -102,11 +102,12 @@
"@babel/preset-react": "^7.18.6", "@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.18.6", "@babel/preset-typescript": "^7.18.6",
"@expo/config": "^7.0.3", "@expo/config": "^7.0.3",
"@types/diff": "^5.0.2",
"@types/linkify-it": "^3.0.2", "@types/linkify-it": "^3.0.2",
"@types/lodash": "^4.14.191", "@types/lodash": "^4.14.191",
"@types/react": "~18.0.26", "@types/react": "~18.0.26",
"@types/react-dom": "~18.0.9", "@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-base64": "^0.2.0",
"@types/react-native-share-menu": "^5.0.2", "@types/react-native-share-menu": "^5.0.2",
"@types/react-timeago": "^4.1.3", "@types/react-timeago": "^4.1.3",
@ -120,6 +121,6 @@
"patch-package": "^6.5.0", "patch-package": "^6.5.0",
"postinstall-postinstall": "^2.1.0", "postinstall-postinstall": "^2.1.0",
"react-native-clean-project": "^4.0.1", "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' | 'Following'
| 'Local' | 'Local'
| 'LocalPublic' | 'LocalPublic'
| 'Trending'
| 'Notifications' | 'Notifications'
| 'Hashtag' | 'Hashtag'
| 'List' | 'List'
| 'Toot' | 'Toot'
| 'Account_Default' | 'Account'
| 'Account_All'
| 'Account_Attachments'
| 'Conversations' | 'Conversations'
| 'Bookmarks' | 'Bookmarks'
| 'Favourites' | 'Favourites'

View File

@ -30,6 +30,7 @@ declare namespace Mastodon {
bot: boolean bot: boolean
source?: Source source?: Source
suspended?: boolean suspended?: boolean
role?: Role
} }
type Announcement = { type Announcement = {
@ -332,24 +333,34 @@ declare namespace Mastodon {
url: string url: string
} }
type Notification = { type Notification =
// Base | {
id: string // Base
type: id: string
| 'follow' type: 'favourite' | 'mention' | 'poll' | 'reblog' | 'status' | 'update'
| 'follow_request' created_at: string
| 'mention' account: Account
| 'reblog' status: Status
| 'favourite' report: undefined
| 'poll' }
| 'status' | {
| 'update' // Base
created_at: string id: string
account: Account type: 'follow' | 'follow_request' | 'admin.sign_up'
created_at: string
// Others account: Account
status?: Status status: undefined
} report: undefined
}
| {
// Base
id: string
type: 'admin.report'
created_at: string
account: Account
status: undefined
report: Report
}
type Poll = { type Poll = {
// Base // Base
@ -384,6 +395,9 @@ declare namespace Mastodon {
mention: boolean mention: boolean
poll: boolean poll: boolean
status: boolean status: boolean
update: boolean
'admin.sign_up': boolean
'admin.report': boolean
} }
server_key: string server_key: string
} }
@ -403,12 +417,37 @@ declare namespace Mastodon {
note: string 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 = { type Results = {
accounts?: Account[] accounts?: Account[]
statuses?: Status[] statuses?: Status[]
hashtags?: Tag[] 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 = { type Status = {
// Base // Base
id: string id: string
@ -479,25 +518,4 @@ declare namespace Mastodon {
history: { day: string; accounts: string; uses: string }[] history: { day: string; accounts: string; uses: string }[]
following: boolean // Since v4.0 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 'gl-react-blurhash'
declare module 'htmlparser2-without-node-native' declare module 'htmlparser2-without-node-native'
declare module 'li'
declare module 'react-native-feather' declare module 'react-native-feather'
declare module 'react-native-htmlview' declare module 'react-native-htmlview'
declare module 'react-native-toast-message' declare module 'react-native-toast-message'
declare module 'rtl-detect'
declare module '@helpers/features' { declare module '@helpers/features' {
const features: { feature: string; version: number; reference?: string }[] 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 { GestureHandlerRootView } from 'react-native-gesture-handler'
import { SafeAreaProvider } from 'react-native-safe-area-context' import { SafeAreaProvider } from 'react-native-safe-area-context'
import { enableFreeze } from 'react-native-screens' import { enableFreeze } from 'react-native-screens'
import { QueryClientProvider } from 'react-query' import { QueryClientProvider } from '@tanstack/react-query'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
import { PersistGate } from 'redux-persist/integration/react' 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}` instance => paths[0] === `@${instance.account.acct}@${instance.uri}`
) )
if (instanceIndex !== -1 && instanceActive !== instanceIndex) { if (instanceIndex !== -1 && instanceActive !== instanceIndex) {
initQuery({ initQuery({ instance: instances[instanceIndex] })
instance: instances[instanceIndex],
prefetch: { enabled: true }
})
} }
} }
} }

View File

@ -1,5 +1,5 @@
import axios from 'axios' import axios from 'axios'
import { ctx, handleError, userAgent } from './helpers' import { ctx, handleError, PagedResponse, userAgent } from './helpers'
export type Params = { export type Params = {
method: 'get' | 'post' | 'put' | 'delete' method: 'get' | 'post' | 'put' | 'delete'
@ -19,7 +19,7 @@ const apiGeneral = async <T = unknown>({
params, params,
headers, headers,
body body
}: Params): Promise<{ body: T }> => { }: Params): Promise<PagedResponse<T>> => {
console.log( console.log(
ctx.bgGreen.bold(' API general ') + ctx.bgGreen.bold(' API general ') +
' ' + ' ' +
@ -47,9 +47,27 @@ const apiGeneral = async <T = unknown>({
...(body && { data: body }) ...(body && { data: body })
}) })
.then(response => { .then(response => {
return Promise.resolve({ let links: {
body: response.data 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()) .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 } export { ctx, handleError, userAgent }

View File

@ -1,7 +1,6 @@
import { RootState } from '@root/store' import { RootState } from '@root/store'
import axios, { AxiosRequestConfig } from 'axios' import axios, { AxiosRequestConfig } from 'axios'
import li from 'li' import { ctx, handleError, PagedResponse, userAgent } from './helpers'
import { ctx, handleError, userAgent } from './helpers'
export type Params = { export type Params = {
method: 'get' | 'post' | 'put' | 'delete' | 'patch' method: 'get' | 'post' | 'put' | 'delete' | 'patch'
@ -15,11 +14,6 @@ export type Params = {
extras?: Omit<AxiosRequestConfig, 'method' | 'url' | 'params' | 'headers' | 'data'> 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>({ const apiInstance = async <T = unknown>({
method, method,
version = 'v1', version = 'v1',
@ -28,7 +22,7 @@ const apiInstance = async <T = unknown>({
headers, headers,
body, body,
extras extras
}: Params): Promise<InstanceResponse<T>> => { }: Params): Promise<PagedResponse<T>> => {
const { store } = require('@root/store') const { store } = require('@root/store')
const state = store.getState() as RootState const state = store.getState() as RootState
const instanceActive = state.instances.instances.findIndex(instance => instance.active) const instanceActive = state.instances.instances.findIndex(instance => instance.active)
@ -74,17 +68,27 @@ const apiInstance = async <T = unknown>({
...extras ...extras
}) })
.then(response => { .then(response => {
let prev let links: {
let next prev?: { id: string; isOffset: boolean }
next?: { id: string; isOffset: boolean }
} = {}
if (response.headers?.link) { if (response.headers?.link) {
const headersLinks = li.parse(response.headers?.link) const linksParsed = response.headers.link.matchAll(
prev = headersLinks.prev?.match(/_id=([0-9]*)/)?.[1] new RegExp('[?&](.*?_id|offset)=(.*?)>; *rel="(.*?)"', 'gi')
next = headersLinks.next?.match(/_id=([0-9]*)/)?.[1] )
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({ return Promise.resolve({ body: response.data, links })
body: response.data,
links: { prev, next }
})
}) })
.catch(handleError()) .catch(handleError())
} }

View File

@ -12,11 +12,7 @@ interface Props {
additionalActions?: () => void additionalActions?: () => void
} }
const AccountButton: React.FC<Props> = ({ const AccountButton: React.FC<Props> = ({ instance, selected = false, additionalActions }) => {
instance,
selected = false,
additionalActions
}) => {
const navigation = useNavigation() const navigation = useNavigation()
return ( return (
@ -27,12 +23,10 @@ const AccountButton: React.FC<Props> = ({
marginBottom: StyleConstants.Spacing.M, marginBottom: StyleConstants.Spacing.M,
marginRight: StyleConstants.Spacing.M marginRight: StyleConstants.Spacing.M
}} }}
content={`@${instance.account.acct}@${instance.uri}${ content={`@${instance.account.acct}@${instance.uri}${selected ? ' ✓' : ''}`}
selected ? ' ✓' : ''
}`}
onPress={() => { onPress={() => {
haptics('Light') haptics('Light')
initQuery({ instance, prefetch: { enabled: true } }) initQuery({ instance })
navigation.goBack() navigation.goBack()
if (additionalActions) { if (additionalActions) {
additionalActions() additionalActions()

View File

@ -4,15 +4,13 @@ import React, { useMemo, useState } from 'react'
import { import {
AccessibilityProps, AccessibilityProps,
Image, Image,
ImageStyle,
Platform,
Pressable, Pressable,
StyleProp, StyleProp,
StyleSheet, StyleSheet,
View, View,
ViewStyle ViewStyle
} from 'react-native' } from 'react-native'
import FastImage from 'react-native-fast-image' import FastImage, { ImageStyle } from 'react-native-fast-image'
import { Blurhash } from 'react-native-blurhash' import { Blurhash } from 'react-native-blurhash'
// blurhas -> if blurhash, show before any loading succeed // blurhas -> if blurhash, show before any loading succeed
@ -97,30 +95,17 @@ const GracefullyImage = ({
{...(onPress ? (hidden ? { disabled: true } : { onPress }) : { disabled: true })} {...(onPress ? (hidden ? { disabled: true } : { onPress }) : { disabled: true })}
> >
{uri.preview && !imageLoaded ? ( {uri.preview && !imageLoaded ? (
<Image <FastImage
fadeDuration={0}
source={{ uri: uri.preview }} source={{ uri: uri.preview }}
style={[styles.placeholder, { backgroundColor: colors.shimmerDefault }]} style={[styles.placeholder, { backgroundColor: colors.shimmerDefault }]}
/> />
) : null} ) : null}
{Platform.OS === 'ios' ? ( <FastImage
<Image source={source}
fadeDuration={0} style={[{ flex: 1 }, imageStyle]}
source={source} onLoad={onLoad}
style={[{ flex: 1 }, imageStyle]} onError={onError}
onLoad={onLoad} />
onError={onError}
/>
) : (
<FastImage
fadeDuration={0}
source={source}
// @ts-ignore
style={[{ flex: 1 }, imageStyle]}
onLoad={onLoad}
onError={onError}
/>
)}
{blurhashView} {blurhashView}
</Pressable> </Pressable>
) )

View File

@ -3,8 +3,8 @@ import { StackNavigationProp } from '@react-navigation/stack'
import { TabLocalStackParamList } from '@utils/navigation/navigators' import { TabLocalStackParamList } from '@utils/navigation/navigators'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useState } from 'react' import React, { PropsWithChildren, useCallback, useState } from 'react'
import { Dimensions, Pressable } from 'react-native' import { Dimensions, Pressable, View } from 'react-native'
import Sparkline from './Sparkline' import Sparkline from './Sparkline'
import CustomText from './Text' import CustomText from './Text'
@ -13,7 +13,11 @@ export interface Props {
onPress?: () => void onPress?: () => void
} }
const ComponentHashtag: React.FC<Props> = ({ hashtag, onPress: customOnPress }) => { const ComponentHashtag: React.FC<PropsWithChildren & Props> = ({
hashtag,
onPress: customOnPress,
children
}) => {
const { colors } = useTheme() const { colors } = useTheme()
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>() const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
@ -31,15 +35,11 @@ const ComponentHashtag: React.FC<Props> = ({ hashtag, onPress: customOnPress })
style={{ style={{
flex: 1, flex: 1,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
padding padding
}} }}
onPress={customOnPress || onPress} onPress={customOnPress || onPress}
onLayout={({
nativeEvent: {
layout: { height }
}
}) => setHeight(height - padding * 2 - 1)}
> >
<CustomText <CustomText
fontStyle='M' fontStyle='M'
@ -52,11 +52,22 @@ const ComponentHashtag: React.FC<Props> = ({ hashtag, onPress: customOnPress })
> >
#{hashtag.name} #{hashtag.name}
</CustomText> </CustomText>
<Sparkline <View
data={hashtag.history.map(h => parseInt(h.uses)).reverse()} style={{ flexDirection: 'row', alignItems: 'center' }}
width={width} onLayout={({
height={height} 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> </Pressable>
) )
} }

View File

@ -1,6 +1,4 @@
import Icon from '@components/Icon'
import CustomText from '@components/Text' import CustomText from '@components/Text'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react' import React from 'react'
import { View } from 'react-native' import { View } from 'react-native'
@ -9,16 +7,10 @@ export interface Props {
content?: string content?: string
inverted?: boolean inverted?: boolean
onPress?: () => void onPress?: () => void
dropdown?: boolean
} }
// Used for Android mostly // Used for Android mostly
const HeaderCenter: React.FC<Props> = ({ const HeaderCenter: React.FC<Props> = ({ content, inverted = false, onPress }) => {
content,
inverted = false,
onPress,
dropdown = false
}) => {
const { colors } = useTheme() const { colors } = useTheme()
return ( return (
@ -33,13 +25,6 @@ const HeaderCenter: React.FC<Props> = ({
children={content} children={content}
{...(onPress && { onPress })} {...(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>
) )
} }

View File

@ -11,6 +11,7 @@ export interface Props {
fill?: string fill?: string
strokeWidth?: number strokeWidth?: number
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
crossOut?: boolean
} }
const Icon: React.FC<Props> = ({ const Icon: React.FC<Props> = ({
@ -20,7 +21,8 @@ const Icon: React.FC<Props> = ({
color, color,
fill, fill,
strokeWidth = 2, strokeWidth = 2,
style style,
crossOut = false
}) => { }) => {
return ( return (
<View <View
@ -42,6 +44,17 @@ const Icon: React.FC<Props> = ({
fill, fill,
strokeWidth strokeWidth
})} })}
{crossOut ? (
<View
style={{
position: 'absolute',
transform: [{ rotate: '45deg' }],
width: size * 1.35,
borderBottomColor: color,
borderBottomWidth: strokeWidth
}}
/>
) : null}
</View> </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 Button from '@components/Button'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import browserPackage from '@helpers/browserPackage' 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 { 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 { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import * as AuthSession from 'expo-auth-session'
import * as WebBrowser from 'expo-web-browser' import * as WebBrowser from 'expo-web-browser'
import { debounce } from 'lodash' 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 { Trans, useTranslation } from 'react-i18next'
import { Alert, Image, KeyboardAvoidingView, Platform, TextInput, View } from 'react-native' import { Alert, Image, KeyboardAvoidingView, Platform, TextInput, View } from 'react-native'
import { ScrollView } from 'react-native-gesture-handler' import { ScrollView } from 'react-native-gesture-handler'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { Placeholder } from 'rn-placeholder' import { Placeholder } from 'rn-placeholder'
import InstanceAuth from './Instance/Auth' import InstanceInfo from './Info'
import InstanceInfo from './Instance/Info' import CustomText from '../Text'
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 { export interface Props {
scrollViewRef?: RefObject<ScrollView> scrollViewRef?: RefObject<ScrollView>
@ -31,30 +36,64 @@ const ComponentInstance: React.FC<Props> = ({
}) => { }) => {
const { t } = useTranslation('componentInstance') const { t } = useTranslation('componentInstance')
const { colors, mode } = useTheme() 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 instances = useSelector(getInstances, () => true)
const [domain, setDomain] = useState<string>()
const instanceQuery = useInstanceQuery({ const instanceQuery = useInstanceQuery({
domain, domain,
options: { enabled: !!domain, retry: false } options: { enabled: !!domain, retry: false }
}) })
const appsQuery = useAppsQuery({
domain,
options: { enabled: false, retry: false }
})
const onChangeText = useCallback( const deprecateAuthFollow = useSelector(checkInstanceFeature('deprecate_auth_follow'))
debounce(
text => { const appsMutation = useAppsMutation({
setDomain(text.replace(/^http(s)?\:\/\//i, '')) retry: false,
appsQuery.remove() onSuccess: async (data, variables) => {
}, const clientId = data.client_id
1000, const clientSecret = data.client_secret
{ trailing: true }
), const discovery = { authorizationEndpoint: `https://${domain}/oauth/authorize` }
[]
) const request = new AuthSession.AuthRequest({
clientId,
clientSecret,
scopes: deprecateAuthFollow
? ['read', 'write', 'push']
: ['read', 'write', 'follow', 'push'],
redirectUri
})
await request.makeAuthUrlAsync(discovery)
const promptResult = await request.promptAsync(discovery)
if (promptResult?.type === 'success') {
const { accessToken } = await AuthSession.exchangeCodeAsync(
{
clientId,
clientSecret,
scopes: ['read', 'write', 'follow', 'push'],
redirectUri,
code: promptResult.params.code,
extraParams: { grant_type: 'authorization_code' }
},
{ tokenEndpoint: `https://${variables.domain}/oauth/token` }
)
queryClient.clear()
dispatch(
addInstance({
domain,
token: accessToken,
instance: instanceQuery.data!,
appData: { clientId, clientSecret }
})
)
goBack && navigation.goBack()
}
}
})
const processUpdate = useCallback(() => { const processUpdate = useCallback(() => {
if (domain) { if (domain) {
@ -66,39 +105,15 @@ const ComponentInstance: React.FC<Props> = ({
}, },
{ {
text: t('common:buttons.continue'), text: t('common:buttons.continue'),
onPress: () => { onPress: () => appsMutation.mutate({ domain })
appsQuery.refetch()
}
} }
]) ])
} else { } else {
appsQuery.refetch() appsMutation.mutate({ domain })
} }
} }
}, [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 ( return (
<KeyboardAvoidingView <KeyboardAvoidingView
style={{ flex: 1 }} style={{ flex: 1 }}
@ -131,7 +146,8 @@ const ComponentInstance: React.FC<Props> = ({
borderBottomWidth: 1, borderBottomWidth: 1,
...StyleConstants.FontStyle.M, ...StyleConstants.FontStyle.M,
color: colors.primaryDefault, color: colors.primaryDefault,
borderBottomColor: instanceQuery.isError ? colors.red : colors.border borderBottomColor: instanceQuery.isError ? colors.red : colors.border,
...(Platform.OS === 'android' && { paddingRight: 0 })
}} }}
editable={false} editable={false}
defaultValue='https://' defaultValue='https://'
@ -143,9 +159,12 @@ const ComponentInstance: React.FC<Props> = ({
...StyleConstants.FontStyle.M, ...StyleConstants.FontStyle.M,
marginRight: StyleConstants.Spacing.M, marginRight: StyleConstants.Spacing.M,
color: colors.primaryDefault, 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' autoCapitalize='none'
clearButtonMode='never' clearButtonMode='never'
keyboardType='url' keyboardType='url'
@ -176,7 +195,7 @@ const ComponentInstance: React.FC<Props> = ({
content={t('server.button')} content={t('server.button')}
onPress={processUpdate} onPress={processUpdate}
disabled={!instanceQuery.data?.uri} disabled={!instanceQuery.data?.uri}
loading={instanceQuery.isFetching || appsQuery.isFetching} loading={instanceQuery.isFetching || appsMutation.isLoading}
/> />
</View> </View>
@ -276,8 +295,6 @@ const ComponentInstance: React.FC<Props> = ({
</View> </View>
</View> </View>
</View> </View>
{requestAuth}
</KeyboardAvoidingView> </KeyboardAvoidingView>
) )
} }

View File

@ -5,7 +5,7 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import { ColorDefinitions } from '@utils/styles/themes' import { ColorDefinitions } from '@utils/styles/themes'
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { Text, View } from 'react-native' import { View } from 'react-native'
import { Flow } from 'react-native-animated-spinkit' import { Flow } from 'react-native-animated-spinkit'
import { State, Switch, TapGestureHandler } from 'react-native-gesture-handler' import { State, Switch, TapGestureHandler } from 'react-native-gesture-handler'
@ -65,7 +65,6 @@ const MenuRow: React.FC<Props> = ({
> >
<TapGestureHandler <TapGestureHandler
onHandlerStateChange={async ({ nativeEvent }) => { onHandlerStateChange={async ({ nativeEvent }) => {
if (typeof iconBack !== 'string') return // Let icon back handles the gesture
if (nativeEvent.state === State.ACTIVE && !loading) { if (nativeEvent.state === State.ACTIVE && !loading) {
if (screenReaderEnabled && switchOnValueChange) { if (screenReaderEnabled && switchOnValueChange) {
switchOnValueChange() switchOnValueChange()
@ -86,9 +85,10 @@ const MenuRow: React.FC<Props> = ({
> >
<View <View
style={{ style={{
flex: 3, flexShrink: 3,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center' alignItems: 'center',
marginRight: StyleConstants.Spacing.M
}} }}
> >
{iconFront && ( {iconFront && (

View File

@ -5,7 +5,7 @@ import { StyleConstants } from '@utils/styles/constants'
import { adaptiveScale } from '@utils/styles/scaling' import { adaptiveScale } from '@utils/styles/scaling'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useMemo } from 'react' 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 FastImage from 'react-native-fast-image'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import validUrl from 'valid-url' import validUrl from 'valid-url'
@ -18,16 +18,11 @@ export interface Props {
size?: 'S' | 'M' | 'L' size?: 'S' | 'M' | 'L'
adaptiveSize?: boolean adaptiveSize?: boolean
fontBold?: boolean fontBold?: boolean
style?: TextStyle
} }
const ParseEmojis = React.memo( const ParseEmojis = React.memo(
({ ({ content, emojis, size = 'M', adaptiveSize = false, fontBold = false, style }: Props) => {
content,
emojis,
size = 'M',
adaptiveSize = false,
fontBold = false
}: Props) => {
const { reduceMotionEnabled } = useAccessibility() const { reduceMotionEnabled } = useAccessibility()
const adaptiveFontsize = useSelector(getSettingsFontsize) const adaptiveFontsize = useSelector(getSettingsFontsize)
@ -51,22 +46,13 @@ const ParseEmojis = React.memo(
image: { image: {
width: adaptedFontsize, width: adaptedFontsize,
height: adaptedFontsize, height: adaptedFontsize,
...(Platform.OS === 'ios' ...(Platform.OS === 'android' && { transform: [{ translateY: 2 }] })
? {
transform: [{ translateY: -2 }]
}
: {
transform: [{ translateY: 1 }]
})
} }
}) })
}, [theme, adaptiveFontsize]) }, [theme, adaptiveFontsize])
return ( return (
<CustomText <CustomText style={[styles.text, style]} fontWeight={fontBold ? 'Bold' : undefined}>
style={styles.text}
fontWeight={fontBold ? 'Bold' : undefined}
>
{emojis ? ( {emojis ? (
content content
.split(regexEmoji) .split(regexEmoji)
@ -78,11 +64,7 @@ const ParseEmojis = React.memo(
return emojiShortcode === `:${emoji.shortcode}:` return emojiShortcode === `:${emoji.shortcode}:`
}) })
if (emojiIndex === -1) { if (emojiIndex === -1) {
return ( return <CustomText key={emojiShortcode + i}>{emojiShortcode}</CustomText>
<CustomText key={emojiShortcode + i}>
{emojiShortcode}
</CustomText>
)
} else { } else {
const uri = reduceMotionEnabled const uri = reduceMotionEnabled
? emojis[emojiIndex].static_url ? emojis[emojiIndex].static_url

View File

@ -10,9 +10,10 @@ import { StyleConstants } from '@utils/styles/constants'
import layoutAnimation from '@utils/styles/layoutAnimation' import layoutAnimation from '@utils/styles/layoutAnimation'
import { adaptiveScale } from '@utils/styles/scaling' import { adaptiveScale } from '@utils/styles/scaling'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import { isEqual } from 'lodash'
import React, { useCallback, useState } from 'react' import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' 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 HTMLView from 'react-native-htmlview'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
@ -133,13 +134,8 @@ const renderNode = ({
name='ExternalLink' name='ExternalLink'
size={adaptedFontsize} size={adaptedFontsize}
style={{ style={{
...(Platform.OS === 'ios' marginLeft: StyleConstants.Spacing.XS,
? { ...(Platform.OS === 'android' && { transform: [{ translateY: 2 }] })
transform: [{ translateY: -2 }]
}
: {
transform: [{ translateY: 1 }]
})
}} }}
/> />
) : null} ) : null}
@ -158,6 +154,7 @@ const renderNode = ({
export interface Props { export interface Props {
content: string content: string
size?: 'S' | 'M' | 'L' size?: 'S' | 'M' | 'L'
textStyles?: TextStyleIOS
adaptiveSize?: boolean adaptiveSize?: boolean
emojis?: Mastodon.Emoji[] emojis?: Mastodon.Emoji[]
mentions?: Mastodon.Mention[] mentions?: Mastodon.Mention[]
@ -175,6 +172,7 @@ const ParseHTML = React.memo(
({ ({
content, content,
size = 'M', size = 'M',
textStyles,
adaptiveSize = false, adaptiveSize = false,
emojis, emojis,
mentions, mentions,
@ -200,7 +198,7 @@ const ParseHTML = React.memo(
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>() const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const route = useRoute() const route = useRoute()
const { colors, theme } = useTheme() const { colors, theme } = useTheme()
const { t, i18n } = useTranslation('componentParse') const { t } = useTranslation('componentParse')
if (!expandHint) { if (!expandHint) {
expandHint = t('HTML.defaultHint') expandHint = t('HTML.defaultHint')
} }
@ -298,6 +296,7 @@ const ParseHTML = React.memo(
} }
}} }}
style={{ style={{
...textStyles,
height: numberOfLines === 1 && !expanded ? 0 : undefined height: numberOfLines === 1 && !expanded ? 0 : undefined
}} }}
numberOfLines={ numberOfLines={
@ -308,7 +307,7 @@ const ParseHTML = React.memo(
</View> </View>
) )
}, },
[theme, i18n.language] [theme]
) )
return ( 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 export default ParseHTML

View File

@ -8,7 +8,7 @@ import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { StyleSheet, View } from 'react-native' import { StyleSheet, View } from 'react-native'
import { useQueryClient } from 'react-query' import { useQueryClient } from '@tanstack/react-query'
export interface Props { export interface Props {
id: Mastodon.Account['id'] id: Mastodon.Account['id']

View File

@ -10,7 +10,7 @@ import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useQueryClient } from 'react-query' import { useQueryClient } from '@tanstack/react-query'
export interface Props { export interface Props {
id: Mastodon.Account['id'] id: Mastodon.Account['id']
@ -30,8 +30,8 @@ const RelationshipOutgoing = React.memo(
haptics('Success') haptics('Success')
queryClient.setQueryData<Mastodon.Relationship[]>(queryKeyRelationship, [res]) queryClient.setQueryData<Mastodon.Relationship[]>(queryKeyRelationship, [res])
if (action === 'block') { if (action === 'block') {
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Following' }] const queryKey = ['Timeline', { page: 'Following' }]
queryClient.invalidateQueries(queryKey) queryClient.invalidateQueries({ queryKey, exact: false })
} }
}, },
onError: (err: any, { payload: { action } }) => { onError: (err: any, { payload: { action } }) => {

View File

@ -1,7 +1,6 @@
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import { maxBy, minBy } from 'lodash' import { maxBy, minBy } from 'lodash'
import React from 'react' import React from 'react'
import { Platform } from 'react-native'
import Svg, { G, Path } from 'react-native-svg' import Svg, { G, Path } from 'react-native-svg'
export interface Props { export interface Props {
@ -69,7 +68,7 @@ const Sparkline: React.FC<Props> = ({ data, width, height, margin = 0 }) => {
const fillPoints = linePoints.concat(closePolyPoints) const fillPoints = linePoints.concat(closePolyPoints)
return ( return (
<Svg height={Platform.OS !== 'android' ? 'auto' : 24} width={width}> <Svg height={height} width={width} style={{ marginRight: margin }}>
<G> <G>
<Path d={'M' + fillPoints.join(' ')} fill={colors.blue} fillOpacity={0.1} /> <Path d={'M' + fillPoints.join(' ')} fill={colors.blue} fillOpacity={0.1} />
<Path <Path

View File

@ -1,68 +1,61 @@
import ComponentSeparator from '@components/Separator' import ComponentSeparator from '@components/Separator'
import { useScrollToTop } from '@react-navigation/native' import { useScrollToTop } from '@react-navigation/native'
import { UseInfiniteQueryOptions } from '@tanstack/react-query'
import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline' import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
import { getInstanceActive } from '@utils/slices/instancesSlice' import { getInstanceActive } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { RefObject, useCallback, useRef } from 'react' import React, { RefObject, useCallback, useRef } from 'react'
import { FlatList, FlatListProps, Platform, RefreshControl } from 'react-native' import { FlatList, FlatListProps, Platform, RefreshControl } from 'react-native'
import Animated, { import Animated, { useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated'
useAnimatedScrollHandler,
useSharedValue
} from 'react-native-reanimated'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import TimelineEmpty from './Timeline/Empty' import TimelineEmpty from './Timeline/Empty'
import TimelineFooter from './Timeline/Footer' import TimelineFooter from './Timeline/Footer'
import TimelineRefresh, { import TimelineRefresh, { SEPARATION_Y_1, SEPARATION_Y_2 } from './Timeline/Refresh'
SEPARATION_Y_1,
SEPARATION_Y_2
} from './Timeline/Refresh'
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList) const AnimatedFlatList = Animated.createAnimatedComponent(FlatList)
export interface Props { export interface Props {
flRef?: RefObject<FlatList<any>> flRef?: RefObject<FlatList<any>>
queryKey: QueryKeyTimeline queryKey: QueryKeyTimeline
queryOptions?: Omit<
UseInfiniteQueryOptions<any>,
'notifyOnChangeProps' | 'getNextPageParam' | 'getPreviousPageParam' | 'select' | 'onSuccess'
>
disableRefresh?: boolean disableRefresh?: boolean
disableInfinity?: 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> = ({ const Timeline: React.FC<Props> = ({
flRef: customFLRef, flRef: customFLRef,
queryKey, queryKey,
queryOptions,
disableRefresh = false, disableRefresh = false,
disableInfinity = false, disableInfinity = false,
customProps customProps
}) => { }) => {
const { colors } = useTheme() const { colors } = useTheme()
const { const { data, refetch, isFetching, isLoading, fetchNextPage, isFetchingNextPage } =
data, useTimelineQuery({
refetch, ...queryKey[1],
isFetching, options: {
isLoading, ...queryOptions,
fetchNextPage, notifyOnChangeProps: Platform.select({
isFetchingNextPage ios: ['dataUpdatedAt', 'isFetching'],
} = useTimelineQuery({ android: ['dataUpdatedAt', 'isFetching', 'isLoading']
...queryKey[1], }),
options: { getNextPageParam: lastPage =>
notifyOnChangeProps: Platform.select({ lastPage?.links?.next && {
ios: ['dataUpdatedAt', 'isFetching'], ...(lastPage.links.next.isOffset
android: ['dataUpdatedAt', 'isFetching', 'isLoading'] ? { offset: lastPage.links.next.id }
}), : { max_id: lastPage.links.next.id })
getNextPageParam: lastPage => }
lastPage?.links?.next && { }
max_id: lastPage.links.next })
}
}
})
const flattenData = data?.pages const flattenData = data?.pages ? data.pages?.flatMap(page => [...page.body]) : []
? data.pages?.flatMap(page => [...page.body])
: []
const onEndReached = useCallback( const onEndReached = useCallback(
() => !disableInfinity && !isFetchingNextPage && fetchNextPage(), () => !disableInfinity && !isFetchingNextPage && fetchNextPage(),
@ -134,10 +127,7 @@ const Timeline: React.FC<Props> = ({
onEndReached={onEndReached} onEndReached={onEndReached}
onEndReachedThreshold={0.75} onEndReachedThreshold={0.75}
ListFooterComponent={ ListFooterComponent={
<TimelineFooter <TimelineFooter queryKey={queryKey} disableInfinity={disableInfinity} />
queryKey={queryKey}
disableInfinity={disableInfinity}
/>
} }
ListEmptyComponent={<TimelineEmpty queryKey={queryKey} />} ListEmptyComponent={<TimelineEmpty queryKey={queryKey} />}
ItemSeparatorComponent={({ leadingItem }) => ItemSeparatorComponent={({ leadingItem }) =>
@ -145,9 +135,7 @@ const Timeline: React.FC<Props> = ({
<ComponentSeparator extraMarginLeft={0} /> <ComponentSeparator extraMarginLeft={0} />
) : ( ) : (
<ComponentSeparator <ComponentSeparator
extraMarginLeft={ extraMarginLeft={StyleConstants.Avatar.M + StyleConstants.Spacing.S}
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 { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { Pressable, View } from 'react-native' 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 TimelineActions from './Shared/Actions'
import TimelineContent from './Shared/Content' import TimelineContent from './Shared/Content'
import StatusContext from './Shared/Context' import StatusContext from './Shared/Context'

View File

@ -34,6 +34,7 @@ export interface Props {
highlighted?: boolean highlighted?: boolean
disableDetails?: boolean disableDetails?: boolean
disableOnPress?: boolean disableOnPress?: boolean
isConversation?: boolean
} }
// When the poll is long // When the poll is long
@ -43,7 +44,8 @@ const TimelineDefault: React.FC<Props> = ({
rootQueryKey, rootQueryKey,
highlighted = false, highlighted = false,
disableDetails = false, disableDetails = false,
disableOnPress = false disableOnPress = false,
isConversation = false
}) => { }) => {
const { colors } = useTheme() const { colors } = useTheme()
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>() const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
@ -69,9 +71,12 @@ const TimelineDefault: React.FC<Props> = ({
} }
const mainStyle: StyleProp<ViewStyle> = { 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, backgroundColor: colors.backgroundDefault,
paddingBottom: disableDetails ? StyleConstants.Spacing.Global.PagePadding : 0 paddingBottom: disableDetails ? StyleConstants.Spacing.Global.PagePadding / 1.5 : 0
} }
const main = () => ( const main = () => (
<> <>
@ -81,7 +86,13 @@ const TimelineDefault: React.FC<Props> = ({
<TimelineActioned action='pinned' /> <TimelineActioned action='pinned' />
) : null} ) : null}
<View style={{ flex: 1, width: '100%', flexDirection: 'row' }}> <View
style={{
flex: 1,
flexDirection: 'row',
...(disableDetails && { alignItems: 'flex-start', overflow: 'hidden' })
}}
>
<TimelineAvatar /> <TimelineAvatar />
<TimelineHeaderDefault /> <TimelineHeaderDefault />
</View> </View>
@ -89,7 +100,11 @@ const TimelineDefault: React.FC<Props> = ({
<View <View
style={{ style={{
paddingTop: highlighted ? StyleConstants.Spacing.S : 0, 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} /> <TimelineContent setSpoilerExpanded={setSpoilerExpanded} />
@ -125,8 +140,10 @@ const TimelineDefault: React.FC<Props> = ({
spoilerHidden, spoilerHidden,
copiableContent, copiableContent,
highlighted, highlighted,
inThread: queryKey?.[1].page === 'Toot',
disableDetails, disableDetails,
disableOnPress disableOnPress,
isConversation
}} }}
> >
{disableOnPress ? ( {disableOnPress ? (

View File

@ -21,7 +21,11 @@ const TimelineFooter = React.memo(
enabled: !disableInfinity, enabled: !disableInfinity,
notifyOnChangeProps: ['hasNextPage'], notifyOnChangeProps: ['hasNextPage'],
getNextPageParam: lastPage => 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 <Trans
i18nKey='componentTimeline:end.message' i18nKey='componentTimeline:end.message'
components={[ components={[
<Icon <Icon name='Coffee' size={StyleConstants.Font.Size.S} color={colors.secondary} />
name='Coffee'
size={StyleConstants.Font.Size.S}
color={colors.secondary}
/>
]} ]}
/> />
</CustomText> </CustomText>

View File

@ -28,18 +28,18 @@ import TimelineHeaderAndroid from './Shared/HeaderAndroid'
export interface Props { export interface Props {
notification: Mastodon.Notification notification: Mastodon.Notification
queryKey: QueryKeyTimeline queryKey: QueryKeyTimeline
highlighted?: boolean
} }
const TimelineNotifications: React.FC<Props> = ({ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
notification,
queryKey,
highlighted = false
}) => {
const instanceAccount = useSelector(getInstanceAccount, () => true) const instanceAccount = useSelector(getInstanceAccount, () => true)
const status = notification.status?.reblog ? notification.status.reblog : notification.status 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 ownAccount = notification.account?.id === instanceAccount?.id
const [spoilerExpanded, setSpoilerExpanded] = useState( const [spoilerExpanded, setSpoilerExpanded] = useState(
instanceAccount.preferences['reading:expand:spoilers'] || false instanceAccount.preferences['reading:expand:spoilers'] || false
@ -91,7 +91,8 @@ const TimelineNotifications: React.FC<Props> = ({
notification.type === 'follow' || notification.type === 'follow' ||
notification.type === 'follow_request' || notification.type === 'follow_request' ||
notification.type === 'mention' || notification.type === 'mention' ||
notification.type === 'status' notification.type === 'status' ||
notification.type === 'admin.sign_up'
? 1 ? 1
: 0.5 : 0.5
}} }}
@ -102,13 +103,11 @@ const TimelineNotifications: React.FC<Props> = ({
</View> </View>
{notification.status ? ( {notification.status ? (
<View <View style={{ paddingLeft: StyleConstants.Avatar.M + StyleConstants.Spacing.S }}>
style={{ <TimelineContent
paddingTop: highlighted ? StyleConstants.Spacing.S : 0, notificationOwnToot={['favourite', 'reblog'].includes(notification.type)}
paddingLeft: highlighted ? 0 : StyleConstants.Avatar.M + StyleConstants.Spacing.S setSpoilerExpanded={setSpoilerExpanded}
}} />
>
<TimelineContent setSpoilerExpanded={setSpoilerExpanded} />
<TimelinePoll /> <TimelinePoll />
<TimelineAttachment /> <TimelineAttachment />
<TimelineCard /> <TimelineCard />
@ -138,8 +137,7 @@ const TimelineNotifications: React.FC<Props> = ({
status, status,
ownAccount, ownAccount,
spoilerHidden, spoilerHidden,
copiableContent, copiableContent
highlighted
}} }}
> >
<ContextMenu.Root> <ContextMenu.Root>

View File

@ -1,10 +1,6 @@
import haptics from '@components/haptics' import haptics from '@components/haptics'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { import { QueryKeyTimeline, TimelineData, useTimelineQuery } from '@utils/queryHooks/timeline'
QueryKeyTimeline,
TimelineData,
useTimelineQuery
} from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { RefObject, useCallback, useRef, useState } from 'react' import React, { RefObject, useCallback, useRef, useState } from 'react'
@ -20,7 +16,7 @@ import Animated, {
useSharedValue, useSharedValue,
withTiming withTiming
} from 'react-native-reanimated' } from 'react-native-reanimated'
import { InfiniteData, useQueryClient } from 'react-query' import { InfiniteData, useQueryClient } from '@tanstack/react-query'
export interface Props { export interface Props {
flRef: RefObject<FlatList<any>> flRef: RefObject<FlatList<any>>
@ -31,14 +27,8 @@ export interface Props {
} }
const CONTAINER_HEIGHT = StyleConstants.Spacing.M * 2.5 const CONTAINER_HEIGHT = StyleConstants.Spacing.M * 2.5
export const SEPARATION_Y_1 = -( export const SEPARATION_Y_1 = -(CONTAINER_HEIGHT / 2 + StyleConstants.Font.Size.S / 2)
CONTAINER_HEIGHT / 2 + export const SEPARATION_Y_2 = -(CONTAINER_HEIGHT * 1.5 + StyleConstants.Font.Size.S / 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> = ({ const TimelineRefresh: React.FC<Props> = ({
flRef, flRef,
@ -57,87 +47,77 @@ const TimelineRefresh: React.FC<Props> = ({
const fetchingLatestIndex = useRef(0) const fetchingLatestIndex = useRef(0)
const refetchActive = useRef(false) const refetchActive = useRef(false)
const { const { refetch, isFetching, isLoading, fetchPreviousPage, hasPreviousPage, isFetchingNextPage } =
refetch, useTimelineQuery({
isFetching, ...queryKey[1],
isLoading, options: {
fetchPreviousPage, getPreviousPageParam: firstPage =>
hasPreviousPage, firstPage?.links?.prev && {
isFetchingNextPage ...(firstPage.links.prev.isOffset
} = useTimelineQuery({ ? { offset: firstPage.links.prev.id }
...queryKey[1], : { max_id: firstPage.links.prev.id }),
options: { // https://github.com/facebook/react-native/issues/25239#issuecomment-731100372
getPreviousPageParam: firstPage => limit: '3'
firstPage?.links?.prev && { },
min_id: firstPage.links.prev, select: data => {
// https://github.com/facebook/react-native/issues/25239#issuecomment-731100372 if (refetchActive.current) {
limit: '3' data.pageParams = [data.pageParams[0]]
data.pages = [data.pages[0]]
refetchActive.current = false
}
return data
}, },
select: data => { onSuccess: () => {
if (refetchActive.current) { if (fetchingLatestIndex.current > 0) {
data.pageParams = [data.pageParams[0]] if (fetchingLatestIndex.current > 5) {
data.pages = [data.pages[0]]
refetchActive.current = false
}
return data
},
onSuccess: () => {
if (fetchingLatestIndex.current > 0) {
if (fetchingLatestIndex.current > 5) {
clearFirstPage()
fetchingLatestIndex.current = 0
} else {
if (hasPreviousPage) {
fetchPreviousPage()
fetchingLatestIndex.current++
} else {
clearFirstPage() clearFirstPage()
fetchingLatestIndex.current = 0 fetchingLatestIndex.current = 0
} else {
if (hasPreviousPage) {
fetchPreviousPage()
fetchingLatestIndex.current++
} else {
clearFirstPage()
fetchingLatestIndex.current = 0
}
} }
} }
} }
} }
} })
})
const { t } = useTranslation('componentTimeline') const { t } = useTranslation('componentTimeline')
const { colors } = useTheme() const { colors } = useTheme()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const clearFirstPage = () => { const clearFirstPage = () => {
queryClient.setQueryData<InfiniteData<TimelineData> | undefined>( queryClient.setQueryData<InfiniteData<TimelineData> | undefined>(queryKey, data => {
queryKey, if (data?.pages[0] && data.pages[0].body.length === 0) {
data => { return {
if (data?.pages[0] && data.pages[0].body.length === 0) { pages: data.pages.slice(1),
return { pageParams: data.pageParams.slice(1)
pages: data.pages.slice(1),
pageParams: data.pageParams.slice(1)
}
} else {
return data
} }
} else {
return data
} }
) })
} }
const prepareRefetch = () => { const prepareRefetch = () => {
refetchActive.current = true refetchActive.current = true
queryClient.setQueryData<InfiniteData<TimelineData> | undefined>( queryClient.setQueryData<InfiniteData<TimelineData> | undefined>(queryKey, data => {
queryKey, if (data) {
data => { data.pageParams = [undefined]
if (data) { const newFirstPage: TimelineData = { body: [] }
data.pageParams = [undefined] for (let page of data.pages) {
const newFirstPage: TimelineData = { body: [] } // @ts-ignore
for (let page of data.pages) { newFirstPage.body.push(...page.body)
// @ts-ignore if (newFirstPage.body.length > 10) break
newFirstPage.body.push(...page.body)
if (newFirstPage.body.length > 10) break
}
data.pages = [newFirstPage]
} }
data.pages = [newFirstPage]
return data
} }
)
return data
})
} }
const callRefetch = async () => { const callRefetch = async () => {
await refetch() await refetch()
@ -161,10 +141,7 @@ const TimelineRefresh: React.FC<Props> = ({
] ]
})) }))
const arrowTop = useAnimatedStyle(() => ({ const arrowTop = useAnimatedStyle(() => ({
marginTop: marginTop: scrollY.value < SEPARATION_Y_2 ? withTiming(CONTAINER_HEIGHT) : withTiming(0)
scrollY.value < SEPARATION_Y_2
? withTiming(CONTAINER_HEIGHT)
: withTiming(0)
})) }))
const arrowStage = useSharedValue(0) const arrowStage = useSharedValue(0)
@ -241,8 +218,7 @@ const TimelineRefresh: React.FC<Props> = ({
const headerPadding = useAnimatedStyle( const headerPadding = useAnimatedStyle(
() => ({ () => ({
paddingTop: paddingTop:
fetchingLatestIndex.current !== 0 || fetchingLatestIndex.current !== 0 || (isFetching && !isLoading && !isFetchingNextPage)
(isFetching && !isLoading && !isFetchingNextPage)
? withTiming(StyleConstants.Spacing.M * 2.5) ? withTiming(StyleConstants.Spacing.M * 2.5)
: withTiming(0) : withTiming(0)
}), }),
@ -254,10 +230,7 @@ const TimelineRefresh: React.FC<Props> = ({
<View style={styles.base}> <View style={styles.base}>
{isFetching ? ( {isFetching ? (
<View style={styles.container2}> <View style={styles.container2}>
<Circle <Circle size={StyleConstants.Font.Size.L} color={colors.secondary} />
size={StyleConstants.Font.Size.L}
color={colors.secondary}
/>
</View> </View>
) : ( ) : (
<> <>

View File

@ -28,7 +28,12 @@ const TimelineActioned: React.FC<Props> = ({ action, isNotification, ...rest })
const iconColor = colors.primaryDefault const iconColor = colors.primaryDefault
const content = (content: string) => ( 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 }) 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'))} {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: default:
return <></> return <></>
} }

View File

@ -17,7 +17,7 @@ import { uniqBy } from 'lodash'
import React, { useCallback, useContext, useMemo } from 'react' import React, { useCallback, useContext, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Pressable, StyleSheet, View } from 'react-native' import { Pressable, StyleSheet, View } from 'react-native'
import { useQueryClient } from 'react-query' import { useQueryClient } from '@tanstack/react-query'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import StatusContext from './Context' import StatusContext from './Context'

View File

@ -45,6 +45,21 @@ const TimelineAttachment = () => {
} }
const [sensitiveShown, setSensitiveShown] = useState(defaultSensitive()) 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 // @ts-ignore
const imageUrls: RootStackParamList['Screen-ImagesViewer']['imageUrls'] = status.media_attachments const imageUrls: RootStackParamList['Screen-ImagesViewer']['imageUrls'] = status.media_attachments
.map(attachment => { .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 { AppState, AppStateStatus, StyleSheet, View } from 'react-native'
import { Blurhash } from 'react-native-blurhash' import { Blurhash } from 'react-native-blurhash'
import AttachmentAltText from './AltText' import AttachmentAltText from './AltText'
import attachmentAspectRatio from './aspectRatio' import { aspectRatio } from './dimensions'
export interface Props { export interface Props {
total: number total: number
@ -64,7 +64,7 @@ const AttachmentAudio: React.FC<Props> = ({ total, index, sensitiveShown, audio
styles.base, styles.base,
{ {
backgroundColor: colors.disabled, 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 GracefullyImage from '@components/GracefullyImage'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react' import React from 'react'
import { View } from 'react-native' import { View } from 'react-native'
import AttachmentAltText from './AltText' import AttachmentAltText from './AltText'
import attachmentAspectRatio from './aspectRatio' import { aspectRatio } from './dimensions'
export interface Props { export interface Props {
total: number total: number
@ -20,6 +21,8 @@ const AttachmentImage = ({
image, image,
navigateToImagesViewer navigateToImagesViewer
}: Props) => { }: Props) => {
const { colors } = useTheme()
return ( return (
<View <View
style={{ style={{
@ -28,21 +31,16 @@ const AttachmentImage = ({
padding: StyleConstants.Spacing.XS / 2 padding: StyleConstants.Spacing.XS / 2
}} }}
> >
<GracefullyImage <View style={{ flex: 1, backgroundColor: colors.shimmerDefault }}>
accessibilityLabel={image.description} <GracefullyImage
hidden={sensitiveShown} accessibilityLabel={image.description}
uri={{ original: image.preview_url, remote: image.remote_url }} hidden={sensitiveShown}
blurhash={image.blurhash} uri={{ original: image.preview_url, remote: image.remote_url }}
onPress={() => navigateToImagesViewer(image.id)} blurhash={image.blurhash}
style={{ onPress={() => navigateToImagesViewer(image.id)}
aspectRatio: style={{ aspectRatio: aspectRatio({ total, index, ...image.meta?.original }) }}
total > 1 || !image.meta?.original?.width || !image.meta?.original?.height />
? attachmentAspectRatio({ total, index }) </View>
: image.meta.original.height / image.meta.original.width > 1
? 1
: image.meta.original.width / image.meta.original.height
}}
/>
<AttachmentAltText sensitiveShown={sensitiveShown} text={image.description} /> <AttachmentAltText sensitiveShown={sensitiveShown} text={image.description} />
</View> </View>
) )

View File

@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next'
import { View } from 'react-native' import { View } from 'react-native'
import { Blurhash } from 'react-native-blurhash' import { Blurhash } from 'react-native-blurhash'
import AttachmentAltText from './AltText' import AttachmentAltText from './AltText'
import attachmentAspectRatio from './aspectRatio' import { aspectRatio } from './dimensions'
export interface Props { export interface Props {
total: number total: number
@ -29,7 +29,7 @@ const AttachmentUnsupported: React.FC<Props> = ({ total, index, sensitiveShown,
padding: StyleConstants.Spacing.XS / 2, padding: StyleConstants.Spacing.XS / 2,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
aspectRatio: attachmentAspectRatio({ total, index }) aspectRatio: aspectRatio({ total, index, ...attachment.meta?.original })
}} }}
> >
{attachment.blurhash ? ( {attachment.blurhash ? (

View File

@ -4,9 +4,10 @@ import { ResizeMode, Video, VideoFullscreenUpdate } from 'expo-av'
import React, { useRef, useState } from 'react' import React, { useRef, useState } from 'react'
import { Pressable, View } from 'react-native' import { Pressable, View } from 'react-native'
import { Blurhash } from 'react-native-blurhash' import { Blurhash } from 'react-native-blurhash'
import attachmentAspectRatio from './aspectRatio'
import AttachmentAltText from './AltText' import AttachmentAltText from './AltText'
import { Platform } from 'expo-modules-core' import { Platform } from 'expo-modules-core'
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import { aspectRatio } from './dimensions'
export interface Props { export interface Props {
total: number total: number
@ -23,6 +24,8 @@ const AttachmentVideo: React.FC<Props> = ({
video, video,
gifv = false gifv = false
}) => { }) => {
const { reduceMotionEnabled } = useAccessibility()
const videoPlayer = useRef<Video>(null) const videoPlayer = useRef<Video>(null)
const [videoLoading, setVideoLoading] = useState(false) const [videoLoading, setVideoLoading] = useState(false)
const [videoLoaded, setVideoLoaded] = useState(false) const [videoLoaded, setVideoLoaded] = useState(false)
@ -46,7 +49,7 @@ const AttachmentVideo: React.FC<Props> = ({
flex: 1, flex: 1,
flexBasis: '50%', flexBasis: '50%',
padding: StyleConstants.Spacing.XS / 2, padding: StyleConstants.Spacing.XS / 2,
aspectRatio: attachmentAspectRatio({ total, index }) aspectRatio: aspectRatio({ total, index, ...video.meta?.original })
}} }}
> >
<Video <Video
@ -57,7 +60,7 @@ const AttachmentVideo: React.FC<Props> = ({
resizeMode={videoResizeMode} resizeMode={videoResizeMode}
{...(gifv {...(gifv
? { ? {
shouldPlay: true, shouldPlay: reduceMotionEnabled ? false : true,
isMuted: true, isMuted: true,
isLooping: true, isLooping: true,
source: { uri: video.url } source: { uri: video.url }
@ -70,10 +73,10 @@ const AttachmentVideo: React.FC<Props> = ({
onFullscreenUpdate={event => { onFullscreenUpdate={event => {
if (event.fullscreenUpdate === VideoFullscreenUpdate.PLAYER_DID_DISMISS) { if (event.fullscreenUpdate === VideoFullscreenUpdate.PLAYER_DID_DISMISS) {
Platform.OS === 'android' && setVideoResizeMode(ResizeMode.COVER) Platform.OS === 'android' && setVideoResizeMode(ResizeMode.COVER)
if (!gifv) { if (gifv && !reduceMotionEnabled) {
videoPlayer.current?.pauseAsync()
} else {
videoPlayer.current?.playAsync() videoPlayer.current?.playAsync()
} else {
videoPlayer.current?.pauseAsync()
} }
} }
}} }}
@ -103,7 +106,7 @@ const AttachmentVideo: React.FC<Props> = ({
video.blurhash ? ( video.blurhash ? (
<Blurhash blurhash={video.blurhash} style={{ width: '100%', height: '100%' }} /> <Blurhash blurhash={video.blurhash} style={{ width: '100%', height: '100%' }} />
) : null ) : null
) : !gifv ? ( ) : !gifv || (gifv && reduceMotionEnabled) ? (
<Button <Button
round round
overlay 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 TimelineAvatar: React.FC<Props> = ({ account }) => {
const { status, highlighted, disableOnPress } = useContext(StatusContext) const { status, highlighted, disableDetails, disableOnPress, isConversation } =
useContext(StatusContext)
const actualAccount = account || status?.account const actualAccount = account || status?.account
if (!actualAccount) return null if (!actualAccount) return null
@ -33,14 +34,22 @@ const TimelineAvatar: React.FC<Props> = ({ account }) => {
!disableOnPress && navigation.push('Tab-Shared-Account', { account: actualAccount }) !disableOnPress && navigation.push('Tab-Shared-Account', { account: actualAccount })
} }
uri={{ original: actualAccount.avatar, static: actualAccount.avatar_static }} uri={{ original: actualAccount.avatar, static: actualAccount.avatar_static }}
dimension={{ dimension={
width: StyleConstants.Avatar.M, disableDetails || isConversation
height: StyleConstants.Avatar.M ? {
}} width: StyleConstants.Avatar.XS,
height: StyleConstants.Avatar.XS
}
: {
width: StyleConstants.Avatar.M,
height: StyleConstants.Avatar.M
}
}
style={{ style={{
borderRadius: StyleConstants.Avatar.M, borderRadius: StyleConstants.Avatar.M,
overflow: 'hidden', 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 { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext, useEffect, useState } from 'react' 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 { Circle } from 'react-native-animated-spinkit'
import TimelineDefault from '../Default' import TimelineDefault from '../Default'
import StatusContext from './Context' import StatusContext from './Context'
@ -187,7 +187,10 @@ const TimelineCard: React.FC = () => {
style={{ style={{
flex: 1, flex: 1,
flexDirection: 'row', 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, marginTop: StyleConstants.Spacing.M,
borderWidth: StyleSheet.hairlineWidth, borderWidth: StyleSheet.hairlineWidth,
borderRadius: StyleConstants.Spacing.S, borderRadius: StyleConstants.Spacing.S,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,7 +16,7 @@ import { maxBy } from 'lodash'
import React, { useCallback, useContext, useMemo, useState } from 'react' import React, { useCallback, useContext, useMemo, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { Pressable, View } from 'react-native' import { Pressable, View } from 'react-native'
import { useQueryClient } from 'react-query' import { useQueryClient } from '@tanstack/react-query'
import StatusContext from './Context' import StatusContext from './Context'
const TimelinePoll: React.FC = () => { const TimelinePoll: React.FC = () => {
@ -33,7 +33,7 @@ const TimelinePoll: React.FC = () => {
const poll = status.poll const poll = status.poll
const { colors, theme } = useTheme() 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)) 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( const isSelected = useCallback(
(index: number): string => (index: number): string =>

View File

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

View File

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

View File

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

View File

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

View File

@ -22,10 +22,7 @@ const mediaSelector = async ({
indicateMaximum = false, indicateMaximum = false,
showActionSheetWithOptions showActionSheetWithOptions
}: Props): Promise<Asset[]> => { }: Props): Promise<Asset[]> => {
const _maximum = const _maximum = maximum || getInstanceConfigurationStatusMaxAttachments(store.getState()) || 4
maximum ||
getInstanceConfigurationStatusMaxAttachments(store.getState()) ||
4
const options = () => { const options = () => {
switch (mediaType) { switch (mediaType) {
@ -33,7 +30,7 @@ const mediaSelector = async ({
return [ return [
i18next.t( i18next.t(
'componentMediaSelector:options.image', 'componentMediaSelector:options.image',
indicateMaximum ? { context: 'max', max: _maximum } : undefined indicateMaximum ? { context: 'max', max: _maximum } : {}
), ),
i18next.t('common:buttons.cancel') i18next.t('common:buttons.cancel')
] ]
@ -41,7 +38,7 @@ const mediaSelector = async ({
return [ return [
i18next.t( i18next.t(
'componentMediaSelector:options.video', 'componentMediaSelector:options.video',
indicateMaximum ? { context: 'max', max: 1 } : undefined indicateMaximum ? { context: 'max', max: 1 } : {}
), ),
i18next.t('common:buttons.cancel') i18next.t('common:buttons.cancel')
] ]
@ -49,11 +46,11 @@ const mediaSelector = async ({
return [ return [
i18next.t( i18next.t(
'componentMediaSelector:options.image', 'componentMediaSelector:options.image',
indicateMaximum ? { context: 'max', max: _maximum } : undefined indicateMaximum ? { context: 'max', max: _maximum } : {}
), ),
i18next.t( i18next.t(
'componentMediaSelector:options.video', 'componentMediaSelector:options.video',
indicateMaximum ? { context: 'max', max: 1 } : undefined indicateMaximum ? { context: 'max', max: 1 } : {}
), ),
i18next.t('common:buttons.cancel') 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", "feature": "account_return_suspended",
"version": 3.3, "version": 3.3
"reference": "https://github.com/mastodon/mastodon/releases/tag/v3.3.0"
}, },
{ {
"feature": "edit_post", "feature": "edit_post",
"version": 3.5, "version": 3.5
"reference": "https://github.com/mastodon/mastodon/releases/tag/v3.5.0"
}, },
{ {
"feature": "deprecate_auth_follow", "feature": "deprecate_auth_follow",
"version": 3.5, "version": 3.5
"reference": "https://github.com/mastodon/mastodon/releases/tag/v3.5.0"
}, },
{ {
"feature": "notification_type_update", "feature": "notification_type_update",
"version": 3.5, "version": 3.5
"reference": "https://github.com/mastodon/mastodon/releases/tag/v3.5.0" },
{
"feature": "notification_type_admin_signup",
"version": 3.5
}, },
{ {
"feature": "notification_types_positive_filter", "feature": "notification_types_positive_filter",
"version": 3.5, "version": 3.5
"reference": "https://github.com/mastodon/mastodon/releases/tag/v3.5.0"
}, },
{ {
"feature": "trends_new_path", "feature": "trends_new_path",
"version": 3.5, "version": 3.5
"reference": "https://github.com/mastodon/mastodon/releases/tag/v3.5.0"
}, },
{ {
"feature": "follow_tags", "feature": "follow_tags",
"version": 4.0, "version": 4.0
"reference": "https://github.com/mastodon/mastodon/releases/tag/v4.0.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 export default queryClient

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