3
.vscode/settings.json
vendored
@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"javascript.inlayHints.functionLikeReturnTypes.enabled": false
|
|
||||||
}
|
|
BIN
demo/screenshots/Component-Instance.png
Normal file
After Width: | Height: | Size: 382 KiB |
BIN
demo/screenshots/Component-MediaSelector.png
Normal file
After Width: | Height: | Size: 311 KiB |
BIN
demo/screenshots/Logout-Confirmation.png
Normal file
After Width: | Height: | Size: 317 KiB |
BIN
demo/screenshots/Screen-AccountSelection.png
Normal file
After Width: | Height: | Size: 116 KiB |
BIN
demo/screenshots/Tab-Local.png
Normal file
After Width: | Height: | Size: 312 KiB |
BIN
demo/screenshots/Tab-Me-List-Edit.png
Normal file
After Width: | Height: | Size: 113 KiB |
BIN
demo/screenshots/Tab-Me-ListAccounts_Empty.png
Normal file
After Width: | Height: | Size: 97 KiB |
BIN
demo/screenshots/Tab-Me-ListAccounts_Error.png
Normal file
After Width: | Height: | Size: 109 KiB |
BIN
demo/screenshots/Tab-Me-List_Delete.png
Normal file
After Width: | Height: | Size: 214 KiB |
BIN
demo/screenshots/Tab-Me-List_Menu.png
Normal file
After Width: | Height: | Size: 271 KiB |
BIN
demo/screenshots/Tab-Me-Profile-Fields.png
Normal file
After Width: | Height: | Size: 177 KiB |
BIN
demo/screenshots/Tab-Me-Profile.png
Normal file
After Width: | Height: | Size: 199 KiB |
BIN
demo/screenshots/Tab-Me-Profile_Feedback.png
Normal file
After Width: | Height: | Size: 211 KiB |
BIN
demo/screenshots/Tab-Me-Push_Bottom.png
Normal file
After Width: | Height: | Size: 236 KiB |
BIN
demo/screenshots/Tab-Me-Push_MissingServerKey.png
Normal file
After Width: | Height: | Size: 113 KiB |
BIN
demo/screenshots/Tab-Me-Push_NotAvailable.png
Normal file
After Width: | Height: | Size: 103 KiB |
BIN
demo/screenshots/Tab-Me-Push_ReEnable.png
Normal file
After Width: | Height: | Size: 248 KiB |
BIN
demo/screenshots/Tab-Me-Push_Top.png
Normal file
After Width: | Height: | Size: 249 KiB |
BIN
demo/screenshots/Tab-Me-Settings-Appearance.png
Normal file
After Width: | Height: | Size: 403 KiB |
BIN
demo/screenshots/Tab-Me-Settings-DarkTheme.png
Normal file
After Width: | Height: | Size: 344 KiB |
BIN
demo/screenshots/Tab-Me-Settings-FontSize.png
Normal file
After Width: | Height: | Size: 164 KiB |
BIN
demo/screenshots/Tab-Me-Settings-OpeningLink.png
Normal file
After Width: | Height: | Size: 356 KiB |
BIN
demo/screenshots/Tab-Me-Settings.png
Normal file
After Width: | Height: | Size: 188 KiB |
BIN
demo/screenshots/Tab-Me-Switch.png
Normal file
After Width: | Height: | Size: 169 KiB |
BIN
demo/screenshots/Tab-Me.png
Normal file
After Width: | Height: | Size: 187 KiB |
BIN
demo/screenshots/Tab-Notifications-Filter.png
Normal file
After Width: | Height: | Size: 174 KiB |
BIN
demo/screenshots/Tab-Notifications.png
Normal file
After Width: | Height: | Size: 274 KiB |
BIN
demo/screenshots/Tab-Public.png
Normal file
After Width: | Height: | Size: 293 KiB |
BIN
demo/screenshots/Tab-Shared-Account.png
Normal file
After Width: | Height: | Size: 609 KiB |
BIN
demo/screenshots/Tab-Shared-AccountInLists.png
Normal file
After Width: | Height: | Size: 104 KiB |
BIN
demo/screenshots/Tab-Shared-Attachments.png
Normal file
After Width: | Height: | Size: 402 KiB |
BIN
demo/screenshots/Tab-Shared-Hashtag.png
Normal file
After Width: | Height: | Size: 339 KiB |
BIN
demo/screenshots/Tab-Shared-History.png
Normal file
After Width: | Height: | Size: 109 KiB |
BIN
demo/screenshots/Tab-Shared-Search.png
Normal file
After Width: | Height: | Size: 198 KiB |
BIN
demo/screenshots/Tab-Shared-Toot.png
Normal file
After Width: | Height: | Size: 334 KiB |
154
demo/statuses.ts
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
@ -1,3 +1,11 @@
|
|||||||
toooting愉快!此版本包括以下改进和修复:
|
toooting愉快!此版本包括以下改进和修复:
|
||||||
- 修复嘟文收藏等显示
|
- 增加 🇺🇦 Slava Ukraini
|
||||||
- 修复不能切换语言
|
- 自动识别发嘟语言
|
||||||
|
- 记住上次公共时间轴选项
|
||||||
|
- 显示编辑历史的差异
|
||||||
|
- 关注列表可隐藏转嘟和回复
|
||||||
|
- 新增管理员推送通知
|
||||||
|
- 支持嘟文右到左文字
|
||||||
|
- 测试显示对话层级
|
||||||
|
- 修复过滤整词功能
|
||||||
|
- 修复平板不能删除草稿
|
@ -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
|
||||||
|
|
||||||
|
@ -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";
|
||||||
};
|
};
|
||||||
|
2
ios/uk.lproj/InfoPlist.strings
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
"NSPhotoLibraryAddUsageDescription" = "Дозвольте tooot зберігати зображення у вашій папці фотоапарата";
|
||||||
|
"NSPhotoLibraryUsageDescription" = "Дозвольте tooot зберігати зображення у вашій папці фотоапарата";
|
39
package.json
@ -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
@ -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'
|
||||||
|
96
src/@types/mastodon.d.ts
vendored
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
2
src/@types/untyped.d.ts
vendored
@ -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 }[]
|
||||||
|
@ -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'
|
||||||
|
|
||||||
|
@ -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 }
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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())
|
||||||
}
|
}
|
||||||
|
@ -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 }
|
||||||
|
@ -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())
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -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 && (
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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']
|
||||||
|
@ -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 } }) => {
|
||||||
|
@ -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
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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'
|
||||||
|
@ -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 ? (
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
@ -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 <></>
|
||||||
}
|
}
|
||||||
|
@ -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'
|
||||||
|
|
||||||
|
@ -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 => {
|
||||||
|
@ -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 })
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
|
@ -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 ? (
|
||||||
|
@ -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
|
||||||
|
@ -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
|
|
38
src/components/Timeline/Shared/Attachment/dimensions.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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
|
||||||
})
|
})
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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'
|
||||||
|
@ -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>
|
||||||
|
@ -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 }
|
||||||
]}
|
]}
|
||||||
|
@ -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 =>
|
||||||
|
@ -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}
|
||||||
|
@ -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
|
||||||
},
|
},
|
||||||
|
@ -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 = ({
|
||||||
|
@ -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 = ({
|
||||||
|
@ -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')
|
||||||
]
|
]
|
||||||
|
10
src/helpers/detectLanguage.ts
Normal 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
|
@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
9
src/helpers/permissions.ts
Normal 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
|
@ -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
|
||||||
|