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

Merge branch 'main' into candidate

This commit is contained in:
xmflsct
2022-12-13 00:12:39 +01:00
68 changed files with 515 additions and 361 deletions

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

View File

@@ -1,6 +1,7 @@
const demoStatuses = [ const demoStatus: Mastodon.Status[] = [
{ {
id: '1', id: '1',
uri: 'https://example.com',
created_at: new Date().toISOString(), created_at: new Date().toISOString(),
sensitive: false, sensitive: false,
visibility: 'public', visibility: 'public',
@@ -13,7 +14,6 @@ const demoStatuses = [
bookmarked: false, bookmarked: false,
content: content:
'<p>Would you like to try out this simple, beautiful and open-source mobile app for Mastodon? 😊</p>', '<p>Would you like to try out this simple, beautiful and open-source mobile app for Mastodon? 😊</p>',
reblog: null,
application: { application: {
name: 'tooot', name: 'tooot',
website: 'https://tooot.app' website: 'https://tooot.app'
@@ -23,19 +23,31 @@ const demoStatuses = [
username: 'tooot📱', username: 'tooot📱',
acct: 'tooot@xmflsct.com', acct: 'tooot@xmflsct.com',
display_name: 'tooot📱', display_name: 'tooot📱',
avatar_static: avatar: 'https://avatars.githubusercontent.com/u/77554750?s=200&v=4',
'https://avatars.githubusercontent.com/u/77554750?s=200&v=4' avatar_static: 'https://avatars.githubusercontent.com/u/77554750?s=200&v=4',
url: '',
header: '',
header_static: '',
locked: false,
discoverable: false,
created_at: new Date().toISOString(),
last_status_at: new Date().toISOString(),
statuses_count: 1,
followers_count: 1,
following_count: 1,
fields: [],
bot: false
}, },
media_attachments: [], media_attachments: [],
poll: { poll: {
id: '1', id: '1',
expires_at: new Date().setDate(new Date().getDate() + 5), expires_at: new Date().setDate(new Date().getDate() + 5).toString(),
expired: false, expired: false,
multiple: false, multiple: false,
votes_count: 10, votes_count: 10,
voters_count: null, voters_count: 2,
voted: false, voted: false,
own_votes: null, own_votes: undefined,
options: [ options: [
{ {
title: 'I would love to!', title: 'I would love to!',
@@ -48,11 +60,15 @@ const demoStatuses = [
], ],
emojis: [] emojis: []
}, },
mentions: [] mentions: [],
tags: [],
emojis: [],
pinned: false
}, },
{ {
id: '2', id: '2',
created_at: new Date().setMinutes(new Date().getMinutes() - 2), uri: 'https://example.com',
created_at: new Date().setMinutes(new Date().getMinutes() - 2).toString(),
sensitive: false, sensitive: false,
spoiler_text: '', spoiler_text: '',
visibility: 'public', visibility: 'public',
@@ -65,18 +81,26 @@ const demoStatuses = [
bookmarked: false, bookmarked: false,
content: content:
'<p>Mastodon is a free and open-source self-hosted social networking service. It allows anyone to host their own server node in the network, and its various separately operated user bases are federated across many different servers. These nodes are referred to as "instances" by Mastodon users.</p>', '<p>Mastodon is a free and open-source self-hosted social networking service. It allows anyone to host their own server node in the network, and its various separately operated user bases are federated across many different servers. These nodes are referred to as "instances" by Mastodon users.</p>',
reblog: null, application: { name: 'Web' },
application: {
name: 'Web',
website: null
},
account: { account: {
id: '1000', id: '1000',
username: 'Mastodon', username: 'Mastodon',
acct: 'mastodon', acct: 'mastodon',
display_name: 'Mastodon', display_name: 'Mastodon',
avatar_static: avatar: 'https://mastodon.social/apple-touch-icon.png',
'https://mastodon.social/apple-touch-icon.png' avatar_static: 'https://mastodon.social/apple-touch-icon.png',
url: '',
header: '',
header_static: '',
locked: false,
discoverable: false,
created_at: new Date().toISOString(),
last_status_at: new Date().toISOString(),
statuses_count: 1,
followers_count: 1,
following_count: 1,
fields: [],
bot: false
}, },
media_attachments: [], media_attachments: [],
card: { card: {
@@ -85,18 +109,31 @@ const demoStatuses = [
description: description:
'Mastodon is an open source decentralized social network - by the people for the people. Join the federation and take back control of your social media!', 'Mastodon is an open source decentralized social network - by the people for the people. Join the federation and take back control of your social media!',
type: 'link', type: 'link',
image: image: 'https://mastodon.social/apple-touch-icon.png',
'https://mastodon.social/apple-touch-icon.png' author_name: '',
author_url: '',
provider_name: '',
provider_url: '',
html: '<p></p>',
width: 100,
height: 100,
embed_url: 'https://example.com',
blurhash: ''
}, },
mentions: [] mentions: [],
tags: [],
emojis: [],
pinned: false
}, },
{ {
id: '3', id: '3',
created_at: '2021-01-24T09:50:00.901Z', uri: '',
created_at: new Date().setHours(new Date().getHours() - 1).toString(),
sensitive: false,
spoiler_text: '', spoiler_text: '',
visibility: 'public', visibility: 'public',
replies_count: 2, replies_count: 2,
reblogs_count: null, reblogs_count: 1,
favourites_count: 3, favourites_count: 3,
favourited: false, favourited: false,
reblogged: false, reblogged: false,
@@ -104,24 +141,38 @@ const demoStatuses = [
bookmarked: true, bookmarked: true,
content: content:
'<p>These servers are connected as a federated social network, allowing users from different servers to interact with each other seamlessly. Once a Mastodon server knows another Mastodon server, it "federates" with the other Mastodon server. Mastodon is a part of the wider Fediverse, allowing its users to also interact with users on different open platforms that support the same protocol, such as PeerTube and Friendica.</p>', '<p>These servers are connected as a federated social network, allowing users from different servers to interact with each other seamlessly. Once a Mastodon server knows another Mastodon server, it "federates" with the other Mastodon server. Mastodon is a part of the wider Fediverse, allowing its users to also interact with users on different open platforms that support the same protocol, such as PeerTube and Friendica.</p>',
reblog: null, application: { name: 'Web' },
application: {
name: 'Web',
website: null
},
account: { account: {
id: '1001', id: '1001',
username: 'Fediverse', username: 'Fediverse',
acct: 'fediverse', acct: 'fediverse',
display_name: 'Fediverse', display_name: 'Fediverse',
avatar:
'https://e7.pngegg.com/pngimages/667/514/png-clipart-mastodon-fediverse-social-media-free-software-logo-social-media-blue-text.png',
avatar_static: avatar_static:
'https://e7.pngegg.com/pngimages/667/514/png-clipart-mastodon-fediverse-social-media-free-software-logo-social-media-blue-text.png' 'https://e7.pngegg.com/pngimages/667/514/png-clipart-mastodon-fediverse-social-media-free-software-logo-social-media-blue-text.png',
url: '',
header: '',
header_static: '',
locked: false,
discoverable: false,
created_at: new Date().toISOString(),
last_status_at: new Date().toISOString(),
statuses_count: 1,
followers_count: 1,
following_count: 1,
fields: [],
bot: false
}, },
media_attachments: [], media_attachments: [],
mentions: [] mentions: [],
tags: [],
emojis: [],
pinned: false
}, },
{ {
id: '4', id: '4',
uri: 'https://example.com',
created_at: '2021-01-24T08:50:00.901Z', created_at: '2021-01-24T08:50:00.901Z',
sensitive: false, sensitive: false,
visibility: 'public', visibility: 'public',
@@ -134,7 +185,6 @@ const demoStatuses = [
bookmarked: false, bookmarked: false,
content: content:
'<p>tooot is an open source, simple mobile client for Mastodon. Focusing on your connections while being able to explore the Fediverse.</p>', '<p>tooot is an open source, simple mobile client for Mastodon. Focusing on your connections while being able to explore the Fediverse.</p>',
reblog: null,
application: { application: {
name: 'tooot', name: 'tooot',
website: 'https://tooot.app' website: 'https://tooot.app'
@@ -144,14 +194,30 @@ const demoStatuses = [
username: 'tooot📱', username: 'tooot📱',
acct: 'tooot@xmflsct.com', acct: 'tooot@xmflsct.com',
display_name: 'tooot📱', display_name: 'tooot📱',
avatar_static: avatar: 'https://avatars.githubusercontent.com/u/77554750?s=200&v=4',
'https://avatars.githubusercontent.com/u/77554750?s=200&v=4' avatar_static: 'https://avatars.githubusercontent.com/u/77554750?s=200&v=4',
url: '',
header: '',
header_static: '',
locked: false,
discoverable: false,
created_at: new Date().toISOString(),
last_status_at: new Date().toISOString(),
statuses_count: 1,
followers_count: 1,
following_count: 1,
fields: [],
bot: false
}, },
media_attachments: [], media_attachments: [],
mentions: [] mentions: [],
tags: [],
emojis: [],
pinned: false
}, },
{ {
id: '5', id: '5',
uri: 'https://example.com',
created_at: '2021-01-24T07:50:00.901Z', created_at: '2021-01-24T07:50:00.901Z',
sensitive: false, sensitive: false,
visibility: 'public', visibility: 'public',
@@ -164,7 +230,6 @@ const demoStatuses = [
bookmarked: false, bookmarked: false,
content: content:
'<p>- tooot supports multiple accounts<br />- tooot supports browsing external instance<br />- tooot aims to support multiple languages</p>', '<p>- tooot supports multiple accounts<br />- tooot supports browsing external instance<br />- tooot aims to support multiple languages</p>',
reblog: null,
application: { application: {
name: 'tooot', name: 'tooot',
website: 'https://tooot.app' website: 'https://tooot.app'
@@ -174,12 +239,27 @@ const demoStatuses = [
username: 'tooot📱', username: 'tooot📱',
acct: 'tooot@xmflsct.com', acct: 'tooot@xmflsct.com',
display_name: 'tooot📱', display_name: 'tooot📱',
avatar_static: avatar: 'https://avatars.githubusercontent.com/u/77554750?s=200&v=4',
'https://avatars.githubusercontent.com/u/77554750?s=200&v=4' avatar_static: 'https://avatars.githubusercontent.com/u/77554750?s=200&v=4',
url: '',
header: '',
header_static: '',
locked: false,
discoverable: false,
created_at: new Date().toISOString(),
last_status_at: new Date().toISOString(),
statuses_count: 1,
followers_count: 1,
following_count: 1,
fields: [],
bot: false
}, },
media_attachments: [], media_attachments: [],
mentions: [] mentions: [],
tags: [],
emojis: [],
pinned: false
} }
] ]
export default demoStatuses export default demoStatus

View File

@@ -333,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
@@ -406,6 +416,19 @@ 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[]

View File

@@ -86,9 +86,10 @@ const MenuRow: React.FC<Props> = ({
> >
<View <View
style={{ style={{
flex: 3, flexGrow: 3,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center' alignItems: 'center',
marginRight: StyleConstants.Spacing.M
}} }}
> >
{iconFront && ( {iconFront && (

View File

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

View File

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

View File

@@ -45,6 +45,21 @@ const TimelineAttachment = () => {
} }
const [sensitiveShown, setSensitiveShown] = useState(defaultSensitive()) const [sensitiveShown, setSensitiveShown] = useState(defaultSensitive())
// const testHorizontal: Mastodon.Attachment[] = Array(2).fill({
// id: Math.random().toString(),
// type: 'image',
// url: 'https://images.unsplash.com/photo-1670870764013-f0e36aa376b0?w=1000',
// preview_url: 'https://images.unsplash.com/photo-1543968996-ee822b8176ba?w=300',
// meta: { original: { width: 1000, height: 625 } }
// })
// const testVertical: Mastodon.Attachment[] = Array(7).fill({
// id: Math.random().toString(),
// type: 'image',
// url: 'https://images.unsplash.com/photo-1670842587871-326b95acbc8c?w=1000',
// preview_url: 'https://images.unsplash.com/photo-1670833288990-64b2f4ef7290?w=300',
// meta: { original: { width: 987, height: 1480 } }
// })
// @ts-ignore // @ts-ignore
const imageUrls: RootStackParamList['Screen-ImagesViewer']['imageUrls'] = status.media_attachments const imageUrls: RootStackParamList['Screen-ImagesViewer']['imageUrls'] = status.media_attachments
.map(attachment => { .map(attachment => {

View File

@@ -8,7 +8,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'
import { AppState, AppStateStatus, StyleSheet, View } from 'react-native' import { AppState, AppStateStatus, StyleSheet, View } from 'react-native'
import { Blurhash } from 'react-native-blurhash' import { Blurhash } from 'react-native-blurhash'
import AttachmentAltText from './AltText' import AttachmentAltText from './AltText'
import attachmentAspectRatio from './aspectRatio' import { aspectRatio } from './dimensions'
export interface Props { export interface Props {
total: number total: number
@@ -64,7 +64,7 @@ const AttachmentAudio: React.FC<Props> = ({ total, index, sensitiveShown, audio
styles.base, styles.base,
{ {
backgroundColor: colors.disabled, backgroundColor: colors.disabled,
aspectRatio: attachmentAspectRatio({ total, index }) aspectRatio: aspectRatio({ total, index, ...audio.meta?.original })
} }
]} ]}
> >

View File

@@ -1,9 +1,10 @@
import GracefullyImage from '@components/GracefullyImage' import GracefullyImage from '@components/GracefullyImage'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react' import React from 'react'
import { View } from 'react-native' import { View } from 'react-native'
import AttachmentAltText from './AltText' import AttachmentAltText from './AltText'
import attachmentAspectRatio from './aspectRatio' import { aspectRatio } from './dimensions'
export interface Props { export interface Props {
total: number total: number
@@ -20,6 +21,8 @@ const AttachmentImage = ({
image, image,
navigateToImagesViewer navigateToImagesViewer
}: Props) => { }: Props) => {
const { colors } = useTheme()
return ( return (
<View <View
style={{ style={{
@@ -28,21 +31,16 @@ const AttachmentImage = ({
padding: StyleConstants.Spacing.XS / 2 padding: StyleConstants.Spacing.XS / 2
}} }}
> >
<GracefullyImage <View style={{ flex: 1, backgroundColor: colors.shimmerDefault }}>
accessibilityLabel={image.description} <GracefullyImage
hidden={sensitiveShown} accessibilityLabel={image.description}
uri={{ original: image.preview_url, remote: image.remote_url }} hidden={sensitiveShown}
blurhash={image.blurhash} uri={{ original: image.preview_url, remote: image.remote_url }}
onPress={() => navigateToImagesViewer(image.id)} blurhash={image.blurhash}
style={{ onPress={() => navigateToImagesViewer(image.id)}
aspectRatio: style={{ aspectRatio: aspectRatio({ total, index, ...image.meta?.original }) }}
total > 1 || !image.meta?.original?.width || !image.meta?.original?.height />
? attachmentAspectRatio({ total, index }) </View>
: image.meta.original.height / image.meta.original.width > 1
? 1
: image.meta.original.width / image.meta.original.height
}}
/>
<AttachmentAltText sensitiveShown={sensitiveShown} text={image.description} /> <AttachmentAltText sensitiveShown={sensitiveShown} text={image.description} />
</View> </View>
) )

View File

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

View File

@@ -4,10 +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 { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import { aspectRatio } from './dimensions'
export interface Props { export interface Props {
total: number total: number
@@ -49,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

View File

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

View File

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

View File

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

View File

@@ -30,18 +30,24 @@ const matchAccount = (
// https://social.xmflsct.com/web/accounts/14195 <- default // 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/web/@tooot <- pretty ! cannot be searched on the same instance
// https://social.xmflsct.com/@tooot <- pretty // 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) const matched = url.match(matcherAccount)
if (matched) { if (matched) {
const hostname = matched[2] const hostname = matched[2]
const style = matched[4].startsWith('@') ? 'pretty' : 'default' const account = matched.filter(i => i).reverse()?.[0]
const account = matched[4] if (account) {
const style = account.startsWith('@') ? 'pretty' : 'default'
const instanceUrl = getInstanceUrl(store.getState()) const instanceUrl = getInstanceUrl(store.getState())
return style === 'default' return style === 'default'
? { id: account, style, sameInstance: hostname === instanceUrl } ? { id: account, style, sameInstance: hostname === instanceUrl }
: { username: account, style, sameInstance: hostname === instanceUrl } : { username: account, style, sameInstance: hostname === instanceUrl }
} else {
return null
}
} }
return null return null

View File

@@ -1,7 +1,7 @@
{ {
"server": { "server": {
"textInput": { "textInput": {
"placeholder": "Instance' domain" "placeholder": "Instance's domain"
}, },
"button": "Login", "button": "Login",
"information": { "information": {

View File

@@ -30,7 +30,9 @@
"default": "{{name}} boosted", "default": "{{name}} boosted",
"notification": "{{name}} boosted your toot" "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": { "actions": {
"reply": { "reply": {
@@ -52,7 +54,8 @@
"bookmarked": { "bookmarked": {
"accessibilityLabel": "Add this toot to bookmarks", "accessibilityLabel": "Add this toot to bookmarks",
"function": "Bookmark toot" "function": "Bookmark toot"
} },
"openReport": "Open report"
}, },
"actionsUsers": { "actionsUsers": {
"reblogged_by": { "reblogged_by": {

View File

@@ -1,13 +1,11 @@
{ {
"heading": { "heading": {
"left": { "left": {
"button": "Cancel",
"alert": { "alert": {
"title": "Cancel editing?", "title": "Cancel editing?",
"buttons": { "buttons": {
"save": "Save draft", "save": "Save draft",
"delete": "Delete draft", "delete": "Delete draft"
"cancel": "Cancel"
} }
} }
}, },

View File

@@ -4,7 +4,6 @@
"name": "Following" "name": "Following"
}, },
"public": { "public": {
"name": "",
"segments": { "segments": {
"federated": "Federated", "federated": "Federated",
"local": "Local", "local": "Local",
@@ -13,9 +12,6 @@
}, },
"notifications": { "notifications": {
"name": "Notifications" "name": "Notifications"
},
"me": {
"name": "About me"
} }
}, },
"common": { "common": {
@@ -348,7 +344,7 @@
"notInLists": "Other lists" "notInLists": "Other lists"
}, },
"attachments": { "attachments": {
"name": "<0 /><1>\"s media</1>" "name": "<0 /><1>'s media</1>"
}, },
"hashtag": { "hashtag": {
"follow": "Follow", "follow": "Follow",

View File

@@ -206,7 +206,7 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
() => ( () => (
<HeaderLeft <HeaderLeft
type='text' type='text'
content={t('heading.left.button')} content={t('common:buttons.cancel')}
onPress={() => { onPress={() => {
if (!composeState.dirty) { if (!composeState.dirty) {
navigation.goBack() 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' style: 'cancel'
} }
]) ])
@@ -342,9 +342,7 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
) )
const headerContent = useMemo(() => { const headerContent = useMemo(() => {
return `${totalTextCount} / ${maxTootChars}${ return `${totalTextCount} / ${maxTootChars}`
__DEV__ ? ` Dirty: ${composeState.dirty.toString()}` : ''
}`
}, [totalTextCount, maxTootChars, composeState.dirty]) }, [totalTextCount, maxTootChars, composeState.dirty])
const inputProps: EmojisState['inputProps'] = [ const inputProps: EmojisState['inputProps'] = [

View File

@@ -112,6 +112,17 @@ const ComposeEditAttachmentImage: React.FC<Props> = ({ index }) => {
return ( 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' }}> <View style={{ overflow: 'hidden', flex: 1, alignItems: 'center' }}>
<Image <Image
style={{ style={{
@@ -155,17 +166,6 @@ const ComposeEditAttachmentImage: React.FC<Props> = ({ index }) => {
/> />
</GestureDetector> </GestureDetector>
</View> </View>
{screenReaderEnabled ? null : (
<CustomText
fontStyle='M'
style={{
padding: StyleConstants.Spacing.Global.PagePadding,
color: colors.primaryDefault
}}
>
{t('content.editAttachment.content.imageFocus')}
</CustomText>
)}
</> </>
) )
} }

View File

@@ -2,7 +2,7 @@ import CustomText from '@components/Text'
import AttachmentVideo from '@components/Timeline/Shared/Attachment/Video' import AttachmentVideo from '@components/Timeline/Shared/Attachment/Video'
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, useRef } from 'react' import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ScrollView, StyleSheet, TextInput, View } from 'react-native' import { ScrollView, StyleSheet, TextInput, View } from 'react-native'
import ComposeContext from '../utils/createContext' import ComposeContext from '../utils/createContext'
@@ -34,10 +34,10 @@ const ComposeEditAttachmentRoot: React.FC<Props> = ({ index }) => {
video={ video={
video.local video.local
? ({ ? ({
url: video.local.uri, url: video.local.uri,
preview_url: video.local.thumbnail, preview_url: video.local.thumbnail,
blurhash: video.remote?.blurhash blurhash: video.remote?.blurhash
} as Mastodon.AttachmentVideo) } as Mastodon.AttachmentVideo)
: (video.remote as Mastodon.AttachmentVideo) : (video.remote as Mastodon.AttachmentVideo)
} }
/> />
@@ -47,45 +47,36 @@ const ComposeEditAttachmentRoot: React.FC<Props> = ({ index }) => {
return null return null
} }
const scrollViewRef = useRef<ScrollView>(null)
return ( return (
<ScrollView ref={scrollViewRef}> <ScrollView>
{mediaDisplay()} <View style={{ padding: StyleConstants.Spacing.Global.PagePadding, paddingBottom: 0 }}>
<View style={{ padding: StyleConstants.Spacing.Global.PagePadding }}> <CustomText fontStyle='M' style={{ color: colors.primaryDefault }} fontWeight='Bold'>
<CustomText
fontStyle='M'
style={{ color: colors.primaryDefault }}
fontWeight='Bold'
>
{t('content.editAttachment.content.altText.heading')} {t('content.editAttachment.content.altText.heading')}
</CustomText> </CustomText>
<TextInput <TextInput
style={{ style={{
height: 200, height: StyleConstants.Font.Size.M * 11 + StyleConstants.Spacing.Global.PagePadding * 2,
...StyleConstants.FontStyle.M, ...StyleConstants.FontStyle.M,
marginTop: StyleConstants.Spacing.M, marginTop: StyleConstants.Spacing.M,
marginBottom: StyleConstants.Spacing.S, marginBottom: StyleConstants.Spacing.S,
padding: StyleConstants.Spacing.Global.PagePadding, padding: StyleConstants.Spacing.Global.PagePadding,
paddingTop: StyleConstants.Spacing.S * 1.5,
borderWidth: StyleSheet.hairlineWidth, borderWidth: StyleSheet.hairlineWidth,
borderColor: colors.border, borderColor: colors.border,
color: colors.primaryDefault color: colors.primaryDefault
}} }}
onFocus={() => scrollViewRef.current?.scrollToEnd()}
maxLength={1500} maxLength={1500}
multiline multiline
onChangeText={(e) => onChangeText={e =>
composeDispatch({ composeDispatch({
type: 'attachment/edit', type: 'attachment/edit',
payload: { payload: {
...theAttachment, ...theAttachment,
description: e description: e
} }
})} })
}
placeholder={t('content.editAttachment.content.altText.placeholder')} placeholder={t('content.editAttachment.content.altText.placeholder')}
placeholderTextColor={colors.secondary} placeholderTextColor={colors.secondary}
scrollEnabled
value={theAttachment.description} value={theAttachment.description}
keyboardAppearance={mode} keyboardAppearance={mode}
/> />
@@ -101,6 +92,7 @@ const ComposeEditAttachmentRoot: React.FC<Props> = ({ index }) => {
{theAttachment.description?.length || 0} / 1500 {theAttachment.description?.length || 0} / 1500
</CustomText> </CustomText>
</View> </View>
{mediaDisplay()}
</ScrollView> </ScrollView>
) )
} }

View File

@@ -180,7 +180,7 @@ const ScreenImagesViewer = ({
options: [ options: [
t('content.options.save'), t('content.options.save'),
t('content.options.share'), t('content.options.share'),
t('content.options.cancel') t('common:buttons.cancel')
], ],
cancelButtonIndex: 2, cancelButtonIndex: 2,
userInterfaceStyle: mode userInterfaceStyle: mode

View File

@@ -32,7 +32,7 @@ export const menuListDelete = ({
key: 'list-delete', key: 'list-delete',
onSelect: () => onSelect: () =>
Alert.alert( 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'), i18next.t('screenTabs:me.listDelete.confirm.message'),
[ [
{ {

View File

@@ -28,10 +28,7 @@ const AccountInformation = React.memo(
<View style={styles.base}> <View style={styles.base}>
<Placeholder <Placeholder
Animation={props => ( Animation={props => (
<Fade <Fade {...props} style={{ backgroundColor: colors.shimmerHighlight }} />
{...props}
style={{ backgroundColor: colors.shimmerHighlight }}
/>
)} )}
> >
<View style={styles.avatarAndActions}> <View style={styles.avatarAndActions}>
@@ -41,7 +38,7 @@ const AccountInformation = React.memo(
<AccountInformationName account={account} /> <AccountInformationName account={account} />
<AccountInformationAccount account={account} localInstance={myInfo} /> <AccountInformationAccount account={account} />
<AccountInformationFields account={account} myInfo={myInfo} /> <AccountInformationFields account={account} myInfo={myInfo} />

View File

@@ -1,10 +1,7 @@
import Icon from '@components/Icon' import Icon from '@components/Icon'
import CustomText from '@components/Text' import CustomText from '@components/Text'
import { useRelationshipQuery } from '@utils/queryHooks/relationship' import { useRelationshipQuery } from '@utils/queryHooks/relationship'
import { import { getInstanceAccount, getInstanceUri } from '@utils/slices/instancesSlice'
getInstanceAccount,
getInstanceUri
} 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 from 'react' import React from 'react'
@@ -15,19 +12,12 @@ import { PlaceholderLine } from 'rn-placeholder'
export interface Props { export interface Props {
account: Mastodon.Account | undefined account: Mastodon.Account | undefined
localInstance: boolean
} }
const AccountInformationAccount: React.FC<Props> = ({ const AccountInformationAccount: React.FC<Props> = ({ account }) => {
account,
localInstance
}) => {
const { t } = useTranslation('screenTabs') const { t } = useTranslation('screenTabs')
const { colors } = useTheme() const { colors } = useTheme()
const instanceAccount = useSelector( const instanceAccount = useSelector(getInstanceAccount, (prev, next) => prev?.acct === next?.acct)
getInstanceAccount,
(prev, next) => prev?.acct === next?.acct
)
const instanceUri = useSelector(getInstanceUri) const instanceUri = useSelector(getInstanceUri)
const { data: relationship } = useRelationshipQuery({ const { data: relationship } = useRelationshipQuery({
@@ -35,6 +25,10 @@ const AccountInformationAccount: React.FC<Props> = ({
options: { enabled: account !== undefined } options: { enabled: account !== undefined }
}) })
const localInstance = instanceAccount.acct.includes('@')
? instanceAccount.acct.includes(`@${instanceUri}`)
: true
if (account || (localInstance && instanceAccount)) { if (account || (localInstance && instanceAccount)) {
return ( return (
<View <View
@@ -54,7 +48,7 @@ const AccountInformationAccount: React.FC<Props> = ({
) : null} ) : null}
<CustomText <CustomText
style={{ style={{
textDecorationLine: (account?.moved || account?.suspended) ? 'line-through' : undefined textDecorationLine: account?.moved || account?.suspended ? 'line-through' : undefined
}} }}
selectable selectable
> >
@@ -94,7 +88,4 @@ const AccountInformationAccount: React.FC<Props> = ({
} }
} }
export default React.memo( export default AccountInformationAccount
AccountInformationAccount,
(_, next) => next.account === undefined
)

View File

@@ -3,9 +3,11 @@ import GracefullyImage from '@components/GracefullyImage'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack' import { StackNavigationProp } from '@react-navigation/stack'
import { TabLocalStackParamList } from '@utils/navigation/navigators' import { TabLocalStackParamList } from '@utils/navigation/navigators'
import { getInstanceActive } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import React from 'react' 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 { export interface Props {
account: Mastodon.Account | undefined account: Mastodon.Account | undefined
@@ -13,24 +15,25 @@ export interface Props {
edit?: boolean edit?: boolean
} }
const AccountInformationAvatar: React.FC<Props> = ({ const AccountInformationAvatar: React.FC<Props> = ({ account, myInfo, edit }) => {
account, const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
myInfo, useSelector(getInstanceActive)
edit
}) => {
const navigation =
useNavigation<StackNavigationProp<TabLocalStackParamList>>()
return ( return (
<Pressable <Pressable
disabled={!myInfo} disabled={!myInfo}
onPress={() => { onPress={() => {
myInfo && account && navigation.push('Tab-Shared-Account', { account }) 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 <GracefullyImage
key={account?.avatar} key={account?.avatar}
style={styles.image} style={{ flex: 1 }}
uri={{ original: account?.avatar, static: account?.avatar_static }} uri={{ original: account?.avatar, static: account?.avatar_static }}
/> />
{edit ? ( {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 export default AccountInformationAvatar

View File

@@ -12,59 +12,53 @@ export interface Props {
hidden?: boolean hidden?: boolean
} }
const AccountInformationCreated = React.memo( const AccountInformationCreated: React.FC<Props> = ({ account, hidden = false }) => {
({ account, hidden = false }: Props) => { if (hidden) {
if (hidden) { return null
return null }
}
const { i18n } = useTranslation() const { i18n } = useTranslation()
const { colors } = useTheme() const { colors } = useTheme()
const { t } = useTranslation('screenTabs') const { t } = useTranslation('screenTabs')
if (account) { if (account) {
return ( return (
<View <View
style={{ style={{
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
borderRadius: 0, borderRadius: 0,
marginBottom: StyleConstants.Spacing.M marginBottom: StyleConstants.Spacing.M
}} }}
> >
<Icon <Icon
name='Calendar' name='Calendar'
size={StyleConstants.Font.Size.S} size={StyleConstants.Font.Size.S}
color={colors.secondary} color={colors.secondary}
style={{ marginRight: StyleConstants.Spacing.XS }} 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 }}
/> />
) <CustomText fontStyle='S' style={{ color: colors.secondary }}>
} {t('shared.account.created_at', {
}, date: new Date(account.created_at || '').toLocaleDateString(i18n.language, {
(_, next) => next.account === undefined 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 export default AccountInformationCreated

View File

@@ -10,53 +10,50 @@ export interface Props {
myInfo?: boolean myInfo?: boolean
} }
const AccountInformationFields = React.memo( const AccountInformationFields: React.FC<Props> = ({ account, myInfo }) => {
({ account, myInfo }: Props) => { if (account?.suspended || myInfo || !account?.fields || account.fields.length === 0) {
if (account?.suspended || myInfo || !account?.fields || account.fields.length === 0) { return null
return null }
}
const { colors } = useTheme() const { colors } = useTheme()
return ( return (
<View style={[styles.fields, { borderTopColor: colors.border }]}> <View style={[styles.fields, { borderTopColor: colors.border }]}>
{account.fields.map((field, index) => ( {account.fields.map((field, index) => (
<View key={index} style={[styles.field, { borderBottomColor: colors.border }]}> <View key={index} style={[styles.field, { borderBottomColor: colors.border }]}>
<View style={[styles.fieldLeft, { borderRightColor: colors.border }]}> <View style={[styles.fieldLeft, { borderRightColor: colors.border }]}>
<ParseHTML <ParseHTML
content={field.name} content={field.name}
size={'S'} size={'S'}
emojis={account.emojis} emojis={account.emojis}
showFullLink showFullLink
numberOfLines={5} numberOfLines={5}
selectable selectable
/>
{field.verified_at ? (
<Icon
name='CheckCircle'
size={StyleConstants.Font.Size.M}
color={colors.primaryDefault}
style={styles.fieldCheck}
/> />
{field.verified_at ? ( ) : null}
<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>
</View> </View>
))} <View style={styles.fieldRight}>
</View> <ParseHTML
) content={field.value}
}, size={'S'}
(_, next) => next.account === undefined emojis={account.emojis}
) showFullLink
numberOfLines={5}
selectable
/>
</View>
</View>
))}
</View>
)
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
fields: { fields: {

View File

@@ -66,4 +66,4 @@ const AccountInformationName: React.FC<Props> = ({ account }) => {
) )
} }
export default React.memo(AccountInformationName, (_, next) => next.account === undefined) export default AccountInformationName

View File

@@ -1,45 +1,35 @@
import { ParseHTML } from '@components/Parse' import { ParseHTML } from '@components/Parse'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import React, { useState } from 'react' import React from 'react'
import { StyleSheet, View } from 'react-native' import { View } from 'react-native'
export interface Props { export interface Props {
account: Mastodon.Account | undefined account: Mastodon.Account | undefined
myInfo?: boolean myInfo?: boolean
} }
const AccountInformationNote = React.memo( const AccountInformationNote: React.FC<Props> = ({ account, myInfo }) => {
({ account, myInfo }: Props) => { if (
const [note, setNote] = useState(account?.source?.note) account?.suspended ||
if ( myInfo ||
account?.suspended || !account?.note ||
myInfo || account.note.length === 0 ||
!account?.note || account.note === '<p></p>'
account.note.length === 0 || ) {
account.note === '<p></p>' return null
) {
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
} }
})
return (
<View style={{ marginBottom: StyleConstants.Spacing.L }}>
<ParseHTML
content={account.note!}
size={'M'}
emojis={account.emojis}
selectable
numberOfLines={999}
/>
</View>
)
}
export default AccountInformationNote export default AccountInformationNote

View File

@@ -39,7 +39,7 @@ const TabSharedAccountInLists: React.FC<
}) })
}, []) }, [])
const listsQuery = useListsQuery({}) const listsQuery = useListsQuery()
const accountInListsQuery = useAccountInListsQuery({ id: account.id }) const accountInListsQuery = useAccountInListsQuery({ id: account.id })
const sections = [ const sections = [
@@ -48,8 +48,10 @@ const TabSharedAccountInLists: React.FC<
id: 'out', id: 'out',
title: t('shared.accountInLists.notInLists'), title: t('shared.accountInLists.notInLists'),
data: data:
listsQuery.data?.filter( listsQuery.data?.filter(({ id }) =>
({ id }) => accountInListsQuery.data?.filter(d => d.id !== id).length accountInListsQuery.data?.length
? accountInListsQuery.data.filter(d => d.id !== id).length
: true
) || [] ) || []
} }
] ]

View File

@@ -54,7 +54,11 @@ const TabSharedSearch: React.FC<TabSharedStackScreenProps<'Tab-Shared-Search'>>
fontSize: StyleConstants.Font.Size.M, fontSize: StyleConstants.Font.Size.M,
flex: 1, flex: 1,
color: colors.primaryDefault, 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 autoFocus
onChangeText={debounce( onChangeText={debounce(