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

View File

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

View File

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

View File

@ -28,18 +28,18 @@ import TimelineHeaderAndroid from './Shared/HeaderAndroid'
export interface Props {
notification: Mastodon.Notification
queryKey: QueryKeyTimeline
highlighted?: boolean
}
const TimelineNotifications: React.FC<Props> = ({
notification,
queryKey,
highlighted = false
}) => {
const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
const instanceAccount = useSelector(getInstanceAccount, () => true)
const status = notification.status?.reblog ? notification.status.reblog : notification.status
const account = notification.status ? notification.status.account : notification.account
const account =
notification.type === 'admin.report'
? notification.report.target_account
: notification.status
? notification.status.account
: notification.account
const ownAccount = notification.account?.id === instanceAccount?.id
const [spoilerExpanded, setSpoilerExpanded] = useState(
instanceAccount.preferences['reading:expand:spoilers'] || false
@ -91,7 +91,8 @@ const TimelineNotifications: React.FC<Props> = ({
notification.type === 'follow' ||
notification.type === 'follow_request' ||
notification.type === 'mention' ||
notification.type === 'status'
notification.type === 'status' ||
notification.type === 'admin.sign_up'
? 1
: 0.5
}}
@ -102,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>

View File

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

View File

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

View File

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

View File

@ -1,9 +1,10 @@
import GracefullyImage from '@components/GracefullyImage'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { View } from 'react-native'
import AttachmentAltText from './AltText'
import attachmentAspectRatio from './aspectRatio'
import { aspectRatio } from './dimensions'
export interface Props {
total: number
@ -20,6 +21,8 @@ const AttachmentImage = ({
image,
navigateToImagesViewer
}: Props) => {
const { colors } = useTheme()
return (
<View
style={{
@ -28,21 +31,16 @@ const AttachmentImage = ({
padding: StyleConstants.Spacing.XS / 2
}}
>
<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>
)

View File

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

View File

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

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

View File

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

View File

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

View File

@ -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": {

View File

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

View File

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

View File

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

View File

@ -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>
)}
</>
)
}

View File

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

View File

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

View File

@ -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'),
[
{

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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