Merge branch 'main' into candidate
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',
|
||||
uri: 'https://example.com',
|
||||
created_at: new Date().toISOString(),
|
||||
sensitive: false,
|
||||
visibility: 'public',
|
||||
@ -13,7 +14,6 @@ const demoStatuses = [
|
||||
bookmarked: false,
|
||||
content:
|
||||
'<p>Would you like to try out this simple, beautiful and open-source mobile app for Mastodon? 😊</p>',
|
||||
reblog: null,
|
||||
application: {
|
||||
name: 'tooot',
|
||||
website: 'https://tooot.app'
|
||||
@ -23,19 +23,31 @@ const demoStatuses = [
|
||||
username: 'tooot📱',
|
||||
acct: 'tooot@xmflsct.com',
|
||||
display_name: 'tooot📱',
|
||||
avatar_static:
|
||||
'https://avatars.githubusercontent.com/u/77554750?s=200&v=4'
|
||||
avatar: 'https://avatars.githubusercontent.com/u/77554750?s=200&v=4',
|
||||
avatar_static: 'https://avatars.githubusercontent.com/u/77554750?s=200&v=4',
|
||||
url: '',
|
||||
header: '',
|
||||
header_static: '',
|
||||
locked: false,
|
||||
discoverable: false,
|
||||
created_at: new Date().toISOString(),
|
||||
last_status_at: new Date().toISOString(),
|
||||
statuses_count: 1,
|
||||
followers_count: 1,
|
||||
following_count: 1,
|
||||
fields: [],
|
||||
bot: false
|
||||
},
|
||||
media_attachments: [],
|
||||
poll: {
|
||||
id: '1',
|
||||
expires_at: new Date().setDate(new Date().getDate() + 5),
|
||||
expires_at: new Date().setDate(new Date().getDate() + 5).toString(),
|
||||
expired: false,
|
||||
multiple: false,
|
||||
votes_count: 10,
|
||||
voters_count: null,
|
||||
voters_count: 2,
|
||||
voted: false,
|
||||
own_votes: null,
|
||||
own_votes: undefined,
|
||||
options: [
|
||||
{
|
||||
title: 'I would love to!',
|
||||
@ -48,11 +60,15 @@ const demoStatuses = [
|
||||
],
|
||||
emojis: []
|
||||
},
|
||||
mentions: []
|
||||
mentions: [],
|
||||
tags: [],
|
||||
emojis: [],
|
||||
pinned: false
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
created_at: new Date().setMinutes(new Date().getMinutes() - 2),
|
||||
uri: 'https://example.com',
|
||||
created_at: new Date().setMinutes(new Date().getMinutes() - 2).toString(),
|
||||
sensitive: false,
|
||||
spoiler_text: '',
|
||||
visibility: 'public',
|
||||
@ -65,18 +81,26 @@ const demoStatuses = [
|
||||
bookmarked: false,
|
||||
content:
|
||||
'<p>Mastodon is a free and open-source self-hosted social networking service. It allows anyone to host their own server node in the network, and its various separately operated user bases are federated across many different servers. These nodes are referred to as "instances" by Mastodon users.</p>',
|
||||
reblog: null,
|
||||
application: {
|
||||
name: 'Web',
|
||||
website: null
|
||||
},
|
||||
application: { name: 'Web' },
|
||||
account: {
|
||||
id: '1000',
|
||||
username: 'Mastodon',
|
||||
acct: 'mastodon',
|
||||
display_name: 'Mastodon',
|
||||
avatar_static:
|
||||
'https://mastodon.social/apple-touch-icon.png'
|
||||
avatar: 'https://mastodon.social/apple-touch-icon.png',
|
||||
avatar_static: 'https://mastodon.social/apple-touch-icon.png',
|
||||
url: '',
|
||||
header: '',
|
||||
header_static: '',
|
||||
locked: false,
|
||||
discoverable: false,
|
||||
created_at: new Date().toISOString(),
|
||||
last_status_at: new Date().toISOString(),
|
||||
statuses_count: 1,
|
||||
followers_count: 1,
|
||||
following_count: 1,
|
||||
fields: [],
|
||||
bot: false
|
||||
},
|
||||
media_attachments: [],
|
||||
card: {
|
||||
@ -85,18 +109,31 @@ const demoStatuses = [
|
||||
description:
|
||||
'Mastodon is an open source decentralized social network - by the people for the people. Join the federation and take back control of your social media!',
|
||||
type: 'link',
|
||||
image:
|
||||
'https://mastodon.social/apple-touch-icon.png'
|
||||
image: 'https://mastodon.social/apple-touch-icon.png',
|
||||
author_name: '',
|
||||
author_url: '',
|
||||
provider_name: '',
|
||||
provider_url: '',
|
||||
html: '<p></p>',
|
||||
width: 100,
|
||||
height: 100,
|
||||
embed_url: 'https://example.com',
|
||||
blurhash: ''
|
||||
},
|
||||
mentions: []
|
||||
mentions: [],
|
||||
tags: [],
|
||||
emojis: [],
|
||||
pinned: false
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
created_at: '2021-01-24T09:50:00.901Z',
|
||||
uri: '',
|
||||
created_at: new Date().setHours(new Date().getHours() - 1).toString(),
|
||||
sensitive: false,
|
||||
spoiler_text: '',
|
||||
visibility: 'public',
|
||||
replies_count: 2,
|
||||
reblogs_count: null,
|
||||
reblogs_count: 1,
|
||||
favourites_count: 3,
|
||||
favourited: false,
|
||||
reblogged: false,
|
||||
@ -104,24 +141,38 @@ const demoStatuses = [
|
||||
bookmarked: true,
|
||||
content:
|
||||
'<p>These servers are connected as a federated social network, allowing users from different servers to interact with each other seamlessly. Once a Mastodon server knows another Mastodon server, it "federates" with the other Mastodon server. Mastodon is a part of the wider Fediverse, allowing its users to also interact with users on different open platforms that support the same protocol, such as PeerTube and Friendica.</p>',
|
||||
reblog: null,
|
||||
application: {
|
||||
name: 'Web',
|
||||
website: null
|
||||
},
|
||||
application: { name: 'Web' },
|
||||
account: {
|
||||
id: '1001',
|
||||
username: 'Fediverse',
|
||||
acct: 'fediverse',
|
||||
display_name: 'Fediverse',
|
||||
avatar:
|
||||
'https://e7.pngegg.com/pngimages/667/514/png-clipart-mastodon-fediverse-social-media-free-software-logo-social-media-blue-text.png',
|
||||
avatar_static:
|
||||
'https://e7.pngegg.com/pngimages/667/514/png-clipart-mastodon-fediverse-social-media-free-software-logo-social-media-blue-text.png'
|
||||
'https://e7.pngegg.com/pngimages/667/514/png-clipart-mastodon-fediverse-social-media-free-software-logo-social-media-blue-text.png',
|
||||
url: '',
|
||||
header: '',
|
||||
header_static: '',
|
||||
locked: false,
|
||||
discoverable: false,
|
||||
created_at: new Date().toISOString(),
|
||||
last_status_at: new Date().toISOString(),
|
||||
statuses_count: 1,
|
||||
followers_count: 1,
|
||||
following_count: 1,
|
||||
fields: [],
|
||||
bot: false
|
||||
},
|
||||
media_attachments: [],
|
||||
mentions: []
|
||||
mentions: [],
|
||||
tags: [],
|
||||
emojis: [],
|
||||
pinned: false
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
uri: 'https://example.com',
|
||||
created_at: '2021-01-24T08:50:00.901Z',
|
||||
sensitive: false,
|
||||
visibility: 'public',
|
||||
@ -134,7 +185,6 @@ const demoStatuses = [
|
||||
bookmarked: false,
|
||||
content:
|
||||
'<p>tooot is an open source, simple mobile client for Mastodon. Focusing on your connections while being able to explore the Fediverse.</p>',
|
||||
reblog: null,
|
||||
application: {
|
||||
name: 'tooot',
|
||||
website: 'https://tooot.app'
|
||||
@ -144,14 +194,30 @@ const demoStatuses = [
|
||||
username: 'tooot📱',
|
||||
acct: 'tooot@xmflsct.com',
|
||||
display_name: 'tooot📱',
|
||||
avatar_static:
|
||||
'https://avatars.githubusercontent.com/u/77554750?s=200&v=4'
|
||||
avatar: 'https://avatars.githubusercontent.com/u/77554750?s=200&v=4',
|
||||
avatar_static: 'https://avatars.githubusercontent.com/u/77554750?s=200&v=4',
|
||||
url: '',
|
||||
header: '',
|
||||
header_static: '',
|
||||
locked: false,
|
||||
discoverable: false,
|
||||
created_at: new Date().toISOString(),
|
||||
last_status_at: new Date().toISOString(),
|
||||
statuses_count: 1,
|
||||
followers_count: 1,
|
||||
following_count: 1,
|
||||
fields: [],
|
||||
bot: false
|
||||
},
|
||||
media_attachments: [],
|
||||
mentions: []
|
||||
mentions: [],
|
||||
tags: [],
|
||||
emojis: [],
|
||||
pinned: false
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
uri: 'https://example.com',
|
||||
created_at: '2021-01-24T07:50:00.901Z',
|
||||
sensitive: false,
|
||||
visibility: 'public',
|
||||
@ -164,7 +230,6 @@ const demoStatuses = [
|
||||
bookmarked: false,
|
||||
content:
|
||||
'<p>- tooot supports multiple accounts<br />- tooot supports browsing external instance<br />- tooot aims to support multiple languages</p>',
|
||||
reblog: null,
|
||||
application: {
|
||||
name: 'tooot',
|
||||
website: 'https://tooot.app'
|
||||
@ -174,12 +239,27 @@ const demoStatuses = [
|
||||
username: 'tooot📱',
|
||||
acct: 'tooot@xmflsct.com',
|
||||
display_name: 'tooot📱',
|
||||
avatar_static:
|
||||
'https://avatars.githubusercontent.com/u/77554750?s=200&v=4'
|
||||
avatar: 'https://avatars.githubusercontent.com/u/77554750?s=200&v=4',
|
||||
avatar_static: 'https://avatars.githubusercontent.com/u/77554750?s=200&v=4',
|
||||
url: '',
|
||||
header: '',
|
||||
header_static: '',
|
||||
locked: false,
|
||||
discoverable: false,
|
||||
created_at: new Date().toISOString(),
|
||||
last_status_at: new Date().toISOString(),
|
||||
statuses_count: 1,
|
||||
followers_count: 1,
|
||||
following_count: 1,
|
||||
fields: [],
|
||||
bot: false
|
||||
},
|
||||
media_attachments: [],
|
||||
mentions: []
|
||||
mentions: [],
|
||||
tags: [],
|
||||
emojis: [],
|
||||
pinned: false
|
||||
}
|
||||
]
|
||||
|
||||
export default demoStatuses
|
||||
export default demoStatus
|
||||
|
59
src/@types/mastodon.d.ts
vendored
@ -333,24 +333,34 @@ declare namespace Mastodon {
|
||||
url: string
|
||||
}
|
||||
|
||||
type Notification = {
|
||||
// Base
|
||||
id: string
|
||||
type:
|
||||
| 'follow'
|
||||
| 'follow_request'
|
||||
| 'mention'
|
||||
| 'reblog'
|
||||
| 'favourite'
|
||||
| 'poll'
|
||||
| 'status'
|
||||
| 'update'
|
||||
created_at: string
|
||||
account: Account
|
||||
|
||||
// Others
|
||||
status?: Status
|
||||
}
|
||||
type Notification =
|
||||
| {
|
||||
// Base
|
||||
id: string
|
||||
type: 'favourite' | 'mention' | 'poll' | 'reblog' | 'status' | 'update'
|
||||
created_at: string
|
||||
account: Account
|
||||
status: Status
|
||||
report: undefined
|
||||
}
|
||||
| {
|
||||
// Base
|
||||
id: string
|
||||
type: 'follow' | 'follow_request' | 'admin.sign_up'
|
||||
created_at: string
|
||||
account: Account
|
||||
status: undefined
|
||||
report: undefined
|
||||
}
|
||||
| {
|
||||
// Base
|
||||
id: string
|
||||
type: 'admin.report'
|
||||
created_at: string
|
||||
account: Account
|
||||
status: undefined
|
||||
report: Report
|
||||
}
|
||||
|
||||
type Poll = {
|
||||
// Base
|
||||
@ -406,6 +416,19 @@ declare namespace Mastodon {
|
||||
note: string
|
||||
}
|
||||
|
||||
type Report = {
|
||||
id: string
|
||||
action_taken: boolean
|
||||
action_taken_at?: string
|
||||
category: 'spam' | 'violation' | 'other'
|
||||
comment: string
|
||||
forwarded: boolean
|
||||
created_at: string
|
||||
status_ids?: string[]
|
||||
rule_ids?: string[]
|
||||
target_account: Account
|
||||
}
|
||||
|
||||
type Results = {
|
||||
accounts?: Account[]
|
||||
statuses?: Status[]
|
||||
|
@ -86,9 +86,10 @@ const MenuRow: React.FC<Props> = ({
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flex: 3,
|
||||
flexGrow: 3,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
alignItems: 'center',
|
||||
marginRight: StyleConstants.Spacing.M
|
||||
}}
|
||||
>
|
||||
{iconFront && (
|
||||
|
@ -28,18 +28,18 @@ import TimelineHeaderAndroid from './Shared/HeaderAndroid'
|
||||
export interface Props {
|
||||
notification: Mastodon.Notification
|
||||
queryKey: QueryKeyTimeline
|
||||
highlighted?: boolean
|
||||
}
|
||||
|
||||
const TimelineNotifications: React.FC<Props> = ({
|
||||
notification,
|
||||
queryKey,
|
||||
highlighted = false
|
||||
}) => {
|
||||
const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
|
||||
const instanceAccount = useSelector(getInstanceAccount, () => true)
|
||||
|
||||
const status = notification.status?.reblog ? notification.status.reblog : notification.status
|
||||
const account = notification.status ? notification.status.account : notification.account
|
||||
const account =
|
||||
notification.type === 'admin.report'
|
||||
? notification.report.target_account
|
||||
: notification.status
|
||||
? notification.status.account
|
||||
: notification.account
|
||||
const ownAccount = notification.account?.id === instanceAccount?.id
|
||||
const [spoilerExpanded, setSpoilerExpanded] = useState(
|
||||
instanceAccount.preferences['reading:expand:spoilers'] || false
|
||||
@ -91,7 +91,8 @@ const TimelineNotifications: React.FC<Props> = ({
|
||||
notification.type === 'follow' ||
|
||||
notification.type === 'follow_request' ||
|
||||
notification.type === 'mention' ||
|
||||
notification.type === 'status'
|
||||
notification.type === 'status' ||
|
||||
notification.type === 'admin.sign_up'
|
||||
? 1
|
||||
: 0.5
|
||||
}}
|
||||
@ -102,12 +103,7 @@ const TimelineNotifications: React.FC<Props> = ({
|
||||
</View>
|
||||
|
||||
{notification.status ? (
|
||||
<View
|
||||
style={{
|
||||
paddingTop: highlighted ? StyleConstants.Spacing.S : 0,
|
||||
paddingLeft: highlighted ? 0 : StyleConstants.Avatar.M + StyleConstants.Spacing.S
|
||||
}}
|
||||
>
|
||||
<View style={{ paddingLeft: StyleConstants.Avatar.M + StyleConstants.Spacing.S }}>
|
||||
<TimelineContent
|
||||
notificationOwnToot={['favourite', 'reblog'].includes(notification.type)}
|
||||
setSpoilerExpanded={setSpoilerExpanded}
|
||||
@ -141,8 +137,7 @@ const TimelineNotifications: React.FC<Props> = ({
|
||||
status,
|
||||
ownAccount,
|
||||
spoilerHidden,
|
||||
copiableContent,
|
||||
highlighted
|
||||
copiableContent
|
||||
}}
|
||||
>
|
||||
<ContextMenu.Root>
|
||||
|
@ -28,7 +28,12 @@ const TimelineActioned: React.FC<Props> = ({ action, isNotification, ...rest })
|
||||
const iconColor = colors.primaryDefault
|
||||
|
||||
const content = (content: string) => (
|
||||
<ParseEmojis content={content} emojis={account.emojis} size='S' />
|
||||
<ParseEmojis
|
||||
content={content}
|
||||
emojis={account.emojis}
|
||||
size='S'
|
||||
style={{ color: action === 'admin.report' ? colors.red : colors.primaryDefault }}
|
||||
/>
|
||||
)
|
||||
|
||||
const onPress = () => navigation.push('Tab-Shared-Account', { account })
|
||||
@ -145,6 +150,30 @@ const TimelineActioned: React.FC<Props> = ({ action, isNotification, ...rest })
|
||||
{content(t('shared.actioned.update'))}
|
||||
</>
|
||||
)
|
||||
case 'admin.sign_up':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='Users'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
{content(t('shared.actioned.admin.sign_up', { name: `@${account.acct}` }))}
|
||||
</>
|
||||
)
|
||||
case 'admin.report':
|
||||
return (
|
||||
<>
|
||||
<Icon
|
||||
name='AlertOctagon'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={colors.red}
|
||||
style={styles.icon}
|
||||
/>
|
||||
{content(t('shared.actioned.admin.report', { name: `@${account.acct}` }))}
|
||||
</>
|
||||
)
|
||||
default:
|
||||
return <></>
|
||||
}
|
||||
|
@ -45,6 +45,21 @@ const TimelineAttachment = () => {
|
||||
}
|
||||
const [sensitiveShown, setSensitiveShown] = useState(defaultSensitive())
|
||||
|
||||
// const testHorizontal: Mastodon.Attachment[] = Array(2).fill({
|
||||
// id: Math.random().toString(),
|
||||
// type: 'image',
|
||||
// url: 'https://images.unsplash.com/photo-1670870764013-f0e36aa376b0?w=1000',
|
||||
// preview_url: 'https://images.unsplash.com/photo-1543968996-ee822b8176ba?w=300',
|
||||
// meta: { original: { width: 1000, height: 625 } }
|
||||
// })
|
||||
// const testVertical: Mastodon.Attachment[] = Array(7).fill({
|
||||
// id: Math.random().toString(),
|
||||
// type: 'image',
|
||||
// url: 'https://images.unsplash.com/photo-1670842587871-326b95acbc8c?w=1000',
|
||||
// preview_url: 'https://images.unsplash.com/photo-1670833288990-64b2f4ef7290?w=300',
|
||||
// meta: { original: { width: 987, height: 1480 } }
|
||||
// })
|
||||
|
||||
// @ts-ignore
|
||||
const imageUrls: RootStackParamList['Screen-ImagesViewer']['imageUrls'] = status.media_attachments
|
||||
.map(attachment => {
|
||||
|
@ -8,7 +8,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { AppState, AppStateStatus, StyleSheet, View } from 'react-native'
|
||||
import { Blurhash } from 'react-native-blurhash'
|
||||
import AttachmentAltText from './AltText'
|
||||
import attachmentAspectRatio from './aspectRatio'
|
||||
import { aspectRatio } from './dimensions'
|
||||
|
||||
export interface Props {
|
||||
total: number
|
||||
@ -64,7 +64,7 @@ const AttachmentAudio: React.FC<Props> = ({ total, index, sensitiveShown, audio
|
||||
styles.base,
|
||||
{
|
||||
backgroundColor: colors.disabled,
|
||||
aspectRatio: attachmentAspectRatio({ total, index })
|
||||
aspectRatio: aspectRatio({ total, index, ...audio.meta?.original })
|
||||
}
|
||||
]}
|
||||
>
|
||||
|
@ -1,9 +1,10 @@
|
||||
import GracefullyImage from '@components/GracefullyImage'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React from 'react'
|
||||
import { View } from 'react-native'
|
||||
import AttachmentAltText from './AltText'
|
||||
import attachmentAspectRatio from './aspectRatio'
|
||||
import { aspectRatio } from './dimensions'
|
||||
|
||||
export interface Props {
|
||||
total: number
|
||||
@ -20,6 +21,8 @@ const AttachmentImage = ({
|
||||
image,
|
||||
navigateToImagesViewer
|
||||
}: Props) => {
|
||||
const { colors } = useTheme()
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
@ -28,21 +31,16 @@ const AttachmentImage = ({
|
||||
padding: StyleConstants.Spacing.XS / 2
|
||||
}}
|
||||
>
|
||||
<GracefullyImage
|
||||
accessibilityLabel={image.description}
|
||||
hidden={sensitiveShown}
|
||||
uri={{ original: image.preview_url, remote: image.remote_url }}
|
||||
blurhash={image.blurhash}
|
||||
onPress={() => navigateToImagesViewer(image.id)}
|
||||
style={{
|
||||
aspectRatio:
|
||||
total > 1 || !image.meta?.original?.width || !image.meta?.original?.height
|
||||
? attachmentAspectRatio({ total, index })
|
||||
: image.meta.original.height / image.meta.original.width > 1
|
||||
? 1
|
||||
: image.meta.original.width / image.meta.original.height
|
||||
}}
|
||||
/>
|
||||
<View style={{ flex: 1, backgroundColor: colors.shimmerDefault }}>
|
||||
<GracefullyImage
|
||||
accessibilityLabel={image.description}
|
||||
hidden={sensitiveShown}
|
||||
uri={{ original: image.preview_url, remote: image.remote_url }}
|
||||
blurhash={image.blurhash}
|
||||
onPress={() => navigateToImagesViewer(image.id)}
|
||||
style={{ aspectRatio: aspectRatio({ total, index, ...image.meta?.original }) }}
|
||||
/>
|
||||
</View>
|
||||
<AttachmentAltText sensitiveShown={sensitiveShown} text={image.description} />
|
||||
</View>
|
||||
)
|
||||
|
@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { View } from 'react-native'
|
||||
import { Blurhash } from 'react-native-blurhash'
|
||||
import AttachmentAltText from './AltText'
|
||||
import attachmentAspectRatio from './aspectRatio'
|
||||
import { aspectRatio } from './dimensions'
|
||||
|
||||
export interface Props {
|
||||
total: number
|
||||
@ -29,7 +29,7 @@ const AttachmentUnsupported: React.FC<Props> = ({ total, index, sensitiveShown,
|
||||
padding: StyleConstants.Spacing.XS / 2,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
aspectRatio: attachmentAspectRatio({ total, index })
|
||||
aspectRatio: aspectRatio({ total, index, ...attachment.meta?.original })
|
||||
}}
|
||||
>
|
||||
{attachment.blurhash ? (
|
||||
|
@ -4,10 +4,10 @@ import { ResizeMode, Video, VideoFullscreenUpdate } from 'expo-av'
|
||||
import React, { useRef, useState } from 'react'
|
||||
import { Pressable, View } from 'react-native'
|
||||
import { Blurhash } from 'react-native-blurhash'
|
||||
import attachmentAspectRatio from './aspectRatio'
|
||||
import AttachmentAltText from './AltText'
|
||||
import { Platform } from 'expo-modules-core'
|
||||
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
||||
import { aspectRatio } from './dimensions'
|
||||
|
||||
export interface Props {
|
||||
total: number
|
||||
@ -49,7 +49,7 @@ const AttachmentVideo: React.FC<Props> = ({
|
||||
flex: 1,
|
||||
flexBasis: '50%',
|
||||
padding: StyleConstants.Spacing.XS / 2,
|
||||
aspectRatio: attachmentAspectRatio({ total, index })
|
||||
aspectRatio: aspectRatio({ total, index, ...video.meta?.original })
|
||||
}}
|
||||
>
|
||||
<Video
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,13 +1,19 @@
|
||||
import Button from '@components/Button'
|
||||
import menuAccount from '@components/contextMenu/account'
|
||||
import menuInstance from '@components/contextMenu/instance'
|
||||
import menuShare from '@components/contextMenu/share'
|
||||
import menuStatus from '@components/contextMenu/status'
|
||||
import Icon from '@components/Icon'
|
||||
import { RelationshipIncoming, RelationshipOutgoing } from '@components/Relationship'
|
||||
import browserPackage from '@helpers/browserPackage'
|
||||
import { getInstanceUrl } from '@utils/slices/instancesSlice'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import * as WebBrowser from 'expo-web-browser'
|
||||
import React, { useContext, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Platform, Pressable, View } from 'react-native'
|
||||
import { useSelector } from 'react-redux'
|
||||
import * as DropdownMenu from 'zeego/dropdown-menu'
|
||||
import StatusContext from './Context'
|
||||
import HeaderSharedAccount from './HeaderShared/Account'
|
||||
@ -21,6 +27,7 @@ export type Props = {
|
||||
}
|
||||
|
||||
const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
const { queryKey, status } = useContext(StatusContext)
|
||||
|
||||
const { colors } = useTheme()
|
||||
@ -40,12 +47,32 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
|
||||
const mStatus = menuStatus({ status, queryKey })
|
||||
const mInstance = menuInstance({ status, queryKey })
|
||||
|
||||
const url = useSelector(getInstanceUrl)
|
||||
|
||||
const actions = () => {
|
||||
switch (notification.type) {
|
||||
case 'follow':
|
||||
return <RelationshipOutgoing id={notification.account.id} />
|
||||
case 'follow_request':
|
||||
return <RelationshipIncoming id={notification.account.id} />
|
||||
case 'admin.report':
|
||||
return (
|
||||
<Button
|
||||
type='text'
|
||||
content={t('shared.actions.openReport')}
|
||||
onPress={async () =>
|
||||
WebBrowser.openAuthSessionAsync(
|
||||
`https://${url}/admin/reports/${notification.report.id}`,
|
||||
'tooot://tooot',
|
||||
{
|
||||
browserPackage: await browserPackage(),
|
||||
dismissButtonStyle: 'done',
|
||||
readerMode: false
|
||||
}
|
||||
)
|
||||
}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
if (status) {
|
||||
return (
|
||||
@ -118,12 +145,25 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
|
||||
<View style={{ flex: 1, flexDirection: 'row' }}>
|
||||
<View
|
||||
style={{
|
||||
flex: notification.type === 'follow' || notification.type === 'follow_request' ? 1 : 4
|
||||
flex:
|
||||
notification.type === 'follow' ||
|
||||
notification.type === 'follow_request' ||
|
||||
notification.type === 'admin.report'
|
||||
? 1
|
||||
: 4
|
||||
}}
|
||||
>
|
||||
<HeaderSharedAccount
|
||||
account={notification.status ? notification.status.account : notification.account}
|
||||
{...((notification.type === 'follow' || notification.type === 'follow_request') && {
|
||||
account={
|
||||
notification.type === 'admin.report'
|
||||
? notification.report.target_account
|
||||
: notification.status
|
||||
? notification.status.account
|
||||
: notification.account
|
||||
}
|
||||
{...((notification.type === 'follow' ||
|
||||
notification.type === 'follow_request' ||
|
||||
notification.type === 'admin.report') && {
|
||||
withoutName: true
|
||||
})}
|
||||
/>
|
||||
@ -151,7 +191,9 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
|
||||
<View
|
||||
style={[
|
||||
{ marginLeft: StyleConstants.Spacing.M },
|
||||
notification.type === 'follow' || notification.type === 'follow_request'
|
||||
notification.type === 'follow' ||
|
||||
notification.type === 'follow_request' ||
|
||||
notification.type === 'admin.report'
|
||||
? { flexShrink: 1 }
|
||||
: { flex: 1 }
|
||||
]}
|
||||
|
@ -30,18 +30,24 @@ const matchAccount = (
|
||||
// https://social.xmflsct.com/web/accounts/14195 <- default
|
||||
// https://social.xmflsct.com/web/@tooot <- pretty ! cannot be searched on the same instance
|
||||
// https://social.xmflsct.com/@tooot <- pretty
|
||||
const matcherAccount = new RegExp(/(https?:\/\/)?([^\/]+)(\/web|\/web\/accounts)?\/([0-9]+|@.+)/)
|
||||
const matcherAccount = new RegExp(
|
||||
/(https?:\/\/)?([^\/]+)(\/web\/accounts\/([0-9]+)|\/web\/(@.+)|\/(@.+))/
|
||||
)
|
||||
|
||||
const matched = url.match(matcherAccount)
|
||||
if (matched) {
|
||||
const hostname = matched[2]
|
||||
const style = matched[4].startsWith('@') ? 'pretty' : 'default'
|
||||
const account = matched[4]
|
||||
const account = matched.filter(i => i).reverse()?.[0]
|
||||
if (account) {
|
||||
const style = account.startsWith('@') ? 'pretty' : 'default'
|
||||
|
||||
const instanceUrl = getInstanceUrl(store.getState())
|
||||
return style === 'default'
|
||||
? { id: account, style, sameInstance: hostname === instanceUrl }
|
||||
: { username: account, style, sameInstance: hostname === instanceUrl }
|
||||
const instanceUrl = getInstanceUrl(store.getState())
|
||||
return style === 'default'
|
||||
? { id: account, style, sameInstance: hostname === instanceUrl }
|
||||
: { username: account, style, sameInstance: hostname === instanceUrl }
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"server": {
|
||||
"textInput": {
|
||||
"placeholder": "Instance' domain"
|
||||
"placeholder": "Instance's domain"
|
||||
},
|
||||
"button": "Login",
|
||||
"information": {
|
||||
|
@ -30,7 +30,9 @@
|
||||
"default": "{{name}} boosted",
|
||||
"notification": "{{name}} boosted your toot"
|
||||
},
|
||||
"update": "Reblog has been edited"
|
||||
"update": "Reblog has been edited",
|
||||
"admin.sign_up": "{{name}} joined the instance",
|
||||
"admin.report": "{{name}} reported:"
|
||||
},
|
||||
"actions": {
|
||||
"reply": {
|
||||
@ -52,7 +54,8 @@
|
||||
"bookmarked": {
|
||||
"accessibilityLabel": "Add this toot to bookmarks",
|
||||
"function": "Bookmark toot"
|
||||
}
|
||||
},
|
||||
"openReport": "Open report"
|
||||
},
|
||||
"actionsUsers": {
|
||||
"reblogged_by": {
|
||||
|
@ -1,13 +1,11 @@
|
||||
{
|
||||
"heading": {
|
||||
"left": {
|
||||
"button": "Cancel",
|
||||
"alert": {
|
||||
"title": "Cancel editing?",
|
||||
"buttons": {
|
||||
"save": "Save draft",
|
||||
"delete": "Delete draft",
|
||||
"cancel": "Cancel"
|
||||
"delete": "Delete draft"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -4,7 +4,6 @@
|
||||
"name": "Following"
|
||||
},
|
||||
"public": {
|
||||
"name": "",
|
||||
"segments": {
|
||||
"federated": "Federated",
|
||||
"local": "Local",
|
||||
@ -13,9 +12,6 @@
|
||||
},
|
||||
"notifications": {
|
||||
"name": "Notifications"
|
||||
},
|
||||
"me": {
|
||||
"name": "About me"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
@ -348,7 +344,7 @@
|
||||
"notInLists": "Other lists"
|
||||
},
|
||||
"attachments": {
|
||||
"name": "<0 /><1>\"s media</1>"
|
||||
"name": "<0 /><1>'s media</1>"
|
||||
},
|
||||
"hashtag": {
|
||||
"follow": "Follow",
|
||||
|
@ -206,7 +206,7 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
|
||||
() => (
|
||||
<HeaderLeft
|
||||
type='text'
|
||||
content={t('heading.left.button')}
|
||||
content={t('common:buttons.cancel')}
|
||||
onPress={() => {
|
||||
if (!composeState.dirty) {
|
||||
navigation.goBack()
|
||||
@ -229,7 +229,7 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
|
||||
}
|
||||
},
|
||||
{
|
||||
text: t('heading.left.alert.buttons.cancel'),
|
||||
text: t('common:buttons.cancel'),
|
||||
style: 'cancel'
|
||||
}
|
||||
])
|
||||
@ -342,9 +342,7 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
|
||||
)
|
||||
|
||||
const headerContent = useMemo(() => {
|
||||
return `${totalTextCount} / ${maxTootChars}${
|
||||
__DEV__ ? ` Dirty: ${composeState.dirty.toString()}` : ''
|
||||
}`
|
||||
return `${totalTextCount} / ${maxTootChars}`
|
||||
}, [totalTextCount, maxTootChars, composeState.dirty])
|
||||
|
||||
const inputProps: EmojisState['inputProps'] = [
|
||||
|
@ -112,6 +112,17 @@ const ComposeEditAttachmentImage: React.FC<Props> = ({ index }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<CustomText
|
||||
fontStyle='M'
|
||||
style={{
|
||||
color: colors.primaryDefault,
|
||||
padding: StyleConstants.Spacing.Global.PagePadding,
|
||||
paddingTop: 0
|
||||
}}
|
||||
fontWeight='Bold'
|
||||
>
|
||||
{t('content.editAttachment.content.imageFocus')}
|
||||
</CustomText>
|
||||
<View style={{ overflow: 'hidden', flex: 1, alignItems: 'center' }}>
|
||||
<Image
|
||||
style={{
|
||||
@ -155,17 +166,6 @@ const ComposeEditAttachmentImage: React.FC<Props> = ({ index }) => {
|
||||
/>
|
||||
</GestureDetector>
|
||||
</View>
|
||||
{screenReaderEnabled ? null : (
|
||||
<CustomText
|
||||
fontStyle='M'
|
||||
style={{
|
||||
padding: StyleConstants.Spacing.Global.PagePadding,
|
||||
color: colors.primaryDefault
|
||||
}}
|
||||
>
|
||||
{t('content.editAttachment.content.imageFocus')}
|
||||
</CustomText>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import CustomText from '@components/Text'
|
||||
import AttachmentVideo from '@components/Timeline/Shared/Attachment/Video'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useContext, useRef } from 'react'
|
||||
import React, { useContext } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ScrollView, StyleSheet, TextInput, View } from 'react-native'
|
||||
import ComposeContext from '../utils/createContext'
|
||||
@ -34,10 +34,10 @@ const ComposeEditAttachmentRoot: React.FC<Props> = ({ index }) => {
|
||||
video={
|
||||
video.local
|
||||
? ({
|
||||
url: video.local.uri,
|
||||
preview_url: video.local.thumbnail,
|
||||
blurhash: video.remote?.blurhash
|
||||
} as Mastodon.AttachmentVideo)
|
||||
url: video.local.uri,
|
||||
preview_url: video.local.thumbnail,
|
||||
blurhash: video.remote?.blurhash
|
||||
} as Mastodon.AttachmentVideo)
|
||||
: (video.remote as Mastodon.AttachmentVideo)
|
||||
}
|
||||
/>
|
||||
@ -47,45 +47,36 @@ const ComposeEditAttachmentRoot: React.FC<Props> = ({ index }) => {
|
||||
return null
|
||||
}
|
||||
|
||||
const scrollViewRef = useRef<ScrollView>(null)
|
||||
|
||||
return (
|
||||
<ScrollView ref={scrollViewRef}>
|
||||
{mediaDisplay()}
|
||||
<View style={{ padding: StyleConstants.Spacing.Global.PagePadding }}>
|
||||
<CustomText
|
||||
fontStyle='M'
|
||||
style={{ color: colors.primaryDefault }}
|
||||
fontWeight='Bold'
|
||||
>
|
||||
<ScrollView>
|
||||
<View style={{ padding: StyleConstants.Spacing.Global.PagePadding, paddingBottom: 0 }}>
|
||||
<CustomText fontStyle='M' style={{ color: colors.primaryDefault }} fontWeight='Bold'>
|
||||
{t('content.editAttachment.content.altText.heading')}
|
||||
</CustomText>
|
||||
<TextInput
|
||||
style={{
|
||||
height: 200,
|
||||
height: StyleConstants.Font.Size.M * 11 + StyleConstants.Spacing.Global.PagePadding * 2,
|
||||
...StyleConstants.FontStyle.M,
|
||||
marginTop: StyleConstants.Spacing.M,
|
||||
marginBottom: StyleConstants.Spacing.S,
|
||||
padding: StyleConstants.Spacing.Global.PagePadding,
|
||||
paddingTop: StyleConstants.Spacing.S * 1.5,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: colors.border,
|
||||
color: colors.primaryDefault
|
||||
}}
|
||||
onFocus={() => scrollViewRef.current?.scrollToEnd()}
|
||||
maxLength={1500}
|
||||
multiline
|
||||
onChangeText={(e) =>
|
||||
onChangeText={e =>
|
||||
composeDispatch({
|
||||
type: 'attachment/edit',
|
||||
payload: {
|
||||
...theAttachment,
|
||||
description: e
|
||||
}
|
||||
})}
|
||||
})
|
||||
}
|
||||
placeholder={t('content.editAttachment.content.altText.placeholder')}
|
||||
placeholderTextColor={colors.secondary}
|
||||
scrollEnabled
|
||||
value={theAttachment.description}
|
||||
keyboardAppearance={mode}
|
||||
/>
|
||||
@ -101,6 +92,7 @@ const ComposeEditAttachmentRoot: React.FC<Props> = ({ index }) => {
|
||||
{theAttachment.description?.length || 0} / 1500
|
||||
</CustomText>
|
||||
</View>
|
||||
{mediaDisplay()}
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
|
@ -180,7 +180,7 @@ const ScreenImagesViewer = ({
|
||||
options: [
|
||||
t('content.options.save'),
|
||||
t('content.options.share'),
|
||||
t('content.options.cancel')
|
||||
t('common:buttons.cancel')
|
||||
],
|
||||
cancelButtonIndex: 2,
|
||||
userInterfaceStyle: mode
|
||||
|
@ -32,7 +32,7 @@ export const menuListDelete = ({
|
||||
key: 'list-delete',
|
||||
onSelect: () =>
|
||||
Alert.alert(
|
||||
i18next.t('screenTabs:me.listDelete.confirm.title', { list: params.title.slice(0, 6) }),
|
||||
i18next.t('screenTabs:me.listDelete.confirm.title', { list: params.title.slice(0, 20) }),
|
||||
i18next.t('screenTabs:me.listDelete.confirm.message'),
|
||||
[
|
||||
{
|
||||
|
@ -28,10 +28,7 @@ const AccountInformation = React.memo(
|
||||
<View style={styles.base}>
|
||||
<Placeholder
|
||||
Animation={props => (
|
||||
<Fade
|
||||
{...props}
|
||||
style={{ backgroundColor: colors.shimmerHighlight }}
|
||||
/>
|
||||
<Fade {...props} style={{ backgroundColor: colors.shimmerHighlight }} />
|
||||
)}
|
||||
>
|
||||
<View style={styles.avatarAndActions}>
|
||||
@ -41,7 +38,7 @@ const AccountInformation = React.memo(
|
||||
|
||||
<AccountInformationName account={account} />
|
||||
|
||||
<AccountInformationAccount account={account} localInstance={myInfo} />
|
||||
<AccountInformationAccount account={account} />
|
||||
|
||||
<AccountInformationFields account={account} myInfo={myInfo} />
|
||||
|
||||
|
@ -1,10 +1,7 @@
|
||||
import Icon from '@components/Icon'
|
||||
import CustomText from '@components/Text'
|
||||
import { useRelationshipQuery } from '@utils/queryHooks/relationship'
|
||||
import {
|
||||
getInstanceAccount,
|
||||
getInstanceUri
|
||||
} from '@utils/slices/instancesSlice'
|
||||
import { getInstanceAccount, getInstanceUri } from '@utils/slices/instancesSlice'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React from 'react'
|
||||
@ -15,19 +12,12 @@ import { PlaceholderLine } from 'rn-placeholder'
|
||||
|
||||
export interface Props {
|
||||
account: Mastodon.Account | undefined
|
||||
localInstance: boolean
|
||||
}
|
||||
|
||||
const AccountInformationAccount: React.FC<Props> = ({
|
||||
account,
|
||||
localInstance
|
||||
}) => {
|
||||
const AccountInformationAccount: React.FC<Props> = ({ account }) => {
|
||||
const { t } = useTranslation('screenTabs')
|
||||
const { colors } = useTheme()
|
||||
const instanceAccount = useSelector(
|
||||
getInstanceAccount,
|
||||
(prev, next) => prev?.acct === next?.acct
|
||||
)
|
||||
const instanceAccount = useSelector(getInstanceAccount, (prev, next) => prev?.acct === next?.acct)
|
||||
const instanceUri = useSelector(getInstanceUri)
|
||||
|
||||
const { data: relationship } = useRelationshipQuery({
|
||||
@ -35,6 +25,10 @@ const AccountInformationAccount: React.FC<Props> = ({
|
||||
options: { enabled: account !== undefined }
|
||||
})
|
||||
|
||||
const localInstance = instanceAccount.acct.includes('@')
|
||||
? instanceAccount.acct.includes(`@${instanceUri}`)
|
||||
: true
|
||||
|
||||
if (account || (localInstance && instanceAccount)) {
|
||||
return (
|
||||
<View
|
||||
@ -54,7 +48,7 @@ const AccountInformationAccount: React.FC<Props> = ({
|
||||
) : null}
|
||||
<CustomText
|
||||
style={{
|
||||
textDecorationLine: (account?.moved || account?.suspended) ? 'line-through' : undefined
|
||||
textDecorationLine: account?.moved || account?.suspended ? 'line-through' : undefined
|
||||
}}
|
||||
selectable
|
||||
>
|
||||
@ -94,7 +88,4 @@ const AccountInformationAccount: React.FC<Props> = ({
|
||||
}
|
||||
}
|
||||
|
||||
export default React.memo(
|
||||
AccountInformationAccount,
|
||||
(_, next) => next.account === undefined
|
||||
)
|
||||
export default AccountInformationAccount
|
||||
|
@ -3,9 +3,11 @@ import GracefullyImage from '@components/GracefullyImage'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { StackNavigationProp } from '@react-navigation/stack'
|
||||
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
||||
import { getInstanceActive } from '@utils/slices/instancesSlice'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import React from 'react'
|
||||
import { Pressable, StyleSheet, View } from 'react-native'
|
||||
import { Pressable, View } from 'react-native'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
export interface Props {
|
||||
account: Mastodon.Account | undefined
|
||||
@ -13,24 +15,25 @@ export interface Props {
|
||||
edit?: boolean
|
||||
}
|
||||
|
||||
const AccountInformationAvatar: React.FC<Props> = ({
|
||||
account,
|
||||
myInfo,
|
||||
edit
|
||||
}) => {
|
||||
const navigation =
|
||||
useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||
const AccountInformationAvatar: React.FC<Props> = ({ account, myInfo, edit }) => {
|
||||
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||
useSelector(getInstanceActive)
|
||||
return (
|
||||
<Pressable
|
||||
disabled={!myInfo}
|
||||
onPress={() => {
|
||||
myInfo && account && navigation.push('Tab-Shared-Account', { account })
|
||||
}}
|
||||
style={styles.base}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
width: StyleConstants.Avatar.L,
|
||||
height: StyleConstants.Avatar.L
|
||||
}}
|
||||
>
|
||||
<GracefullyImage
|
||||
key={account?.avatar}
|
||||
style={styles.image}
|
||||
style={{ flex: 1 }}
|
||||
uri={{ original: account?.avatar, static: account?.avatar_static }}
|
||||
/>
|
||||
{edit ? (
|
||||
@ -51,14 +54,4 @@ const AccountInformationAvatar: React.FC<Props> = ({
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
width: StyleConstants.Avatar.L,
|
||||
height: StyleConstants.Avatar.L
|
||||
},
|
||||
image: { flex: 1 }
|
||||
})
|
||||
|
||||
export default AccountInformationAvatar
|
||||
|
@ -12,59 +12,53 @@ export interface Props {
|
||||
hidden?: boolean
|
||||
}
|
||||
|
||||
const AccountInformationCreated = React.memo(
|
||||
({ account, hidden = false }: Props) => {
|
||||
if (hidden) {
|
||||
return null
|
||||
}
|
||||
const AccountInformationCreated: React.FC<Props> = ({ account, hidden = false }) => {
|
||||
if (hidden) {
|
||||
return null
|
||||
}
|
||||
|
||||
const { i18n } = useTranslation()
|
||||
const { colors } = useTheme()
|
||||
const { t } = useTranslation('screenTabs')
|
||||
const { i18n } = useTranslation()
|
||||
const { colors } = useTheme()
|
||||
const { t } = useTranslation('screenTabs')
|
||||
|
||||
if (account) {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderRadius: 0,
|
||||
marginBottom: StyleConstants.Spacing.M
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name='Calendar'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={colors.secondary}
|
||||
style={{ marginRight: StyleConstants.Spacing.XS }}
|
||||
/>
|
||||
<CustomText fontStyle='S' style={{ color: colors.secondary }}>
|
||||
{t('shared.account.created_at', {
|
||||
date: new Date(account.created_at || '').toLocaleDateString(
|
||||
i18n.language,
|
||||
{
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
}
|
||||
)
|
||||
})}
|
||||
</CustomText>
|
||||
</View>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<PlaceholderLine
|
||||
width={StyleConstants.Font.Size.S * 4}
|
||||
height={StyleConstants.Font.LineHeight.S}
|
||||
color={colors.shimmerDefault}
|
||||
noMargin
|
||||
style={{ borderRadius: 0, marginBottom: StyleConstants.Spacing.M }}
|
||||
if (account) {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderRadius: 0,
|
||||
marginBottom: StyleConstants.Spacing.M
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name='Calendar'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={colors.secondary}
|
||||
style={{ marginRight: StyleConstants.Spacing.XS }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
},
|
||||
(_, next) => next.account === undefined
|
||||
)
|
||||
<CustomText fontStyle='S' style={{ color: colors.secondary }}>
|
||||
{t('shared.account.created_at', {
|
||||
date: new Date(account.created_at || '').toLocaleDateString(i18n.language, {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})
|
||||
})}
|
||||
</CustomText>
|
||||
</View>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<PlaceholderLine
|
||||
width={StyleConstants.Font.Size.S * 4}
|
||||
height={StyleConstants.Font.LineHeight.S}
|
||||
color={colors.shimmerDefault}
|
||||
noMargin
|
||||
style={{ borderRadius: 0, marginBottom: StyleConstants.Spacing.M }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default AccountInformationCreated
|
||||
|
@ -10,53 +10,50 @@ export interface Props {
|
||||
myInfo?: boolean
|
||||
}
|
||||
|
||||
const AccountInformationFields = React.memo(
|
||||
({ account, myInfo }: Props) => {
|
||||
if (account?.suspended || myInfo || !account?.fields || account.fields.length === 0) {
|
||||
return null
|
||||
}
|
||||
const AccountInformationFields: React.FC<Props> = ({ account, myInfo }) => {
|
||||
if (account?.suspended || myInfo || !account?.fields || account.fields.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const { colors } = useTheme()
|
||||
const { colors } = useTheme()
|
||||
|
||||
return (
|
||||
<View style={[styles.fields, { borderTopColor: colors.border }]}>
|
||||
{account.fields.map((field, index) => (
|
||||
<View key={index} style={[styles.field, { borderBottomColor: colors.border }]}>
|
||||
<View style={[styles.fieldLeft, { borderRightColor: colors.border }]}>
|
||||
<ParseHTML
|
||||
content={field.name}
|
||||
size={'S'}
|
||||
emojis={account.emojis}
|
||||
showFullLink
|
||||
numberOfLines={5}
|
||||
selectable
|
||||
return (
|
||||
<View style={[styles.fields, { borderTopColor: colors.border }]}>
|
||||
{account.fields.map((field, index) => (
|
||||
<View key={index} style={[styles.field, { borderBottomColor: colors.border }]}>
|
||||
<View style={[styles.fieldLeft, { borderRightColor: colors.border }]}>
|
||||
<ParseHTML
|
||||
content={field.name}
|
||||
size={'S'}
|
||||
emojis={account.emojis}
|
||||
showFullLink
|
||||
numberOfLines={5}
|
||||
selectable
|
||||
/>
|
||||
{field.verified_at ? (
|
||||
<Icon
|
||||
name='CheckCircle'
|
||||
size={StyleConstants.Font.Size.M}
|
||||
color={colors.primaryDefault}
|
||||
style={styles.fieldCheck}
|
||||
/>
|
||||
{field.verified_at ? (
|
||||
<Icon
|
||||
name='CheckCircle'
|
||||
size={StyleConstants.Font.Size.M}
|
||||
color={colors.primaryDefault}
|
||||
style={styles.fieldCheck}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
<View style={styles.fieldRight}>
|
||||
<ParseHTML
|
||||
content={field.value}
|
||||
size={'S'}
|
||||
emojis={account.emojis}
|
||||
showFullLink
|
||||
numberOfLines={5}
|
||||
selectable
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)
|
||||
},
|
||||
(_, next) => next.account === undefined
|
||||
)
|
||||
<View style={styles.fieldRight}>
|
||||
<ParseHTML
|
||||
content={field.value}
|
||||
size={'S'}
|
||||
emojis={account.emojis}
|
||||
showFullLink
|
||||
numberOfLines={5}
|
||||
selectable
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
fields: {
|
||||
|
@ -66,4 +66,4 @@ const AccountInformationName: React.FC<Props> = ({ account }) => {
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(AccountInformationName, (_, next) => next.account === undefined)
|
||||
export default AccountInformationName
|
||||
|
@ -1,45 +1,35 @@
|
||||
import { ParseHTML } from '@components/Parse'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import React, { useState } from 'react'
|
||||
import { StyleSheet, View } from 'react-native'
|
||||
import React from 'react'
|
||||
import { View } from 'react-native'
|
||||
|
||||
export interface Props {
|
||||
account: Mastodon.Account | undefined
|
||||
myInfo?: boolean
|
||||
}
|
||||
|
||||
const AccountInformationNote = React.memo(
|
||||
({ account, myInfo }: Props) => {
|
||||
const [note, setNote] = useState(account?.source?.note)
|
||||
if (
|
||||
account?.suspended ||
|
||||
myInfo ||
|
||||
!account?.note ||
|
||||
account.note.length === 0 ||
|
||||
account.note === '<p></p>'
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.note}>
|
||||
<ParseHTML
|
||||
content={account.note!}
|
||||
size={'M'}
|
||||
emojis={account.emojis}
|
||||
selectable
|
||||
numberOfLines={999}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
},
|
||||
(_, next) => next.account === undefined
|
||||
)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
note: {
|
||||
marginBottom: StyleConstants.Spacing.L
|
||||
const AccountInformationNote: React.FC<Props> = ({ account, myInfo }) => {
|
||||
if (
|
||||
account?.suspended ||
|
||||
myInfo ||
|
||||
!account?.note ||
|
||||
account.note.length === 0 ||
|
||||
account.note === '<p></p>'
|
||||
) {
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<View style={{ marginBottom: StyleConstants.Spacing.L }}>
|
||||
<ParseHTML
|
||||
content={account.note!}
|
||||
size={'M'}
|
||||
emojis={account.emojis}
|
||||
selectable
|
||||
numberOfLines={999}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountInformationNote
|
||||
|
@ -39,7 +39,7 @@ const TabSharedAccountInLists: React.FC<
|
||||
})
|
||||
}, [])
|
||||
|
||||
const listsQuery = useListsQuery({})
|
||||
const listsQuery = useListsQuery()
|
||||
const accountInListsQuery = useAccountInListsQuery({ id: account.id })
|
||||
|
||||
const sections = [
|
||||
@ -48,8 +48,10 @@ const TabSharedAccountInLists: React.FC<
|
||||
id: 'out',
|
||||
title: t('shared.accountInLists.notInLists'),
|
||||
data:
|
||||
listsQuery.data?.filter(
|
||||
({ id }) => accountInListsQuery.data?.filter(d => d.id !== id).length
|
||||
listsQuery.data?.filter(({ id }) =>
|
||||
accountInListsQuery.data?.length
|
||||
? accountInListsQuery.data.filter(d => d.id !== id).length
|
||||
: true
|
||||
) || []
|
||||
}
|
||||
]
|
||||
|
@ -54,7 +54,11 @@ const TabSharedSearch: React.FC<TabSharedStackScreenProps<'Tab-Shared-Search'>>
|
||||
fontSize: StyleConstants.Font.Size.M,
|
||||
flex: 1,
|
||||
color: colors.primaryDefault,
|
||||
paddingLeft: StyleConstants.Spacing.XS
|
||||
marginLeft: StyleConstants.Spacing.XS,
|
||||
paddingLeft: StyleConstants.Spacing.XS,
|
||||
paddingVertical: StyleConstants.Spacing.XS,
|
||||
borderBottomColor: colors.border,
|
||||
borderBottomWidth: 1
|
||||
}}
|
||||
autoFocus
|
||||
onChangeText={debounce(
|
||||
|