Merge branch 'main' into candidate

This commit is contained in:
xmflsct 2022-12-18 23:33:16 +01:00
commit 171cfd0ead
53 changed files with 500 additions and 529 deletions

View File

@ -0,0 +1 @@
../../it/description.txt

View File

@ -0,0 +1 @@
../../it/subtitle.txt

View File

@ -1,5 +1,10 @@
tooot is an open source, simple yet elegant Mastodon mobile client.
tooot is an open source, simple yet elegant Mastodon mobile client. A Mastodon (https://joinmastodon.org/) account is required to use this app.
A Mastodon (https://joinmastodon.org/) account is required to use this app.
tooot supports:
- Cross platform, including iPadOS and MacOS
- Multiple accounts
- Dark mode or adapt to system
- Adjustable toot font size
- Push notification
If you have suggestions, please reach out to @tooot@xmflsct.com or support@tooot.ap.
If you have suggestions, please reach out to @tooot@xmflsct.com or support@tooot.app.

View File

@ -1 +1,4 @@
Enjoy toooting! This version includes following improvements and fixes:
- Align filter experience with v4.0 and above
- Supports enlarging user's avatar and banner
- Fix iPad weird sizing (not optimisation)

View File

@ -0,0 +1,10 @@
tooot è un client Mastodon semplice e open source. Per utilizzare questo client, devi disporre di un account Mastodon. (https://joinmastodon.org/).
Tooot supporta:
- Multipiattaforma, inclusi iPadOS e MacOS
- Accesso a più account
- Modalità scura o adattiva
- Dimensione del carattere del testo regolabile
- Notifiche push e altre funzioni
Per suggerimenti o commenti sull'utilizzo, contattare @tooot@xmflsct.com o support@tooot.app.

View File

@ -0,0 +1 @@
Client open source per Mastodon

View File

@ -1,11 +1,10 @@
tooot是一个专门为中文用户社区所打造的开源、简洁长毛象客户端。使用此客户端需要已经拥有一个长毛象https://joinmastodon.org/)账号。
tooot起始于专注中文社区的简洁、开源长毛象手机客户端。使用此客户端需要已经拥有一个长毛象https://joinmastodon.org/)账号。
tooot支持
- iPad
- 跨平台,及iPadOS、MacOS
- 多账号登录
- 黑暗或自适应模式
- 可调整正文字体大小
- 可调正文字体尺寸
- 消息推送
等功能。
如有使用建议或意见,请联系@tooot@xmflsct.com或者support@tooot.app。

View File

@ -1 +1,4 @@
toooting愉快此版本包括以下改进和修复
- 改进过滤体验与v4.0以上版本一致
- 支持查看用户的头像和横幅图片
- 修复iPad部分尺寸问题非优化

View File

@ -263,7 +263,8 @@ declare namespace Mastodon {
verified_at: string | null
}
type Filter = {
type Filter<T extends 'v1' | 'v2'> = T extends 'v2' ? Filter_V2 : Filter_V1
type Filter_V1 = {
id: string
phrase: string
context: ('home' | 'notifications' | 'public' | 'thread' | 'account')[]
@ -271,6 +272,25 @@ declare namespace Mastodon {
irreversible: boolean
whole_word: boolean
}
type Filter_V2 = {
id: string
title: string
context: ('home' | 'notifications' | 'public' | 'thread' | 'account')[]
expires_at?: string
filter_action: 'warn' | 'hide'
keywords: FilterKeyword[]
statuses: FilterStatus[]
}
type FilterKeyword = { id: string; keyword: string; whole_word: boolean }
type FilterStatus = { id: string; status_id: string }
type FilterResult = {
filter: Filter<'v2'>
keyword_matches?: FilterKeyword['keyword'][]
status_matches?: FilterStatus['id'][]
}
type List = {
id: string
@ -461,7 +481,7 @@ declare namespace Mastodon {
sensitive: boolean
spoiler_text?: string
media_attachments: Attachment[]
application: Application
application?: Application
// Attributes
mentions: Mention[]
@ -472,7 +492,7 @@ declare namespace Mastodon {
reblogs_count: number
favourites_count: number
replies_count: number
edited_at?: string // FEATURE edit_post
edited_at?: string
favourited: boolean
reblogged: boolean
muted: boolean
@ -488,6 +508,7 @@ declare namespace Mastodon {
card?: Card
language?: string
text?: string
filtered?: FilterResult[]
}
type StatusHistory = {

View File

@ -98,19 +98,17 @@ const TimelineConversation: React.FC<Props> = ({ conversation, queryKey, highlig
</View>
{conversation.last_status ? (
<>
<View
style={{
paddingTop: highlighted ? StyleConstants.Spacing.S : 0,
paddingLeft: highlighted ? 0 : StyleConstants.Avatar.M + StyleConstants.Spacing.S
}}
>
<TimelineContent />
<TimelinePoll />
</View>
<View
style={{
paddingTop: highlighted ? StyleConstants.Spacing.S : 0,
paddingLeft: highlighted ? 0 : StyleConstants.Avatar.M + StyleConstants.Spacing.S
}}
>
<TimelineContent />
<TimelinePoll />
<TimelineActions />
</>
</View>
) : null}
</Pressable>
</StatusContext.Provider>

View File

@ -9,11 +9,12 @@ import TimelineCard from '@components/Timeline/Shared/Card'
import TimelineContent from '@components/Timeline/Shared/Content'
import TimelineHeaderDefault from '@components/Timeline/Shared/HeaderDefault'
import TimelinePoll from '@components/Timeline/Shared/Poll'
import removeHTML from '@helpers/removeHTML'
import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { TabLocalStackParamList } from '@utils/navigation/navigators'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { getInstanceAccount } from '@utils/slices/instancesSlice'
import { checkInstanceFeature, getInstanceAccount } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useRef, useState } from 'react'
@ -22,7 +23,7 @@ import { useSelector } from 'react-redux'
import * as ContextMenu from 'zeego/context-menu'
import StatusContext from './Shared/Context'
import TimelineFeedback from './Shared/Feedback'
import TimelineFiltered, { shouldFilter } from './Shared/Filtered'
import TimelineFiltered, { FilteredProps, shouldFilter } from './Shared/Filtered'
import TimelineFullConversation from './Shared/FullConversation'
import TimelineHeaderAndroid from './Shared/HeaderAndroid'
import TimelineTranslate from './Shared/Translate'
@ -47,12 +48,20 @@ const TimelineDefault: React.FC<Props> = ({
disableOnPress = false,
isConversation = false
}) => {
const status = item.reblog ? item.reblog : item
const rawContent = useRef<string[]>([])
if (highlighted) {
rawContent.current = [
removeHTML(status.content),
status.spoiler_text ? removeHTML(status.spoiler_text) : ''
].filter(c => c.length)
}
const { colors } = useTheme()
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const instanceAccount = useSelector(getInstanceAccount, () => true)
const status = item.reblog ? item.reblog : item
const ownAccount = status.account?.id === instanceAccount?.id
const [spoilerExpanded, setSpoilerExpanded] = useState(
instanceAccount?.preferences?.['reading:expand:spoilers'] || false
@ -60,17 +69,8 @@ const TimelineDefault: React.FC<Props> = ({
const spoilerHidden = status.spoiler_text?.length
? !instanceAccount?.preferences?.['reading:expand:spoilers'] && !spoilerExpanded
: false
const copiableContent = useRef<{ content: string; complete: boolean }>({
content: '',
complete: false
})
const detectedLanguage = useRef<string>(status.language || '')
const filtered = queryKey && shouldFilter({ copiableContent, status, queryKey })
if (queryKey && filtered && !highlighted) {
return <TimelineFiltered phrase={filtered} />
}
const mainStyle: StyleProp<ViewStyle> = {
flex: 1,
padding: disableDetails
@ -103,8 +103,9 @@ const TimelineDefault: React.FC<Props> = ({
paddingTop: highlighted ? StyleConstants.Spacing.S : 0,
paddingLeft: highlighted
? 0
: (disableDetails ? StyleConstants.Avatar.XS : StyleConstants.Avatar.M) +
StyleConstants.Spacing.S,
: (disableDetails || isConversation
? StyleConstants.Avatar.XS
: StyleConstants.Avatar.M) + StyleConstants.Spacing.S,
...(disableDetails && { marginTop: -StyleConstants.Spacing.S })
}}
>
@ -115,9 +116,9 @@ const TimelineDefault: React.FC<Props> = ({
<TimelineFullConversation />
<TimelineTranslate />
<TimelineFeedback />
</View>
<TimelineActions />
<TimelineActions />
</View>
</>
)
@ -125,11 +126,36 @@ const TimelineDefault: React.FC<Props> = ({
visibility: status.visibility,
type: 'status',
url: status.url || status.uri,
copiableContent
rawContent
})
const mStatus = menuStatus({ status, queryKey, rootQueryKey })
const mInstance = menuInstance({ status, queryKey, rootQueryKey })
if (!ownAccount) {
let filterResults: FilteredProps['filterResults'] = []
const [filterRevealed, setFilterRevealed] = useState(false)
const hasFilterServerSide = useSelector(checkInstanceFeature('filter_server_side'))
if (hasFilterServerSide) {
if (status.filtered?.length) {
filterResults = status.filtered?.map(filter => filter.filter)
}
} else {
if (queryKey) {
const checkFilter = shouldFilter({ queryKey, status })
if (checkFilter?.length) {
filterResults = checkFilter
}
}
}
if (queryKey && !highlighted && filterResults?.length && !filterRevealed) {
return !filterResults.filter(result => result.filter_action === 'hide').length ? (
<Pressable onPress={() => setFilterRevealed(!filterRevealed)}>
<TimelineFiltered filterResults={filterResults} />
</Pressable>
) : null
}
}
return (
<StatusContext.Provider
value={{
@ -139,7 +165,7 @@ const TimelineDefault: React.FC<Props> = ({
reblogStatus: item.reblog ? item : undefined,
ownAccount,
spoilerHidden,
copiableContent,
rawContent,
detectedLanguage,
highlighted,
inThread: queryKey?.[1].page === 'Toot',

View File

@ -13,7 +13,7 @@ import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { TabLocalStackParamList } from '@utils/navigation/navigators'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { getInstanceAccount } from '@utils/slices/instancesSlice'
import { checkInstanceFeature, getInstanceAccount } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useRef, useState } from 'react'
@ -21,7 +21,7 @@ import { Pressable, View } from 'react-native'
import { useSelector } from 'react-redux'
import * as ContextMenu from 'zeego/context-menu'
import StatusContext from './Shared/Context'
import TimelineFiltered, { shouldFilter } from './Shared/Filtered'
import TimelineFiltered, { FilteredProps, shouldFilter } from './Shared/Filtered'
import TimelineFullConversation from './Shared/FullConversation'
import TimelineHeaderAndroid from './Shared/HeaderAndroid'
@ -47,21 +47,6 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
const spoilerHidden = notification.status?.spoiler_text?.length
? !instanceAccount.preferences?.['reading:expand:spoilers'] && !spoilerExpanded
: false
const copiableContent = useRef<{ content: string; complete: boolean }>({
content: '',
complete: false
})
const filtered =
notification.status &&
shouldFilter({
copiableContent,
status: notification.status,
queryKey
})
if (notification.status && filtered) {
return <TimelineFiltered phrase={filtered} />
}
const { colors } = useTheme()
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
@ -112,11 +97,11 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
<TimelineAttachment />
<TimelineCard />
<TimelineFullConversation />
<TimelineActions />
</View>
) : null}
</View>
<TimelineActions />
</>
)
}
@ -124,20 +109,44 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
const mShare = menuShare({
visibility: notification.status?.visibility,
type: 'status',
url: notification.status?.url || notification.status?.uri,
copiableContent
url: notification.status?.url || notification.status?.uri
})
const mStatus = menuStatus({ status: notification.status, queryKey })
const mInstance = menuInstance({ status: notification.status, queryKey })
if (!ownAccount) {
let filterResults: FilteredProps['filterResults'] = []
const [filterRevealed, setFilterRevealed] = useState(false)
const hasFilterServerSide = useSelector(checkInstanceFeature('filter_server_side'))
if (notification.status) {
if (hasFilterServerSide) {
if (notification.status.filtered?.length) {
filterResults = notification.status.filtered.map(filter => filter.filter)
}
} else {
const checkFilter = shouldFilter({ queryKey, status: notification.status })
if (checkFilter?.length) {
filterResults = checkFilter
}
}
if (filterResults?.length && !filterRevealed) {
return !filterResults.filter(result => result.filter_action === 'hide').length ? (
<Pressable onPress={() => setFilterRevealed(!filterRevealed)}>
<TimelineFiltered filterResults={filterResults} />
</Pressable>
) : null
}
}
}
return (
<StatusContext.Provider
value={{
queryKey,
status,
ownAccount,
spoilerHidden,
copiableContent
spoilerHidden
}}
>
<ContextMenu.Root>

View File

@ -55,7 +55,7 @@ const TimelineRefresh: React.FC<Props> = ({
firstPage?.links?.prev && {
...(firstPage.links.prev.isOffset
? { offset: firstPage.links.prev.id }
: { max_id: firstPage.links.prev.id }),
: { min_id: firstPage.links.prev.id }),
// https://github.com/facebook/react-native/issues/25239#issuecomment-731100372
limit: '3'
},

View File

@ -263,63 +263,57 @@ const TimelineActions: React.FC = () => {
}, [status.bookmarked])
return (
<View
style={{
paddingLeft: highlighted ? 0 : StyleConstants.Avatar.M + StyleConstants.Spacing.S
}}
>
<View style={{ flexDirection: 'row' }}>
<Pressable
{...(highlighted
? {
accessibilityLabel: t('shared.actions.reply.accessibilityLabel'),
accessibilityRole: 'button'
}
: { accessibilityLabel: '' })}
style={styles.action}
onPress={onPressReply}
children={childrenReply}
/>
<View style={{ flexDirection: 'row' }}>
<Pressable
{...(highlighted
? {
accessibilityLabel: t('shared.actions.reply.accessibilityLabel'),
accessibilityRole: 'button'
}
: { accessibilityLabel: '' })}
style={styles.action}
onPress={onPressReply}
children={childrenReply}
/>
<Pressable
{...(highlighted
? {
accessibilityLabel: t('shared.actions.reblogged.accessibilityLabel'),
accessibilityRole: 'button'
}
: { accessibilityLabel: '' })}
style={styles.action}
onPress={onPressReblog}
children={childrenReblog}
disabled={
status.visibility === 'direct' || (status.visibility === 'private' && !ownAccount)
}
/>
<Pressable
{...(highlighted
? {
accessibilityLabel: t('shared.actions.reblogged.accessibilityLabel'),
accessibilityRole: 'button'
}
: { accessibilityLabel: '' })}
style={styles.action}
onPress={onPressReblog}
children={childrenReblog}
disabled={
status.visibility === 'direct' || (status.visibility === 'private' && !ownAccount)
}
/>
<Pressable
{...(highlighted
? {
accessibilityLabel: t('shared.actions.favourited.accessibilityLabel'),
accessibilityRole: 'button'
}
: { accessibilityLabel: '' })}
style={styles.action}
onPress={onPressFavourite}
children={childrenFavourite}
/>
<Pressable
{...(highlighted
? {
accessibilityLabel: t('shared.actions.favourited.accessibilityLabel'),
accessibilityRole: 'button'
}
: { accessibilityLabel: '' })}
style={styles.action}
onPress={onPressFavourite}
children={childrenFavourite}
/>
<Pressable
{...(highlighted
? {
accessibilityLabel: t('shared.actions.bookmarked.accessibilityLabel'),
accessibilityRole: 'button'
}
: { accessibilityLabel: '' })}
style={styles.action}
onPress={onPressBookmark}
children={childrenBookmark}
/>
</View>
<Pressable
{...(highlighted
? {
accessibilityLabel: t('shared.actions.bookmarked.accessibilityLabel'),
accessibilityRole: 'button'
}
: { accessibilityLabel: '' })}
style={styles.action}
onPress={onPressBookmark}
children={childrenBookmark}
/>
</View>
)
}

View File

@ -70,7 +70,6 @@ const TimelineAttachment = () => {
preview_url: attachment.preview_url,
url: attachment.url,
remote_url: attachment.remote_url,
blurhash: attachment.blurhash,
width: attachment.meta?.original?.width,
height: attachment.meta?.original?.height
}
@ -90,7 +89,6 @@ const TimelineAttachment = () => {
preview_url: attachment.preview_url,
url: attachment.url,
remote_url: attachment.remote_url,
blurhash: attachment.blurhash,
width: attachment.meta?.original?.width,
height: attachment.meta?.original?.height
}

View File

@ -48,8 +48,7 @@ const TimelineAvatar: React.FC<Props> = ({ account }) => {
style={{
borderRadius: StyleConstants.Avatar.M,
overflow: 'hidden',
marginRight: StyleConstants.Spacing.S,
marginLeft: isConversation ? StyleConstants.Avatar.M - StyleConstants.Avatar.XS : undefined
marginRight: StyleConstants.Spacing.S
}}
/>
)

View File

@ -1,7 +1,9 @@
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { createContext } from 'react'
type ContextType = {
export type HighlightedStatusContextType = {}
type StatusContextType = {
queryKey?: QueryKeyTimeline
rootQueryKey?: QueryKeyTimeline
@ -10,10 +12,7 @@ type ContextType = {
reblogStatus?: Mastodon.Status // When it is a reblog, pass the root status
ownAccount?: boolean
spoilerHidden?: boolean
copiableContent?: React.MutableRefObject<{
content: string
complete: boolean
}>
rawContent?: React.MutableRefObject<string[]> // When highlighted, for translate, edit history
detectedLanguage?: React.MutableRefObject<string>
highlighted?: boolean
@ -22,6 +21,6 @@ type ContextType = {
disableOnPress?: boolean
isConversation?: boolean
}
const StatusContext = createContext<ContextType>({} as ContextType)
const StatusContext = createContext<StatusContextType>({} as StatusContextType)
export default StatusContext

View File

@ -1,129 +1,133 @@
import CustomText from '@components/Text'
import removeHTML from '@helpers/removeHTML'
import { store } from '@root/store'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { getInstance, getInstanceAccount } from '@utils/slices/instancesSlice'
import { getInstance } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import htmlparser2 from 'htmlparser2-without-node-native'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { View } from 'react-native'
const TimelineFiltered = React.memo(
({ phrase }: { phrase: string }) => {
const { colors } = useTheme()
const { t } = useTranslation('componentTimeline')
export interface FilteredProps {
filterResults: { title: string; filter_action: Mastodon.Filter<'v2'>['filter_action'] }[]
}
return (
<View style={{ backgroundColor: colors.backgroundDefault }}>
<CustomText
fontStyle='S'
style={{
color: colors.secondary,
textAlign: 'center',
paddingVertical: StyleConstants.Spacing.S,
paddingLeft: StyleConstants.Avatar.M + StyleConstants.Spacing.S
}}
>
{t('shared.filtered', { phrase })}
</CustomText>
</View>
)
},
() => true
)
const TimelineFiltered: React.FC<FilteredProps> = ({ filterResults }) => {
const { colors } = useTheme()
const { t } = useTranslation('componentTimeline')
export const shouldFilter = ({
copiableContent,
status,
queryKey
}: {
copiableContent: React.MutableRefObject<{
content: string
complete: boolean
}>
status: Mastodon.Status
queryKey: QueryKeyTimeline
}): string | null => {
const page = queryKey[1]
const instance = getInstance(store.getState())
const ownAccount = getInstanceAccount(store.getState())?.id === status.account?.id
let shouldFilter: string | null = null
if (!ownAccount) {
let rawContent = ''
const parser = new htmlparser2.Parser({
ontext: (text: string) => {
if (!copiableContent.current.complete) {
copiableContent.current.content = copiableContent.current.content + text
}
rawContent = rawContent + text
}
})
if (status.spoiler_text) {
parser.write(status.spoiler_text)
rawContent = rawContent + `\n\n`
const main = () => {
if (!filterResults?.length) {
return <></>
}
parser.write(status.content)
parser.end()
const checkFilter = (filter: Mastodon.Filter) => {
const escapedPhrase = filter.phrase.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
switch (filter.whole_word) {
case true:
if (new RegExp(`\\b${escapedPhrase}\\b`, 'i').test(rawContent)) {
shouldFilter = filter.phrase
}
break
case false:
if (new RegExp(escapedPhrase, 'i').test(rawContent)) {
shouldFilter = filter.phrase
}
break
}
switch (typeof filterResults[0]) {
case 'string': // v1 filter
return <>{t('shared.filtered.match', { context: 'v1', phrase: filterResults[0] })}</>
default:
return (
<>
{t('shared.filtered.match', {
context: 'v2',
count: filterResults.length,
filters: filterResults.map(result => result.title).join(t('common:separator'))
})}
<CustomText
style={{ color: colors.blue }}
children={`\n${t('shared.filtered.reveal')}`}
/>
</>
)
}
instance?.filters?.forEach(filter => {
if (shouldFilter) {
return
}
if (filter.expires_at) {
if (new Date().getTime() > new Date(filter.expires_at).getTime()) {
return
}
}
switch (page.page) {
case 'Following':
case 'Local':
case 'List':
case 'Account':
if (filter.context.includes('home')) {
checkFilter(filter)
}
break
case 'Notifications':
if (filter.context.includes('notifications')) {
checkFilter(filter)
}
break
case 'LocalPublic':
if (filter.context.includes('public')) {
checkFilter(filter)
}
break
case 'Toot':
if (filter.context.includes('thread')) {
checkFilter(filter)
}
}
})
copiableContent.current.complete = true
}
return shouldFilter
return (
<View style={{ backgroundColor: colors.backgroundDefault }}>
<CustomText
fontStyle='S'
style={{
color: colors.secondary,
textAlign: 'center',
paddingVertical: StyleConstants.Spacing.S,
paddingLeft: StyleConstants.Avatar.M + StyleConstants.Spacing.S
}}
>
{main()}
</CustomText>
</View>
)
}
export const shouldFilter = ({
queryKey,
status
}: {
queryKey: QueryKeyTimeline
status: Pick<Mastodon.Status, 'content' | 'spoiler_text'>
}): FilteredProps['filterResults'] | undefined => {
const page = queryKey[1]
const instance = getInstance(store.getState())
let returnFilter: FilteredProps['filterResults'] | undefined
const rawContentCombined = [
removeHTML(status.content),
status.spoiler_text ? removeHTML(status.spoiler_text) : ''
]
.filter(c => c.length)
.join(`\n`)
const checkFilter = (filter: Mastodon.Filter<'v1'>) => {
const escapedPhrase = filter.phrase.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
switch (filter.whole_word) {
case true:
if (new RegExp(`\\b${escapedPhrase}\\b`, 'i').test(rawContentCombined)) {
returnFilter = [{ title: filter.phrase, filter_action: 'warn' }]
}
break
case false:
if (new RegExp(escapedPhrase, 'i').test(rawContentCombined)) {
returnFilter = [{ title: filter.phrase, filter_action: 'warn' }]
}
break
}
}
instance?.filters?.forEach(filter => {
if (returnFilter) {
return
}
if (filter.expires_at) {
if (new Date().getTime() > new Date(filter.expires_at).getTime()) {
return
}
}
switch (page.page) {
case 'Following':
case 'Local':
case 'List':
case 'Account':
if (filter.context.includes('home')) {
checkFilter(filter as Mastodon.Filter<'v1'>)
}
break
case 'Notifications':
if (filter.context.includes('notifications')) {
checkFilter(filter as Mastodon.Filter<'v1'>)
}
break
case 'LocalPublic':
if (filter.context.includes('public')) {
checkFilter(filter as Mastodon.Filter<'v1'>)
}
break
case 'Toot':
if (filter.context.includes('thread')) {
checkFilter(filter as Mastodon.Filter<'v1'>)
}
}
})
return returnFilter
}
export default TimelineFiltered

View File

@ -10,7 +10,7 @@ import * as DropdownMenu from 'zeego/dropdown-menu'
import StatusContext from './Context'
const TimelineHeaderAndroid: React.FC = () => {
const { queryKey, rootQueryKey, status, disableDetails, disableOnPress } =
const { queryKey, rootQueryKey, status, disableDetails, disableOnPress, rawContent } =
useContext(StatusContext)
if (Platform.OS !== 'android' || !status || disableDetails || disableOnPress) return null
@ -21,7 +21,8 @@ const TimelineHeaderAndroid: React.FC = () => {
const mShare = menuShare({
visibility: status.visibility,
type: 'status',
url: status.url || status.uri
url: status.url || status.uri,
rawContent
})
const mAccount = menuAccount({
type: 'status',

View File

@ -16,7 +16,7 @@ import HeaderSharedMuted from './HeaderShared/Muted'
import HeaderSharedVisibility from './HeaderShared/Visibility'
const TimelineHeaderDefault: React.FC = () => {
const { queryKey, rootQueryKey, status, copiableContent, highlighted, disableDetails } =
const { queryKey, rootQueryKey, status, highlighted, disableDetails, rawContent } =
useContext(StatusContext)
if (!status) return null
@ -28,7 +28,7 @@ const TimelineHeaderDefault: React.FC = () => {
visibility: status.visibility,
type: 'status',
url: status.url || status.uri,
copiableContent
rawContent
})
const mAccount = menuAccount({
type: 'status',

View File

@ -13,38 +13,19 @@ import { Circle } from 'react-native-animated-spinkit'
import StatusContext from './Context'
const TimelineTranslate = () => {
const { status, highlighted, copiableContent, detectedLanguage } = useContext(StatusContext)
if (!status || !highlighted) return null
const { status, highlighted, rawContent, detectedLanguage } = useContext(StatusContext)
if (!status || !highlighted || !rawContent?.current.length) return null
const { t } = useTranslation('componentTimeline')
const { colors } = useTheme()
const backupTextProcessing = (): string[] => {
const text = status.spoiler_text ? [status.spoiler_text, status.content] : [status.content]
for (const i in text) {
for (const emoji of status.emojis) {
text[i] = text[i].replaceAll(`:${emoji.shortcode}:`, ' ')
}
text[i] = text[i]
.replace(/(<([^>]+)>)/gi, ' ')
.replace(/@.*? /gi, ' ')
.replace(/#.*? /gi, ' ')
.replace(/http(s):\/\/.*? /gi, ' ')
}
return text
}
const text = copiableContent?.current.content
? [copiableContent?.current.content]
: backupTextProcessing()
const [detected, setDetected] = useState<{
language: string
confidence: number
}>({ language: status.language || '', confidence: 0 })
useEffect(() => {
const detect = async () => {
const result = await detectLanguage(text.join('\n\n'))
const result = await detectLanguage(rawContent.current.join('\n\n'))
if (result) {
setDetected(result)
if (detectedLanguage) {
@ -64,7 +45,7 @@ const TimelineTranslate = () => {
const { refetch, data, isFetching, isSuccess, isError } = useTranslateQuery({
source: detected.language,
target: targetLanguage,
text,
text: rawContent.current,
options: { enabled }
})

View File

@ -7,10 +7,7 @@ const menuShare = (
params:
| {
visibility?: Mastodon.Status['visibility']
copiableContent?: React.MutableRefObject<{
content?: string | undefined
complete: boolean
}>
rawContent?: React.MutableRefObject<string[]>
type: 'status'
url?: string
}
@ -48,17 +45,17 @@ const menuShare = (
icon: 'square.and.arrow.up'
})
}
if (params.type === 'status' && Platform.OS === 'ios')
if (params.type === 'status')
menus[0].push({
key: 'copy',
item: {
onSelect: () => {
Clipboard.setString(params.copiableContent?.current.content || '')
Clipboard.setString(params.rawContent?.current.join(`\n\n`) || '')
displayMessage({ type: 'success', message: t(`copy.succeed`) })
},
disabled: false,
destructive: false,
hidden: !params.copiableContent?.current.content?.length
hidden: !params.rawContent?.current.length
},
title: t('copy.action'),
icon: 'doc.on.doc'

View File

@ -42,5 +42,9 @@
{
"feature": "notification_type_admin_report",
"version": 4.0
},
{
"feature": "filter_server_side",
"version": 4.0
}
]

View File

@ -6,6 +6,9 @@ const removeHTML = (text: string): string => {
const parser = new htmlparser2.Parser({
ontext: (text: string) => {
raw = raw + text
},
onclosetag: (tag: string) => {
if (['p', 'br'].includes(tag)) raw = raw + `\n`
}
})

View File

@ -91,7 +91,12 @@
"content": {
"expandHint": "Hidden content"
},
"filtered": "Filtered: {{phrase}}.",
"filtered": {
"reveal": "Show anyway",
"match_v1": "Filtered: {{phrase}}.",
"match_v2_one": "Filtered by {{filters}}.",
"match_v2_other": "Filtered by {{count}} filters, {{filters}}."
},
"fullConversation": "Read conversations",
"translate": {
"default": "Translate",

View File

@ -24,7 +24,7 @@ const ScreenActions = ({
const insets = useSafeAreaInsets()
const DEFAULT_VALUE = 350
const screenHeight = Dimensions.get('screen').height
const screenHeight = Dimensions.get('window').height
const panY = useSharedValue(DEFAULT_VALUE)
useEffect(() => {
panY.value = withTiming(0)

View File

@ -61,7 +61,7 @@ const ScreenAnnouncements: React.FC<RootStackScreenProps<'Screen-Announcements'>
<View
key={index}
style={{
width: Dimensions.get('screen').width,
width: Dimensions.get('window').width,
padding: StyleConstants.Spacing.Global.PagePadding,
marginVertical: StyleConstants.Spacing.Global.PagePadding,
justifyContent: 'center'
@ -200,7 +200,7 @@ const ScreenAnnouncements: React.FC<RootStackScreenProps<'Screen-Announcements'>
return (
<View
style={{
width: Dimensions.get('screen').width,
width: Dimensions.get('window').width,
justifyContent: 'center',
alignItems: 'center'
}}

View File

@ -142,12 +142,12 @@ const ComposeDraftsList: React.FC<ScreenComposeStackScreenProps<'Screen-Compose-
key={index}
style={{
width:
(Dimensions.get('screen').width -
(Dimensions.get('window').width -
StyleConstants.Spacing.Global.PagePadding * 2 -
StyleConstants.Spacing.S * 3) /
4,
height:
(Dimensions.get('screen').width -
(Dimensions.get('window').width -
StyleConstants.Spacing.Global.PagePadding * 2 -
StyleConstants.Spacing.S * 3) /
4,

View File

@ -33,7 +33,6 @@ const ComposeTextInput: React.FC = () => {
paddingBottom: StyleConstants.Spacing.M,
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
color: colors.primaryDefault,
borderBottomColor: colors.border,
fontSize: adaptedFontsize,
lineHeight: adaptedLineheight
}}

View File

@ -25,7 +25,7 @@ const ZoomFlatList = createZoomListComponent(FlatList)
const ScreenImagesViewer = ({
route: {
params: { imageUrls, id }
params: { imageUrls, id, hideCounter }
},
navigation
}: RootStackScreenProps<'Screen-ImagesViewer'>) => {
@ -34,8 +34,8 @@ const ScreenImagesViewer = ({
return null
}
const SCREEN_WIDTH = Dimensions.get('screen').width
const SCREEN_HEIGHT = Dimensions.get('screen').height
const WINDOW_WIDTH = Dimensions.get('window').width
const WINDOW_HEIGHT = Dimensions.get('window').height
const insets = useSafeAreaInsets()
@ -85,13 +85,13 @@ const ScreenImagesViewer = ({
}: {
item: RootStackScreenProps<'Screen-ImagesViewer'>['route']['params']['imageUrls'][0]
}) => {
const screenRatio = SCREEN_WIDTH / SCREEN_HEIGHT
const screenRatio = WINDOW_WIDTH / WINDOW_HEIGHT
const imageRatio = item.width && item.height ? item.width / item.height : 1
const imageWidth = item.width || 100
const imageHeight = item.height || 100
const maxWidthScale = item.width ? (item.width / SCREEN_WIDTH / PixelRatio.get()) * 4 : 0
const maxHeightScale = item.height ? (item.height / SCREEN_WIDTH / PixelRatio.get()) * 4 : 0
const maxWidthScale = item.width ? (item.width / WINDOW_WIDTH / PixelRatio.get()) * 4 : 0
const maxHeightScale = item.height ? (item.height / WINDOW_WIDTH / PixelRatio.get()) * 4 : 0
const max = Math.max.apply(Math, [maxWidthScale, maxHeightScale, 4])
return (
@ -109,8 +109,8 @@ const ScreenImagesViewer = ({
children={
<View
style={{
width: SCREEN_WIDTH,
height: SCREEN_HEIGHT,
width: WINDOW_WIDTH,
height: WINDOW_HEIGHT,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center'
@ -121,12 +121,12 @@ const ScreenImagesViewer = ({
dimension={{
width:
screenRatio > imageRatio
? (SCREEN_HEIGHT / imageHeight) * imageWidth
: SCREEN_WIDTH,
? (WINDOW_HEIGHT / imageHeight) * imageWidth
: WINDOW_WIDTH,
height:
screenRatio > imageRatio
? SCREEN_HEIGHT
: (SCREEN_WIDTH / imageWidth) * imageHeight
? WINDOW_HEIGHT
: (WINDOW_WIDTH / imageWidth) * imageHeight
}}
/>
</View>
@ -159,7 +159,9 @@ const ScreenImagesViewer = ({
}}
>
<HeaderLeft content='X' native={false} background onPress={() => navigation.goBack()} />
<HeaderCenter inverted content={`${currentIndex + 1} / ${imageUrls.length}`} />
{!hideCounter ? (
<HeaderCenter inverted content={`${currentIndex + 1} / ${imageUrls.length}`} />
) : null}
<HeaderRight
accessibilityLabel={t('content.actions.accessibilityLabel')}
accessibilityHint={t('content.actions.accessibilityHint')}
@ -215,8 +217,8 @@ const ScreenImagesViewer = ({
}}
initialScrollIndex={initialIndex}
getItemLayout={(_, index) => ({
length: SCREEN_WIDTH,
offset: SCREEN_WIDTH * index,
length: WINDOW_WIDTH,
offset: WINDOW_WIDTH * index,
index
})}
/>

View File

@ -46,18 +46,14 @@ const TabMePush: React.FC = () => {
useEffect(() => {
const checkPush = async () => {
switch (Platform.OS) {
case 'ios':
const settings = await Notifications.getPermissionsAsync()
layoutAnimation()
setPushEnabled(settings.granted)
setPushCanAskAgain(settings.canAskAgain)
break
case 'android':
await setChannels(instance)
layoutAnimation()
dispatch(retrieveExpoToken())
break
const permissions = await Notifications.getPermissionsAsync()
setPushEnabled(permissions.granted)
setPushCanAskAgain(permissions.canAskAgain)
layoutAnimation()
if (Platform.OS === 'android') {
await setChannels(instance)
dispatch(retrieveExpoToken())
}
}

View File

@ -5,11 +5,7 @@ import CustomText from '@components/Text'
import TimelineDefault from '@components/Timeline/Default'
import { useAppDispatch } from '@root/store'
import { TabMeStackScreenProps } from '@utils/navigation/navigators'
import {
changeFontsize,
getSettingsFontsize,
SettingsState
} from '@utils/slices/settingsSlice'
import { changeFontsize, getSettingsFontsize, SettingsState } from '@utils/slices/settingsSlice'
import { StyleConstants } from '@utils/styles/constants'
import { adaptiveScale } from '@utils/styles/scaling'
import { useTheme } from '@utils/styles/ThemeManager'
@ -34,9 +30,7 @@ export const mapFontsizeToName = (size: SettingsState['fontsize']) => {
}
}
const TabMeSettingsFontsize: React.FC<
TabMeStackScreenProps<'Tab-Me-Settings-Fontsize'>
> = () => {
const TabMeSettingsFontsize: React.FC<TabMeStackScreenProps<'Tab-Me-Settings-Fontsize'>> = () => {
const { colors, theme } = useTheme()
const { t } = useTranslation('screenTabs')
const initialSize = useSelector(getSettingsFontsize)
@ -86,8 +80,7 @@ const TabMeSettingsFontsize: React.FC<
marginBottom: StyleConstants.Spacing.M,
fontSize: adaptiveScale(StyleConstants.Font.Size.M, size),
lineHeight: adaptiveScale(StyleConstants.Font.LineHeight.M, size),
color:
initialSize === size ? colors.primaryDefault : colors.secondary,
color: initialSize === size ? colors.primaryDefault : colors.secondary,
borderWidth: StyleSheet.hairlineWidth,
borderColor: colors.border
}}

View File

@ -88,7 +88,7 @@ const Root: React.FC<NativeStackScreenProps<TabPublicStackParamList, 'Tab-Public
renderTabBar={() => null}
onIndexChange={index => setSegment(index)}
navigationState={{ index: segment, routes }}
initialLayout={{ width: Dimensions.get('screen').width }}
initialLayout={{ width: Dimensions.get('window').width }}
/>
)
}

View File

@ -4,16 +4,16 @@ import { HeaderLeft, HeaderRight } from '@components/Header'
import Timeline from '@components/Timeline'
import TimelineDefault from '@components/Timeline/Default'
import SegmentedControl from '@react-native-community/segmented-control'
import { useQueryClient } from '@tanstack/react-query'
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
import { useAccountQuery } from '@utils/queryHooks/account'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useEffect, useMemo, useRef, useState } from 'react'
import React, { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Text, View } from 'react-native'
import { useSharedValue } from 'react-native-reanimated'
import { useIsFetching } from '@tanstack/react-query'
import * as DropdownMenu from 'zeego/dropdown-menu'
import AccountAttachments from './Account/Attachments'
import AccountHeader from './Account/Header'
@ -87,35 +87,29 @@ const TabSharedAccount: React.FC<TabSharedStackScreenProps<'Tab-Shared-Account'>
const scrollY = useSharedValue(0)
const queryClient = useQueryClient()
const [queryKey, setQueryKey] = useState<QueryKeyTimeline>([
'Timeline',
{ page: 'Account', account: account.id, exclude_reblogs: true, only_media: false }
])
const page = queryKey[1]
const isFetchingTimeline = useIsFetching(queryKey)
const fetchedTimeline = useRef(false)
useEffect(() => {
if (!isFetchingTimeline && !fetchedTimeline.current) {
fetchedTimeline.current = true
}
}, [isFetchingTimeline, fetchedTimeline.current])
const [segment, setSegment] = useState<number>(0)
const ListHeaderComponent = useMemo(() => {
return (
<>
<View style={{ borderBottomWidth: 1, borderBottomColor: colors.border }}>
<AccountHeader account={data} />
<AccountInformation account={data} />
{!data?.suspended && fetchedTimeline.current ? (
<AccountAttachments account={data} />
) : null}
{!data?.suspended ? <AccountAttachments account={data} /> : null}
</View>
{!data?.suspended ? (
<SegmentedControl
appearance={mode}
values={[t('shared.account.toots.default'), t('shared.account.toots.all')]}
selectedIndex={page.page === 'Account' ? 0 : 1}
selectedIndex={segment}
onChange={({ nativeEvent }) => {
setSegment(nativeEvent.selectedSegmentIndex)
switch (nativeEvent.selectedSegmentIndex) {
case 0:
setQueryKey([
@ -171,7 +165,7 @@ const TabSharedAccount: React.FC<TabSharedStackScreenProps<'Tab-Shared-Account'>
) : null}
</>
)
}, [data, fetchedTimeline.current, queryKey[1].page, mode])
}, [segment, data, queryKey[1].page, mode])
return (
<>
@ -187,7 +181,9 @@ const TabSharedAccount: React.FC<TabSharedStackScreenProps<'Tab-Shared-Account'>
renderItem: ({ item }) => <TimelineDefault item={item} queryKey={queryKey} />,
onScroll: ({ nativeEvent }) => (scrollY.value = nativeEvent.contentOffset.y),
ListHeaderComponent,
maintainVisibleContentPosition: undefined
maintainVisibleContentPosition: undefined,
onRefresh: () => queryClient.refetchQueries(queryKey),
refreshing: queryClient.getQueryState(queryKey)?.fetchStatus === 'fetching'
}}
/>
)}

View File

@ -3,10 +3,10 @@ import Icon from '@components/Icon'
import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { TabLocalStackParamList } from '@utils/navigation/navigators'
import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
import { useTimelineQuery } from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useEffect } from 'react'
import React, { useCallback } from 'react'
import { Dimensions, ListRenderItem, Pressable, View } from 'react-native'
import { FlatList } from 'react-native-gesture-handler'
import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
@ -23,23 +23,14 @@ const AccountAttachments: React.FC<Props> = ({ account }) => {
const DISPLAY_AMOUNT = 6
const width = (Dimensions.get('screen').width - StyleConstants.Spacing.Global.PagePadding * 2) / 4
const width = (Dimensions.get('window').width - StyleConstants.Spacing.Global.PagePadding * 2) / 4
const queryKeyParams: QueryKeyTimeline[1] = {
const { data } = useTimelineQuery({
page: 'Account',
account: account.id,
exclude_reblogs: false,
only_media: true
}
const { data, refetch } = useTimelineQuery({
...queryKeyParams,
options: { enabled: false }
})
useEffect(() => {
if (account?.id) {
refetch()
}
}, [account])
const flattenData = data?.pages
? data.pages

View File

@ -1,47 +1,45 @@
import Button from '@components/Button'
import GracefullyImage from '@components/GracefullyImage'
import navigationRef from '@helpers/navigationRef'
import { getInstanceActive } from '@utils/slices/instancesSlice'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { Dimensions, View } from 'react-native'
import { Dimensions, Image, Pressable } from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { useSelector } from 'react-redux'
export interface Props {
account?: Mastodon.Account
edit?: boolean
}
const AccountHeader = React.memo(
({ account, edit }: Props) => {
const { colors } = useTheme()
const topInset = useSafeAreaInsets().top
const AccountHeader: React.FC<Props> = ({ account }) => {
const { colors } = useTheme()
const topInset = useSafeAreaInsets().top
return (
<View>
<GracefullyImage
uri={{ original: account?.header, static: account?.header_static }}
style={{
height: Dimensions.get('screen').width / 3 + topInset,
backgroundColor: colors.disabled
}}
/>
{edit ? (
<View
style={{
position: 'absolute',
width: '100%',
height: '100%',
alignContent: 'center',
justifyContent: 'center',
alignItems: 'center'
}}
>
<Button type='icon' content='Edit' round onPress={() => {}} />
</View>
) : null}
</View>
)
},
(_, next) => next.account === undefined
)
useSelector(getInstanceActive)
return (
<Pressable
onPress={() => {
if (account) {
Image.getSize(account.header, (width, height) =>
navigationRef.navigate('Screen-ImagesViewer', {
imageUrls: [{ id: 'avatar', url: account.header, width, height }],
id: 'avatar',
hideCounter: true
})
)
}
}}
>
<GracefullyImage
uri={{ original: account?.header, static: account?.header_static }}
style={{
height: Dimensions.get('window').width / 3 + topInset,
backgroundColor: colors.disabled
}}
/>
</Pressable>
)
}
export default AccountHeader

View File

@ -1,28 +1,37 @@
import Button from '@components/Button'
import GracefullyImage from '@components/GracefullyImage'
import navigationRef from '@helpers/navigationRef'
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, View } from 'react-native'
import { Pressable } from 'react-native'
import { useSelector } from 'react-redux'
export interface Props {
account: Mastodon.Account | undefined
myInfo: boolean
edit?: boolean
}
const AccountInformationAvatar: React.FC<Props> = ({ account, myInfo, edit }) => {
const AccountInformationAvatar: React.FC<Props> = ({ account, myInfo }) => {
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
useSelector(getInstanceActive)
return (
<Pressable
disabled={!myInfo}
onPress={() => {
myInfo && account && navigation.push('Tab-Shared-Account', { account })
if (account) {
if (myInfo) {
navigation.push('Tab-Shared-Account', { account })
return
} else {
navigationRef.navigate('Screen-ImagesViewer', {
imageUrls: [{ id: 'avatar', url: account.avatar }],
id: 'avatar',
hideCounter: true
})
}
}
}}
style={{
borderRadius: 8,
@ -36,20 +45,6 @@ const AccountInformationAvatar: React.FC<Props> = ({ account, myInfo, edit }) =>
style={{ flex: 1 }}
uri={{ original: account?.avatar, static: account?.avatar_static }}
/>
{edit ? (
<View
style={{
position: 'absolute',
width: '100%',
height: '100%',
alignContent: 'center',
justifyContent: 'center',
alignItems: 'center'
}}
>
<Button type='icon' content='Edit' round onPress={() => {}} />
</View>
) : null}
</Pressable>
)
}

View File

@ -4,11 +4,7 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { Dimensions, StyleSheet, View } from 'react-native'
import Animated, {
Extrapolate,
interpolate,
useAnimatedStyle
} from 'react-native-reanimated'
import Animated, { Extrapolate, interpolate, useAnimatedStyle } from 'react-native-reanimated'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
export interface Props {
@ -22,7 +18,7 @@ const AccountNav = React.memo(
const headerHeight = useSafeAreaInsets().top + 44
const nameY =
Dimensions.get('screen').width / 3 +
Dimensions.get('window').width / 3 +
StyleConstants.Avatar.L -
StyleConstants.Spacing.Global.PagePadding * 2 +
StyleConstants.Spacing.M -
@ -35,12 +31,7 @@ const AccountNav = React.memo(
})
const styleMarginTop = useAnimatedStyle(() => {
return {
marginTop: interpolate(
scrollY.value,
[nameY, nameY + 20],
[50, 0],
Extrapolate.CLAMP
)
marginTop: interpolate(scrollY.value, [nameY, nameY + 20], [50, 0], Extrapolate.CLAMP)
}
})
@ -61,8 +52,7 @@ const AccountNav = React.memo(
flex: 1,
alignItems: 'center',
overflow: 'hidden',
marginTop:
useSafeAreaInsets().top + (44 - StyleConstants.Font.Size.L) / 2
marginTop: useSafeAreaInsets().top + (44 - StyleConstants.Font.Size.L) / 2
}}
>
<Animated.View style={[{ flexDirection: 'row' }, styleMarginTop]}>

View File

@ -5,8 +5,10 @@ import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import React, { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { FlatList } from 'react-native'
import { FlatList, View } from 'react-native'
import { InfiniteQueryObserver, useQueryClient } from '@tanstack/react-query'
import ComponentSeparator from '@components/Separator'
import { StyleConstants } from '@utils/styles/constants'
const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
navigation,
@ -98,94 +100,49 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
queryKey={queryKey}
queryOptions={{ staleTime: 0, refetchOnMount: true }}
customProps={{
renderItem: ({ item }) => {
ItemSeparatorComponent: ({ leadingItem }) => {
const levels = {
current:
replyLevels.current.find(reply => reply.id === leadingItem.in_reply_to_id)?.level || 0
}
return (
<TimelineDefault
item={item}
queryKey={queryKey}
rootQueryKey={rootQueryKey}
highlighted={toot.id === item.id}
<ComponentSeparator
extraMarginLeft={
toot.id === leadingItem.id
? 0
: StyleConstants.Avatar.XS +
StyleConstants.Spacing.S +
Math.max(0, levels.current - 1) * 8
}
/>
)
},
// renderItem: ({ item, index }) => {
// const levels = {
// previous:
// replyLevels.current.find(
// reply => reply.id === data.current?.[index - 1]?.in_reply_to_id
// )?.level || 0,
// current:
// replyLevels.current.find(reply => reply.id === item.in_reply_to_id)?.level || 0,
// next:
// replyLevels.current.find(
// reply => reply.id === data.current?.[index + 1]?.in_reply_to_id
// )?.level || 0
// }
renderItem: ({ item, index }) => {
const levels = {
previous:
replyLevels.current.find(
reply => reply.id === data.current?.[index - 1]?.in_reply_to_id
)?.level || 0,
current:
replyLevels.current.find(reply => reply.id === item.in_reply_to_id)?.level || 0,
next:
replyLevels.current.find(
reply => reply.id === data.current?.[index + 1]?.in_reply_to_id
)?.level || 0
}
// return (
// <>
// <TimelineDefault
// item={item}
// queryKey={queryKey}
// rootQueryKey={rootQueryKey}
// highlighted={toot.id === item.id}
// isConversation={toot.id !== item.id}
// />
// {Array.from(Array(levels.current)).map((_, i) => {
// if (index < highlightIndex.current) return null
// if (
// levels.previous + 1 === levels.current ||
// (levels.previous && levels.current && levels.previous === levels.current)
// ) {
// return (
// <View
// key={i}
// style={{
// position: 'absolute',
// top: 0,
// left: StyleConstants.Spacing.Global.PagePadding / 2 + 8 * i,
// height:
// levels.current === levels.next
// ? StyleConstants.Spacing.Global.PagePadding
// : StyleConstants.Spacing.Global.PagePadding + StyleConstants.Avatar.XS,
// borderLeftColor: colors.border,
// borderLeftWidth: 1
// }}
// />
// )
// } else {
// return null
// }
// })}
// {Array.from(Array(levels.next)).map((_, i) => {
// if (index < highlightIndex.current) return null
// if (
// levels.current + 1 === levels.next ||
// (levels.current && levels.next && levels.current === levels.next)
// ) {
// return (
// <View
// key={i}
// style={{
// position: 'absolute',
// top:
// levels.current + 1 === levels.next && levels.next > i + 1
// ? StyleConstants.Spacing.Global.PagePadding + StyleConstants.Avatar.XS
// : StyleConstants.Spacing.Global.PagePadding,
// left: StyleConstants.Spacing.Global.PagePadding / 2 + 8 * i,
// height: 200,
// borderLeftColor: colors.border,
// borderLeftWidth: 1
// }}
// />
// )
// } else {
// return null
// }
// })}
// </>
// )
// },
return (
<View style={{ marginLeft: Math.max(0, levels.current - 1) * StyleConstants.Spacing.S }}>
<TimelineDefault
item={item}
queryKey={queryKey}
rootQueryKey={rootQueryKey}
highlighted={toot.id === item.id}
isConversation={toot.id !== item.id}
/>
</View>
)
},
onScrollToIndexFailed: error => {
const offset = error.averageItemLength * error.index
flRef.current?.scrollToOffset({ offset })

View File

@ -19,7 +19,7 @@ export type InstanceV10 = {
}
version: string
configuration?: Mastodon.Instance['configuration']
filters: Mastodon.Filter[]
filters: Mastodon.Filter<any>[]
notifications_filter: {
follow: boolean
follow_request: boolean

View File

@ -18,7 +18,7 @@ export type InstanceV11 = {
}
version: string
configuration?: Mastodon.Instance['configuration']
filters: Mastodon.Filter[]
filters: Mastodon.Filter<any>[]
notifications_filter: {
follow: boolean
follow_request: boolean

View File

@ -17,7 +17,7 @@ type Instance = {
avatarStatic: Mastodon.Account['avatar_static']
preferences: Mastodon.Preferences
}
filters: Mastodon.Filter[]
filters: Mastodon.Filter<any>[]
notifications_filter: {
follow: boolean
favourite: boolean

View File

@ -18,7 +18,7 @@ type Instance = {
}
max_toot_chars?: number // To be deprecated in v4
configuration?: Mastodon.Instance['configuration']
filters: Mastodon.Filter[]
filters: Mastodon.Filter<any>[]
notifications_filter: {
follow: boolean
favourite: boolean

View File

@ -19,7 +19,7 @@ type Instance = {
}
max_toot_chars?: number // To be deprecated in v4
configuration?: Mastodon.Instance['configuration']
filters: Mastodon.Filter[]
filters: Mastodon.Filter<any>[]
notifications_filter: {
follow: boolean
favourite: boolean

View File

@ -19,7 +19,7 @@ type Instance = {
}
max_toot_chars?: number // To be deprecated in v4
configuration?: Mastodon.Instance['configuration']
filters: Mastodon.Filter[]
filters: Mastodon.Filter<any>[]
notifications_filter: {
follow: boolean
favourite: boolean

View File

@ -19,7 +19,7 @@ export type InstanceV9 = {
}
version: string
configuration?: Mastodon.Instance['configuration']
filters: Mastodon.Filter[]
filters: Mastodon.Filter<any>[]
notifications_filter: {
follow: boolean
favourite: boolean

View File

@ -51,14 +51,14 @@ export type RootStackParamList = {
'Screen-ImagesViewer': {
imageUrls: {
id: Mastodon.Attachment['id']
preview_url: Mastodon.AttachmentImage['preview_url']
preview_url?: Mastodon.AttachmentImage['preview_url']
url: Mastodon.AttachmentImage['url']
remote_url?: Mastodon.AttachmentImage['remote_url']
blurhash: Mastodon.AttachmentImage['blurhash']
width?: number
height?: number
}[]
id: Mastodon.Attachment['id']
hideCounter?: boolean
}
'Screen-AccountSelection': {
component?: () => JSX.Element | undefined

View File

@ -35,7 +35,11 @@ const pushUseConnect = () => {
}),
{
enabled: false,
retry: 10,
retry: (failureCount, error) => {
if (error.status == 404) return false
return failureCount < 10
},
retryOnMount: false,
refetchOnMount: false,
refetchOnWindowFocus: false,

View File

@ -51,7 +51,7 @@ export type QueryKeyTimeline = [
const queryFunction = async ({ queryKey, pageParam }: QueryFunctionContext<QueryKeyTimeline>) => {
const page = queryKey[1]
let params: { [key: string]: string } = { ...pageParam, limit: 40 }
let params: { [key: string]: string } = { limit: 40, ...pageParam }
switch (page.page) {
case 'Following':

View File

@ -52,7 +52,7 @@ const addInstance = createAsyncThunk(
headers: { Authorization: `Bearer ${token}` }
})
const { body: filters } = await apiGeneral<Mastodon.Filter[]>({
const { body: filters } = await apiGeneral<Mastodon.Filter<any>[]>({
method: 'get',
domain,
url: `api/v1/filters`,

View File

@ -3,8 +3,8 @@ import { createAsyncThunk } from '@reduxjs/toolkit'
export const updateFilters = createAsyncThunk(
'instances/updateFilters',
async (): Promise<Mastodon.Filter[]> => {
return apiInstance<Mastodon.Filter[]>({
async (): Promise<Mastodon.Filter<any>[]> => {
return apiInstance<Mastodon.Filter<any>[]>({
method: 'get',
url: `filters`
}).then(res => res.body)

View File

@ -3,13 +3,13 @@ const Base = 4
export const StyleConstants = {
Font: {
Size: { S: 14, M: 16, L: 18 },
LineHeight: { S: 20, M: 22, L: 28 },
LineHeight: { S: 18, M: 21, L: 26 },
Weight: { Normal: '400' as '400', Bold: '600' as '600' }
},
FontStyle: {
S: { fontSize: 14, lineHeight: 20 },
M: { fontSize: 16, lineHeight: 22 },
L: { fontSize: 20, lineHeight: 28 }
S: { fontSize: 14, lineHeight: 18 },
M: { fontSize: 16, lineHeight: 21 },
L: { fontSize: 20, lineHeight: 26 }
},
Spacing: {

View File

@ -1,15 +1,3 @@
// import { Dimensions } from 'react-native'
// const { width } = Dimensions.get('screen')
// const guidelineBaseWidth = 375
// const guidelineBaseHeight = 667
// const scale = (size: number) => (width / guidelineBaseWidth) * size
// const verticalScale = (size: number) => (height / guidelineBaseHeight) * size
// const adaptiveScale = (size: number, factor: number = 0) =>
// size + (scale(size) - size) * factor
const adaptiveScale = (size: number, factor: number = 0) =>
size + size * (factor / 8)
const adaptiveScale = (size: number, factor: number = 0) => Math.round(size + size * (factor / 8))
export { adaptiveScale }