mirror of https://github.com/tooot-app/app
Merge branch 'main' into candidate
This commit is contained in:
commit
171cfd0ead
|
@ -0,0 +1 @@
|
|||
../../it/description.txt
|
|
@ -0,0 +1 @@
|
|||
../../it/subtitle.txt
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
|
@ -0,0 +1 @@
|
|||
Client open source per Mastodon
|
|
@ -1,11 +1,10 @@
|
|||
tooot是一个专门为中文用户社区所打造的开源、简洁长毛象客户端。使用此客户端需要已经拥有一个长毛象(https://joinmastodon.org/)账号。
|
||||
tooot起始于专注中文社区的简洁、开源长毛象手机客户端。使用此客户端需要已经拥有一个长毛象(https://joinmastodon.org/)账号。
|
||||
|
||||
tooot支持:
|
||||
- iPad
|
||||
- 跨平台,及iPadOS、MacOS
|
||||
- 多账号登录
|
||||
- 黑暗或自适应模式
|
||||
- 可调整正文字体大小
|
||||
- 可调正文字体尺寸
|
||||
- 消息推送
|
||||
等功能。
|
||||
|
||||
如有使用建议或意见,请联系@tooot@xmflsct.com或者support@tooot.app。
|
|
@ -1 +1,4 @@
|
|||
toooting愉快!此版本包括以下改进和修复:
|
||||
- 改进过滤体验,与v4.0以上版本一致
|
||||
- 支持查看用户的头像和横幅图片
|
||||
- 修复iPad部分尺寸问题(非优化)
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -98,7 +98,6 @@ const TimelineConversation: React.FC<Props> = ({ conversation, queryKey, highlig
|
|||
</View>
|
||||
|
||||
{conversation.last_status ? (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
paddingTop: highlighted ? StyleConstants.Spacing.S : 0,
|
||||
|
@ -107,10 +106,9 @@ const TimelineConversation: React.FC<Props> = ({ conversation, queryKey, highlig
|
|||
>
|
||||
<TimelineContent />
|
||||
<TimelinePoll />
|
||||
</View>
|
||||
|
||||
<TimelineActions />
|
||||
</>
|
||||
</View>
|
||||
) : null}
|
||||
</Pressable>
|
||||
</StatusContext.Provider>
|
||||
|
|
|
@ -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 />
|
||||
</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',
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'
|
||||
},
|
||||
|
|
|
@ -263,11 +263,6 @@ const TimelineActions: React.FC = () => {
|
|||
}, [status.bookmarked])
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
paddingLeft: highlighted ? 0 : StyleConstants.Avatar.M + StyleConstants.Spacing.S
|
||||
}}
|
||||
>
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
<Pressable
|
||||
{...(highlighted
|
||||
|
@ -320,7 +315,6 @@ const TimelineActions: React.FC = () => {
|
|||
children={childrenBookmark}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,19 +1,46 @@
|
|||
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 }) => {
|
||||
export interface FilteredProps {
|
||||
filterResults: { title: string; filter_action: Mastodon.Filter<'v2'>['filter_action'] }[]
|
||||
}
|
||||
|
||||
const TimelineFiltered: React.FC<FilteredProps> = ({ filterResults }) => {
|
||||
const { colors } = useTheme()
|
||||
const { t } = useTranslation('componentTimeline')
|
||||
|
||||
const main = () => {
|
||||
if (!filterResults?.length) {
|
||||
return <></>
|
||||
}
|
||||
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')}`}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={{ backgroundColor: colors.backgroundDefault }}>
|
||||
<CustomText
|
||||
|
@ -25,67 +52,47 @@ const TimelineFiltered = React.memo(
|
|||
paddingLeft: StyleConstants.Avatar.M + StyleConstants.Spacing.S
|
||||
}}
|
||||
>
|
||||
{t('shared.filtered', { phrase })}
|
||||
{main()}
|
||||
</CustomText>
|
||||
</View>
|
||||
)
|
||||
},
|
||||
() => true
|
||||
)
|
||||
}
|
||||
|
||||
export const shouldFilter = ({
|
||||
copiableContent,
|
||||
status,
|
||||
queryKey
|
||||
queryKey,
|
||||
status
|
||||
}: {
|
||||
copiableContent: React.MutableRefObject<{
|
||||
content: string
|
||||
complete: boolean
|
||||
}>
|
||||
status: Mastodon.Status
|
||||
queryKey: QueryKeyTimeline
|
||||
}): string | null => {
|
||||
status: Pick<Mastodon.Status, 'content' | 'spoiler_text'>
|
||||
}): FilteredProps['filterResults'] | undefined => {
|
||||
const page = queryKey[1]
|
||||
const instance = getInstance(store.getState())
|
||||
const ownAccount = getInstanceAccount(store.getState())?.id === status.account?.id
|
||||
|
||||
let shouldFilter: string | null = null
|
||||
let returnFilter: FilteredProps['filterResults'] | undefined
|
||||
|
||||
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`
|
||||
}
|
||||
parser.write(status.content)
|
||||
parser.end()
|
||||
|
||||
const checkFilter = (filter: Mastodon.Filter) => {
|
||||
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(rawContent)) {
|
||||
shouldFilter = filter.phrase
|
||||
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(rawContent)) {
|
||||
shouldFilter = filter.phrase
|
||||
if (new RegExp(escapedPhrase, 'i').test(rawContentCombined)) {
|
||||
returnFilter = [{ title: filter.phrase, filter_action: 'warn' }]
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
instance?.filters?.forEach(filter => {
|
||||
if (shouldFilter) {
|
||||
if (returnFilter) {
|
||||
return
|
||||
}
|
||||
if (filter.expires_at) {
|
||||
|
@ -100,30 +107,27 @@ export const shouldFilter = ({
|
|||
case 'List':
|
||||
case 'Account':
|
||||
if (filter.context.includes('home')) {
|
||||
checkFilter(filter)
|
||||
checkFilter(filter as Mastodon.Filter<'v1'>)
|
||||
}
|
||||
break
|
||||
case 'Notifications':
|
||||
if (filter.context.includes('notifications')) {
|
||||
checkFilter(filter)
|
||||
checkFilter(filter as Mastodon.Filter<'v1'>)
|
||||
}
|
||||
break
|
||||
case 'LocalPublic':
|
||||
if (filter.context.includes('public')) {
|
||||
checkFilter(filter)
|
||||
checkFilter(filter as Mastodon.Filter<'v1'>)
|
||||
}
|
||||
break
|
||||
case 'Toot':
|
||||
if (filter.context.includes('thread')) {
|
||||
checkFilter(filter)
|
||||
checkFilter(filter as Mastodon.Filter<'v1'>)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
copiableContent.current.complete = true
|
||||
}
|
||||
|
||||
return shouldFilter
|
||||
return returnFilter
|
||||
}
|
||||
|
||||
export default TimelineFiltered
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 }
|
||||
})
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -42,5 +42,9 @@
|
|||
{
|
||||
"feature": "notification_type_admin_report",
|
||||
"version": 4.0
|
||||
},
|
||||
{
|
||||
"feature": "filter_server_side",
|
||||
"version": 4.0
|
||||
}
|
||||
]
|
|
@ -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`
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
}}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}}
|
||||
|
|
|
@ -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()} />
|
||||
{!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
|
||||
})}
|
||||
/>
|
||||
|
|
|
@ -46,18 +46,14 @@ const TabMePush: React.FC = () => {
|
|||
|
||||
useEffect(() => {
|
||||
const checkPush = async () => {
|
||||
switch (Platform.OS) {
|
||||
case 'ios':
|
||||
const settings = await Notifications.getPermissionsAsync()
|
||||
const permissions = await Notifications.getPermissionsAsync()
|
||||
setPushEnabled(permissions.granted)
|
||||
setPushCanAskAgain(permissions.canAskAgain)
|
||||
layoutAnimation()
|
||||
setPushEnabled(settings.granted)
|
||||
setPushCanAskAgain(settings.canAskAgain)
|
||||
break
|
||||
case 'android':
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
await setChannels(instance)
|
||||
layoutAnimation()
|
||||
dispatch(retrieveExpoToken())
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}}
|
||||
|
|
|
@ -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 }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 AccountHeader: React.FC<Props> = ({ account }) => {
|
||||
const { colors } = useTheme()
|
||||
const topInset = useSafeAreaInsets().top
|
||||
|
||||
useSelector(getInstanceActive)
|
||||
|
||||
return (
|
||||
<View>
|
||||
<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('screen').width / 3 + topInset,
|
||||
height: Dimensions.get('window').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
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountHeader
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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]}>
|
||||
|
|
|
@ -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 (
|
||||
<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
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
},
|
||||
// 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
|
||||
// }
|
||||
// })}
|
||||
// </>
|
||||
// )
|
||||
// },
|
||||
onScrollToIndexFailed: error => {
|
||||
const offset = error.averageItemLength * error.index
|
||||
flRef.current?.scrollToOffset({ offset })
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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`,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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 }
|
||||
|
|
Loading…
Reference in New Issue