mirror of
https://github.com/tooot-app/app
synced 2025-06-05 22:19:13 +02:00
Merge branch 'main' into candidate
This commit is contained in:
@ -1,9 +1,3 @@
|
||||
Enjoy toooting! This version includes following improvements and fixes:
|
||||
- Auto fetch remote content in conversations!
|
||||
- Remember last read position in timeline!
|
||||
- Follow a user with other logged in accounts
|
||||
- Allowing adding more context of reports
|
||||
- Option to disable autoplay gif
|
||||
- Hide boosts from users
|
||||
- Followed hashtags are underlined
|
||||
- Support GoToSocial
|
||||
- Added following remote instance
|
||||
- Added set note of followed users
|
@ -1,9 +1,3 @@
|
||||
toooting愉快!此版本包括以下改进和修复:
|
||||
- 主动获取对话的远程内容
|
||||
- 自动加载上次我的关注的阅读位置
|
||||
- 用其它已登陆的账户关注用户
|
||||
- 可添加举报细节
|
||||
- 新增暂停自动播放gif动画选项
|
||||
- 隐藏用户的转嘟
|
||||
- 下划线高亮正在关注的话题标签
|
||||
- 支持GoToSocial
|
||||
- 新增关注远程实例功能
|
||||
- 新增关注用户备注功能
|
@ -68,7 +68,7 @@
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Allow $(PRODUCT_NAME) to access your camera roll to attach photos or videos to your toot</string>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>SplashScreen.storyboard</string>
|
||||
<string>SplashScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tooot",
|
||||
"version": "4.8.9",
|
||||
"version": "4.9.0",
|
||||
"description": "tooot for Mastodon",
|
||||
"author": "xmflsct <me@xmflsct.com>",
|
||||
"license": "GPL-3.0-or-later",
|
||||
|
2
src/@types/app.d.ts
vendored
2
src/@types/app.d.ts
vendored
@ -3,7 +3,7 @@ declare namespace App {
|
||||
| 'Following'
|
||||
| 'Local'
|
||||
| 'LocalPublic'
|
||||
| 'Trending'
|
||||
| 'Explore'
|
||||
| 'Notifications'
|
||||
| 'Hashtag'
|
||||
| 'List'
|
||||
|
@ -42,7 +42,7 @@ const ComponentAccount: React.FC<PropsWithChildren & Props> = ({ account, props,
|
||||
style={{
|
||||
width: StyleConstants.Avatar.S,
|
||||
height: StyleConstants.Avatar.S,
|
||||
borderRadius: 8,
|
||||
borderRadius: StyleConstants.BorderRadius,
|
||||
marginRight: StyleConstants.Spacing.S
|
||||
}}
|
||||
dim
|
||||
|
@ -45,7 +45,7 @@ const AccountButton: React.FC<Props> = ({ account, additionalActions }) => {
|
||||
width: StyleConstants.Font.Size.L,
|
||||
height: StyleConstants.Font.Size.L
|
||||
}}
|
||||
style={{ borderRadius: StyleConstants.Font.Size.L / 2, overflow: 'hidden' }}
|
||||
style={{ borderRadius: 99, overflow: 'hidden' }}
|
||||
/>
|
||||
<CustomText
|
||||
fontStyle='M'
|
||||
|
@ -116,7 +116,7 @@ const Button: React.FC<Props> = ({
|
||||
}}
|
||||
style={[
|
||||
{
|
||||
borderRadius: 100,
|
||||
borderRadius: 99,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderWidth: overlay ? 0 : selected ? 1.5 : 1,
|
||||
|
@ -35,7 +35,7 @@ const EmojisButton: React.FC = () => {
|
||||
borderWidth: 2,
|
||||
borderColor: colors.primaryDefault,
|
||||
padding: StyleConstants.Spacing.Global.PagePadding / 2,
|
||||
borderRadius: 100
|
||||
borderRadius: 99
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
|
@ -51,7 +51,7 @@ const HeaderLeft: React.FC<Props> = ({
|
||||
minWidth: 44,
|
||||
marginLeft: native ? -StyleConstants.Spacing.S : StyleConstants.Spacing.S,
|
||||
...(type === undefined && {
|
||||
borderRadius: 100
|
||||
borderRadius: 99
|
||||
}),
|
||||
...(type === 'text' && {
|
||||
paddingHorizontal: StyleConstants.Spacing.S
|
||||
|
@ -98,9 +98,7 @@ const HeaderRight: React.FC<Props> = ({
|
||||
minHeight: 44,
|
||||
minWidth: 44,
|
||||
marginRight: native ? -StyleConstants.Spacing.S : StyleConstants.Spacing.S,
|
||||
...(type === undefined && {
|
||||
borderRadius: 100
|
||||
}),
|
||||
...(type === undefined && { borderRadius: 99 }),
|
||||
...(type === 'text' && {
|
||||
paddingHorizontal: StyleConstants.Spacing.S
|
||||
})
|
||||
|
@ -22,7 +22,7 @@ export interface Props {
|
||||
switchDisabled?: boolean
|
||||
switchOnValueChange?: () => void
|
||||
|
||||
iconBack?: 'chevron-right' | 'external-link' | 'check'
|
||||
iconBack?: 'chevron-right' | 'chevron-down' | 'external-link' | 'check'
|
||||
iconBackColor?: ColorDefinitions
|
||||
|
||||
loading?: boolean
|
||||
@ -66,14 +66,7 @@ const MenuRow: React.FC<Props> = ({
|
||||
}}
|
||||
>
|
||||
<View style={{ flex: 1 }}>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginTop: StyleConstants.Spacing.S
|
||||
}}
|
||||
>
|
||||
<View style={{ flex: 1, flexDirection: 'row', justifyContent: 'space-between' }}>
|
||||
<View
|
||||
style={{
|
||||
flexShrink: 3,
|
||||
@ -96,7 +89,7 @@ const MenuRow: React.FC<Props> = ({
|
||||
width: 8,
|
||||
height: 8,
|
||||
backgroundColor: colors.red,
|
||||
borderRadius: 8,
|
||||
borderRadius: StyleConstants.BorderRadius,
|
||||
marginRight: StyleConstants.Spacing.S
|
||||
}}
|
||||
/>
|
||||
|
@ -29,20 +29,20 @@ const RelationshipOutgoing: React.FC<Props> = ({ id }: Props) => {
|
||||
const queryKeyRelationship: QueryKeyRelationship = ['Relationship', { id }]
|
||||
const queryClient = useQueryClient()
|
||||
const mutation = useRelationshipMutation({
|
||||
onSuccess: (res, { payload: { action } }) => {
|
||||
onSuccess: (res, vars) => {
|
||||
haptics('Success')
|
||||
queryClient.setQueryData<Mastodon.Relationship[]>(queryKeyRelationship, [res])
|
||||
if (action === 'block') {
|
||||
if (vars.type === 'outgoing' && vars.payload.action === 'block') {
|
||||
const queryKey = ['Timeline', { page: 'Following' }]
|
||||
queryClient.invalidateQueries({ queryKey, exact: false })
|
||||
}
|
||||
},
|
||||
onError: (err: any, { payload: { action } }) => {
|
||||
onError: (err: any, vars) => {
|
||||
displayMessage({
|
||||
theme,
|
||||
type: 'error',
|
||||
message: t('common:message.error.message', {
|
||||
function: t(`componentRelationship:${action}.function` as any)
|
||||
function: t(`componentRelationship:${(vars.payload as any).action}.function` as any)
|
||||
}),
|
||||
...(err.status &&
|
||||
typeof err.status === 'number' &&
|
||||
|
@ -31,9 +31,11 @@ export const SwipeToActions = <T extends unknown>({
|
||||
haptics(action.haptic || 'Light')
|
||||
action.onPress({ item })
|
||||
}}
|
||||
style={{ backgroundColor: 'rgba(0, 255, 0, 0.2)' }}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
paddingHorizontal: StyleConstants.Spacing.L,
|
||||
flexBasis: perActionWidth,
|
||||
backgroundColor: action.color,
|
||||
|
@ -67,7 +67,7 @@ const TimelineConversation: React.FC<Props> = ({ conversation, queryKey, highlig
|
||||
<View style={{ flex: 1, width: '100%', flexDirection: 'row' }}>
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 4,
|
||||
borderRadius: StyleConstants.BorderRadius,
|
||||
overflow: 'hidden',
|
||||
marginRight: StyleConstants.Spacing.S,
|
||||
width: StyleConstants.Avatar.M,
|
||||
|
@ -118,7 +118,7 @@ const AttachmentAudio: React.FC<Props> = ({ total, index, sensitiveShown, audio
|
||||
width: '100%',
|
||||
height: StyleConstants.Spacing.M + StyleConstants.Spacing.S * 2,
|
||||
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding,
|
||||
borderRadius: 100,
|
||||
borderRadius: 99,
|
||||
opacity: sensitiveShown ? 0.35 : undefined
|
||||
}}
|
||||
>
|
||||
|
@ -9,8 +9,9 @@ import { StackNavigationProp } from '@react-navigation/stack'
|
||||
import { RootStackParamList } from '@utils/navigation/navigators'
|
||||
import { usePreferencesQuery } from '@utils/queryHooks/preferences'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { isLargeDevice } from '@utils/styles/scaling'
|
||||
import { chunk } from 'lodash'
|
||||
import React, { useContext, useState } from 'react'
|
||||
import React, { Fragment, useContext, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Pressable, View } from 'react-native'
|
||||
import StatusContext from '../Context'
|
||||
@ -71,14 +72,9 @@ const TimelineAttachment = () => {
|
||||
}
|
||||
default:
|
||||
if (
|
||||
attachment.preview_url?.endsWith('.jpg') ||
|
||||
attachment.preview_url?.endsWith('.jpeg') ||
|
||||
attachment.preview_url?.endsWith('.png') ||
|
||||
attachment.preview_url?.endsWith('.gif') ||
|
||||
attachment.remote_url?.endsWith('.jpg') ||
|
||||
attachment.remote_url?.endsWith('.jpeg') ||
|
||||
attachment.remote_url?.endsWith('.png') ||
|
||||
attachment.remote_url?.endsWith('.gif')
|
||||
// https://docs.expo.dev/versions/unversioned/sdk/image/#supported-image-formats
|
||||
attachment.preview_url?.match(/.(?:a?png|jpe?g|webp|avif|heic|gif|svg|ico|icns)$/i) ||
|
||||
attachment.remote_url?.match(/.(?:a?png|jpe?g|webp|avif|heic|gif|svg|ico|icns)$/i)
|
||||
) {
|
||||
return {
|
||||
id: attachment.id,
|
||||
@ -179,11 +175,13 @@ const TimelineAttachment = () => {
|
||||
style={{
|
||||
marginTop: StyleConstants.Spacing.M,
|
||||
flex: 1,
|
||||
gap: StyleConstants.Spacing.XS
|
||||
gap: StyleConstants.Spacing.XS,
|
||||
...(isLargeDevice && { maxWidth: 375 })
|
||||
}}
|
||||
>
|
||||
{chunk(status.media_attachments, 2).map((chunk, chunkIndex) => (
|
||||
<View
|
||||
key={chunkIndex}
|
||||
style={{
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
@ -193,7 +191,9 @@ const TimelineAttachment = () => {
|
||||
gap: StyleConstants.Spacing.XS
|
||||
}}
|
||||
>
|
||||
{chunk.map((a, aIndex) => mapAttachmentType(a, chunkIndex * 2 + aIndex))}
|
||||
{chunk.map((a, aIndex) => (
|
||||
<Fragment key={aIndex}>{mapAttachmentType(a, chunkIndex * 2 + aIndex)}</Fragment>
|
||||
))}
|
||||
</View>
|
||||
))}
|
||||
|
||||
|
@ -49,7 +49,7 @@ const TimelineAvatar: React.FC<Props> = ({ account }) => {
|
||||
}
|
||||
}
|
||||
style={{
|
||||
borderRadius: StyleConstants.Avatar.M,
|
||||
borderRadius: 99,
|
||||
overflow: 'hidden',
|
||||
marginRight: StyleConstants.Spacing.S
|
||||
}}
|
||||
|
@ -137,7 +137,7 @@ const TimelineCard: React.FC = () => {
|
||||
flexDirection: 'row',
|
||||
marginTop: StyleConstants.Spacing.M,
|
||||
borderWidth: 1,
|
||||
borderRadius: StyleConstants.Spacing.S,
|
||||
borderRadius: StyleConstants.BorderRadius,
|
||||
overflow: 'hidden',
|
||||
borderColor: colors.border
|
||||
}}
|
||||
|
@ -42,6 +42,7 @@ export interface Props {
|
||||
'notifyOnChangeProps' | 'getNextPageParam' | 'getPreviousPageParam' | 'select' | 'onSuccess'
|
||||
>
|
||||
disableRefresh?: boolean
|
||||
refreshAutoRefetch?: boolean
|
||||
disableInfinity?: boolean
|
||||
readMarker?: 'read_marker_following'
|
||||
customProps?: Partial<FlatListProps<any>>
|
||||
@ -52,6 +53,7 @@ const Timeline: React.FC<Props> = ({
|
||||
queryKey,
|
||||
queryOptions,
|
||||
disableRefresh = false,
|
||||
refreshAutoRefetch = true,
|
||||
disableInfinity = false,
|
||||
readMarker = undefined,
|
||||
customProps
|
||||
@ -154,6 +156,7 @@ const Timeline: React.FC<Props> = ({
|
||||
if (
|
||||
curr === true &&
|
||||
prev === false &&
|
||||
refreshAutoRefetch &&
|
||||
!isFetchingPrev.value &&
|
||||
fetchingType.value === 0 &&
|
||||
shouldAutoFetch.value &&
|
||||
|
@ -105,21 +105,21 @@ const menuAccount = ({
|
||||
})
|
||||
const queryKeyRelationship: QueryKeyRelationship = ['Relationship', { id: actualAccount?.id }]
|
||||
const relationshipMutation = useRelationshipMutation({
|
||||
onSuccess: (res, { payload: { action } }) => {
|
||||
onSuccess: (res, vars) => {
|
||||
haptics('Success')
|
||||
queryClient.setQueryData<Mastodon.Relationship[]>(queryKeyRelationship, [res])
|
||||
if (action === 'block') {
|
||||
if (vars.type === 'outgoing' && vars.payload.action === 'block') {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['Timeline', { page: 'Following' }],
|
||||
exact: false
|
||||
})
|
||||
}
|
||||
},
|
||||
onError: (err: any, { payload: { action } }) => {
|
||||
onError: (err: any, vars) => {
|
||||
displayMessage({
|
||||
type: 'danger',
|
||||
message: t('common:message.error.message', {
|
||||
function: t(`componentContextMenu:${action}.function` as any)
|
||||
function: t(`componentContextMenu:${(vars.payload as any).action}.function` as any)
|
||||
}),
|
||||
...(err.status &&
|
||||
typeof err.status === 'number' &&
|
||||
|
26
src/components/discardConfirmation.ts
Normal file
26
src/components/discardConfirmation.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import i18n from '@i18n/index'
|
||||
import { Alert } from 'react-native'
|
||||
|
||||
export const discardConfirmation = ({
|
||||
condition,
|
||||
action
|
||||
}: {
|
||||
condition: boolean
|
||||
action: () => void
|
||||
}) => {
|
||||
if (condition) {
|
||||
Alert.alert(i18n.t('common:discard.title'), i18n.t('common:discard.message'), [
|
||||
{
|
||||
text: i18n.t('common:buttons.discard'),
|
||||
style: 'destructive',
|
||||
onPress: () => action()
|
||||
},
|
||||
{
|
||||
text: i18n.t('common:buttons.cancel'),
|
||||
style: 'default'
|
||||
}
|
||||
])
|
||||
} else {
|
||||
action()
|
||||
}
|
||||
}
|
@ -4,9 +4,6 @@ import { Platform } from 'react-native'
|
||||
const haptics = (
|
||||
type: 'Success' | 'Warning' | 'Error' | 'Light' | 'Medium' | 'Heavy'
|
||||
) => {
|
||||
if (Platform.OS === 'ios' && parseInt(Platform.Version, 10) <= 12) {
|
||||
return
|
||||
}
|
||||
if (Platform.OS === 'android') {
|
||||
if (type === 'Error') {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle['Light']).catch(() => {})
|
||||
|
@ -5,7 +5,7 @@ import i18next from 'i18next'
|
||||
import { Asset, launchImageLibrary } from 'react-native-image-picker'
|
||||
|
||||
const queryKeyInstance: QueryKeyInstance = ['Instance']
|
||||
export const MAX_MEDIA_ATTACHMENTS: number =
|
||||
export const MAX_MEDIA_ATTACHMENTS = (): number =>
|
||||
queryClient.getQueryData<Mastodon.Instance<any>>(queryKeyInstance)?.configuration?.statuses
|
||||
.max_media_attachments || 4
|
||||
|
||||
@ -27,7 +27,7 @@ const mediaSelector = async ({
|
||||
indicateMaximum = false,
|
||||
showActionSheetWithOptions
|
||||
}: Props): Promise<Asset[]> => {
|
||||
const _maximum = maximum || MAX_MEDIA_ATTACHMENTS
|
||||
const _maximum = maximum || MAX_MEDIA_ATTACHMENTS()
|
||||
|
||||
const options = () => {
|
||||
switch (mediaType) {
|
||||
|
@ -8,7 +8,8 @@
|
||||
"create": "Create",
|
||||
"delete": "Delete",
|
||||
"done": "Done",
|
||||
"confirm": "Confirm"
|
||||
"confirm": "Confirm",
|
||||
"add": "Add"
|
||||
},
|
||||
"customEmoji": {
|
||||
"accessibilityLabel": "Custom emoji {{emoji}}"
|
||||
|
@ -7,9 +7,7 @@
|
||||
"button": "Login",
|
||||
"information": {
|
||||
"name": "Name",
|
||||
"accounts": "Users",
|
||||
"statuses": "Toots",
|
||||
"domains": "Universes"
|
||||
"description": "Description"
|
||||
},
|
||||
"disclaimer": {
|
||||
"base": "Logging in process uses system browser that, your account information won't be visible to tooot app."
|
||||
|
@ -11,7 +11,13 @@
|
||||
"segments": {
|
||||
"federated": "Federated",
|
||||
"local": "Local",
|
||||
"trending": "Trending"
|
||||
"explore": "Explore"
|
||||
},
|
||||
"exploring": {
|
||||
"heading": "Exploring",
|
||||
"trending": "Trending",
|
||||
"followRemote": "Follow remote instance",
|
||||
"noTitle": "No Title"
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
@ -380,6 +386,7 @@
|
||||
"accessibilityHint": "You can mute, block, report or share this user"
|
||||
},
|
||||
"followed_by": " is following you",
|
||||
"privateNote": "Set private note",
|
||||
"moved": "User moved",
|
||||
"created_at": "Joined: {{date}}",
|
||||
"summary": {
|
||||
|
@ -52,7 +52,7 @@ const Share = ({
|
||||
padding: StyleConstants.Spacing.M,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.shimmerHighlight,
|
||||
borderRadius: 8
|
||||
borderRadius: StyleConstants.BorderRadius
|
||||
}}
|
||||
children={text}
|
||||
/>
|
||||
@ -65,7 +65,7 @@ const Share = ({
|
||||
padding: StyleConstants.Spacing.M,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.shimmerHighlight,
|
||||
borderRadius: 8
|
||||
borderRadius: StyleConstants.BorderRadius
|
||||
}}
|
||||
>
|
||||
<FlatList
|
||||
|
@ -73,7 +73,7 @@ const ScreenAnnouncements: React.FC<RootStackScreenProps<'Screen-Announcements'>
|
||||
padding: StyleConstants.Spacing.Global.PagePadding,
|
||||
marginTop: StyleConstants.Spacing.Global.PagePadding,
|
||||
borderWidth: 1,
|
||||
borderRadius: 6,
|
||||
borderRadius: StyleConstants.BorderRadius,
|
||||
borderColor: colors.primaryDefault,
|
||||
backgroundColor: colors.backgroundDefault
|
||||
}}
|
||||
@ -123,7 +123,7 @@ const ScreenAnnouncements: React.FC<RootStackScreenProps<'Screen-Announcements'>
|
||||
marginTop: StyleConstants.Spacing.Global.PagePadding / 2,
|
||||
marginBottom: StyleConstants.Spacing.Global.PagePadding / 2,
|
||||
marginRight: StyleConstants.Spacing.M,
|
||||
borderRadius: 6,
|
||||
borderRadius: StyleConstants.BorderRadius,
|
||||
flexDirection: 'row',
|
||||
borderColor: reaction.me ? colors.disabled : colors.primaryDefault,
|
||||
backgroundColor: reaction.me ? colors.disabled : colors.backgroundDefault
|
||||
@ -231,7 +231,7 @@ const ScreenAnnouncements: React.FC<RootStackScreenProps<'Screen-Announcements'>
|
||||
style={{
|
||||
width: StyleConstants.Spacing.S,
|
||||
height: StyleConstants.Spacing.S,
|
||||
borderRadius: StyleConstants.Spacing.S,
|
||||
borderRadius: StyleConstants.BorderRadius,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primaryDefault,
|
||||
backgroundColor: i === index ? colors.primaryDefault : undefined,
|
||||
@ -271,7 +271,7 @@ const ScreenAnnouncements: React.FC<RootStackScreenProps<'Screen-Announcements'>
|
||||
style={{
|
||||
width: StyleConstants.Spacing.S,
|
||||
height: StyleConstants.Spacing.S,
|
||||
borderRadius: StyleConstants.Spacing.S,
|
||||
borderRadius: StyleConstants.BorderRadius,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primaryDefault,
|
||||
backgroundColor: i === index ? colors.primaryDefault : undefined,
|
||||
|
@ -56,7 +56,7 @@ const ComposeDraftsList: React.FC<ScreenComposeStackScreenProps<'Screen-Compose-
|
||||
padding: StyleConstants.Spacing.S,
|
||||
borderColor: colors.border,
|
||||
borderWidth: 1,
|
||||
borderRadius: StyleConstants.Spacing.S,
|
||||
borderRadius: StyleConstants.BorderRadius,
|
||||
marginBottom: StyleConstants.Spacing.S
|
||||
}}
|
||||
>
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { discardConfirmation } from '@components/discardConfirmation'
|
||||
import haptics from '@components/haptics'
|
||||
import { HeaderLeft, HeaderRight } from '@components/Header'
|
||||
import { ModalScrollView } from '@components/ModalScrollView'
|
||||
@ -6,9 +7,11 @@ import apiInstance from '@utils/api/instance'
|
||||
import { ScreenComposeStackScreenProps } from '@utils/navigation/navigators'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { Image } from 'expo-image'
|
||||
import React, { useContext, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Alert, TextInput } from 'react-native'
|
||||
import { Alert, TextInput, View } from 'react-native'
|
||||
import { DEFAULT_WIDTH } from './Root/Footer/Attachments'
|
||||
import ComposeContext from './utils/createContext'
|
||||
|
||||
const ComposeEditAttachment: React.FC<
|
||||
@ -20,7 +23,7 @@ const ComposeEditAttachment: React.FC<
|
||||
}
|
||||
}) => {
|
||||
const { colors } = useTheme()
|
||||
const { t } = useTranslation('screenCompose')
|
||||
const { t } = useTranslation(['common', 'screenCompose'])
|
||||
|
||||
const { composeState, composeDispatch } = useContext(ComposeContext)
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
@ -31,55 +34,89 @@ const ComposeEditAttachment: React.FC<
|
||||
return null
|
||||
}
|
||||
|
||||
const [altText, setAltText] = useState<string | undefined>(theAttachment.description)
|
||||
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
title: t('content.editAttachment.header.title'),
|
||||
headerLeft: () => <HeaderLeft content='chevron-down' onPress={() => navigation.goBack()} />,
|
||||
title: t('screenCompose:content.editAttachment.header.title'),
|
||||
headerLeft: () => (
|
||||
<HeaderLeft
|
||||
content='chevron-down'
|
||||
onPress={() => {
|
||||
discardConfirmation({
|
||||
condition: theAttachment.description != altText,
|
||||
action: () => navigation.goBack()
|
||||
})
|
||||
}}
|
||||
/>
|
||||
),
|
||||
headerRight: () => (
|
||||
<HeaderRight
|
||||
accessibilityLabel={t('content.editAttachment.header.right.accessibilityLabel')}
|
||||
content='save'
|
||||
accessibilityLabel={t(
|
||||
'screenCompose:content.editAttachment.header.right.accessibilityLabel'
|
||||
)}
|
||||
type='text'
|
||||
content={t('common:buttons.apply')}
|
||||
loading={isSubmitting}
|
||||
onPress={() => {
|
||||
if (composeState.type === 'edit') {
|
||||
composeDispatch({ type: 'attachment/edit', payload: { ...theAttachment } })
|
||||
composeDispatch({
|
||||
type: 'attachment/edit',
|
||||
payload: { ...theAttachment, description: altText }
|
||||
})
|
||||
navigation.goBack()
|
||||
return
|
||||
}
|
||||
|
||||
setIsSubmitting(true)
|
||||
const body = { description: theAttachment.description }
|
||||
|
||||
theAttachment?.id &&
|
||||
apiInstance<Mastodon.Attachment>({
|
||||
method: 'put',
|
||||
url: `media/${theAttachment.id}`,
|
||||
body
|
||||
body: { description: altText }
|
||||
})
|
||||
.then(() => {
|
||||
.then(res => {
|
||||
setIsSubmitting(false)
|
||||
haptics('Success')
|
||||
composeDispatch({
|
||||
type: 'attachment/edit',
|
||||
payload: res.body
|
||||
})
|
||||
navigation.goBack()
|
||||
})
|
||||
.catch(() => {
|
||||
setIsSubmitting(false)
|
||||
haptics('Error')
|
||||
Alert.alert(t('content.editAttachment.header.right.failed.title'), undefined, [
|
||||
Alert.alert(
|
||||
t('screenCompose:content.editAttachment.header.right.failed.title'),
|
||||
undefined,
|
||||
[
|
||||
{
|
||||
text: t('content.editAttachment.header.right.failed.button'),
|
||||
text: t('screenCompose:content.editAttachment.header.right.failed.button'),
|
||||
style: 'cancel'
|
||||
}
|
||||
])
|
||||
]
|
||||
)
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}, [theAttachment])
|
||||
}, [theAttachment, altText])
|
||||
|
||||
return (
|
||||
<ModalScrollView>
|
||||
<View style={{ alignItems: 'center', marginBottom: StyleConstants.Spacing.M }}>
|
||||
<Image
|
||||
style={{
|
||||
width: DEFAULT_WIDTH,
|
||||
height: DEFAULT_WIDTH,
|
||||
borderRadius: StyleConstants.BorderRadius
|
||||
}}
|
||||
source={theAttachment.preview_url}
|
||||
/>
|
||||
</View>
|
||||
<CustomText fontStyle='M' style={{ color: colors.primaryDefault }} fontWeight='Bold'>
|
||||
{t('content.editAttachment.content.altText.heading')}
|
||||
{t('screenCompose:content.editAttachment.content.altText.heading')}
|
||||
</CustomText>
|
||||
<TextInput
|
||||
style={{
|
||||
@ -94,18 +131,10 @@ const ComposeEditAttachment: React.FC<
|
||||
}}
|
||||
maxLength={1500}
|
||||
multiline
|
||||
onChangeText={e =>
|
||||
composeDispatch({
|
||||
type: 'attachment/edit',
|
||||
payload: {
|
||||
...theAttachment,
|
||||
description: e
|
||||
}
|
||||
})
|
||||
}
|
||||
placeholder={t('content.editAttachment.content.altText.placeholder')}
|
||||
value={altText}
|
||||
onChangeText={e => setAltText(e)}
|
||||
placeholder={t('screenCompose:content.editAttachment.content.altText.placeholder')}
|
||||
placeholderTextColor={colors.secondary}
|
||||
value={theAttachment.description}
|
||||
/>
|
||||
<CustomText
|
||||
fontStyle='S'
|
||||
|
@ -30,7 +30,7 @@ const ComposeActions: React.FC = () => {
|
||||
const attachmentOnPress = () => {
|
||||
if (composeState.poll.active) return
|
||||
|
||||
if (composeState.attachments.uploads.length < MAX_MEDIA_ATTACHMENTS) {
|
||||
if (composeState.attachments.uploads.length < MAX_MEDIA_ATTACHMENTS()) {
|
||||
return chooseAndUploadAttachment({ composeDispatch, showActionSheetWithOptions })
|
||||
}
|
||||
}
|
||||
|
@ -12,19 +12,19 @@ import { StyleConstants } from '@utils/styles/constants'
|
||||
import layoutAnimation from '@utils/styles/layoutAnimation'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { Image } from 'expo-image'
|
||||
import React, { RefObject, useContext, useEffect, useRef } from 'react'
|
||||
import React, { RefObject, useContext, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FlatList, Pressable, StyleSheet, View } from 'react-native'
|
||||
import ComposeContext from '../../utils/createContext'
|
||||
import { ExtendedAttachment } from '../../utils/types'
|
||||
import chooseAndUploadAttachment from './addAttachment'
|
||||
|
||||
export const DEFAULT_WIDTH = 150
|
||||
|
||||
export interface Props {
|
||||
accessibleRefAttachments: RefObject<View>
|
||||
}
|
||||
|
||||
const DEFAULT_HEIGHT = 200
|
||||
|
||||
const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
|
||||
const { showActionSheetWithOptions } = useActionSheet()
|
||||
const { composeState, composeDispatch } = useContext(ComposeContext)
|
||||
@ -40,72 +40,22 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
|
||||
payload: { sensitive: !composeState.attachments.sensitive }
|
||||
})
|
||||
|
||||
const calculateWidth = (item: ExtendedAttachment) => {
|
||||
if (item.local) {
|
||||
return ((item.local.width || 100) / (item.local.height || 100)) * DEFAULT_HEIGHT
|
||||
} else {
|
||||
if (item.remote) {
|
||||
if (item.remote.meta.original.aspect) {
|
||||
return item.remote.meta.original.aspect * DEFAULT_HEIGHT
|
||||
} else if (item.remote.meta.original.width && item.remote.meta.original.height) {
|
||||
return (
|
||||
(item.remote.meta.original.width / item.remote.meta.original.height) * DEFAULT_HEIGHT
|
||||
)
|
||||
} else {
|
||||
return DEFAULT_HEIGHT
|
||||
}
|
||||
} else {
|
||||
return DEFAULT_HEIGHT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const snapToOffsets = () => {
|
||||
const attachmentsOffsets = composeState.attachments.uploads.map((_, index) => {
|
||||
let currentOffset = 0
|
||||
Array.from(Array(index).keys()).map(
|
||||
i =>
|
||||
(currentOffset =
|
||||
currentOffset +
|
||||
calculateWidth(composeState.attachments.uploads[i]) +
|
||||
StyleConstants.Spacing.Global.PagePadding)
|
||||
)
|
||||
return currentOffset
|
||||
})
|
||||
return attachmentsOffsets.length < 4
|
||||
? [
|
||||
...attachmentsOffsets,
|
||||
attachmentsOffsets.reduce((a, b) => a + b, 0) +
|
||||
DEFAULT_HEIGHT +
|
||||
StyleConstants.Spacing.Global.PagePadding
|
||||
]
|
||||
: attachmentsOffsets
|
||||
}
|
||||
let prevOffsets = useRef<number[]>()
|
||||
useEffect(() => {
|
||||
const snap = snapToOffsets()
|
||||
if (snap.length > (prevOffsets.current ? prevOffsets.current.length : 0)) {
|
||||
flatListRef.current?.scrollToOffset({
|
||||
offset: snap[snapToOffsets.length - 2] + snap[snapToOffsets.length - 1]
|
||||
})
|
||||
}
|
||||
prevOffsets.current = snap
|
||||
}, [snapToOffsets, prevOffsets.current])
|
||||
|
||||
const renderAttachment = ({ item, index }: { item: ExtendedAttachment; index: number }) => {
|
||||
return (
|
||||
<View
|
||||
key={index}
|
||||
style={{
|
||||
height: DEFAULT_HEIGHT,
|
||||
marginLeft: StyleConstants.Spacing.Global.PagePadding,
|
||||
marginTop: StyleConstants.Spacing.Global.PagePadding,
|
||||
marginBottom: StyleConstants.Spacing.Global.PagePadding,
|
||||
width: calculateWidth(item)
|
||||
marginBottom: StyleConstants.Spacing.Global.PagePadding
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
style={{
|
||||
width: DEFAULT_WIDTH,
|
||||
height: DEFAULT_WIDTH,
|
||||
borderRadius: StyleConstants.BorderRadius
|
||||
}}
|
||||
source={
|
||||
item.local?.thumbnail
|
||||
? { uri: item.local?.thumbnail }
|
||||
@ -123,7 +73,7 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
|
||||
paddingRight: StyleConstants.Spacing.S,
|
||||
paddingTop: StyleConstants.Spacing.XS,
|
||||
paddingBottom: StyleConstants.Spacing.XS,
|
||||
color: colors.backgroundDefault,
|
||||
color: colors.primaryOverlay,
|
||||
backgroundColor: colors.backgroundOverlayInvert
|
||||
}}
|
||||
>
|
||||
@ -157,7 +107,7 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
|
||||
})}
|
||||
type='icon'
|
||||
content='x'
|
||||
spacing='M'
|
||||
size='L'
|
||||
round
|
||||
overlay
|
||||
onPress={() => {
|
||||
@ -175,11 +125,11 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
|
||||
accessibilityLabel={t('content.root.footer.attachments.edit.accessibilityLabel', {
|
||||
attachment: index + 1
|
||||
})}
|
||||
type='icon'
|
||||
content='edit'
|
||||
spacing='M'
|
||||
round
|
||||
overlay
|
||||
size='S'
|
||||
type='text'
|
||||
content={!!item.remote?.description?.length ? 'ALT ✓' : '+ ALT'}
|
||||
fontBold
|
||||
onPress={() => navigation.navigate('Screen-Compose-EditAttachment', { index })}
|
||||
/>
|
||||
) : null}
|
||||
@ -230,23 +180,23 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
|
||||
pagingEnabled={false}
|
||||
snapToAlignment='center'
|
||||
renderItem={renderAttachment}
|
||||
snapToOffsets={snapToOffsets()}
|
||||
snapToOffsets={new Array(composeState.attachments.uploads.length).fill(DEFAULT_WIDTH)}
|
||||
keyboardShouldPersistTaps='always'
|
||||
showsHorizontalScrollIndicator={false}
|
||||
data={composeState.attachments.uploads}
|
||||
keyExtractor={item => item.local?.uri || item.remote?.url || Math.random().toString()}
|
||||
ListFooterComponent={
|
||||
composeState.attachments.uploads.length < MAX_MEDIA_ATTACHMENTS ? (
|
||||
composeState.attachments.uploads.length < MAX_MEDIA_ATTACHMENTS() ? (
|
||||
<Pressable
|
||||
accessible
|
||||
accessibilityLabel={t('content.root.footer.attachments.upload.accessibilityLabel')}
|
||||
style={{
|
||||
height: DEFAULT_HEIGHT,
|
||||
width: DEFAULT_WIDTH,
|
||||
height: DEFAULT_WIDTH,
|
||||
marginLeft: StyleConstants.Spacing.Global.PagePadding,
|
||||
marginTop: StyleConstants.Spacing.Global.PagePadding,
|
||||
marginBottom: StyleConstants.Spacing.Global.PagePadding,
|
||||
width: DEFAULT_HEIGHT,
|
||||
backgroundColor: colors.backgroundOverlayInvert
|
||||
backgroundColor: colors.disabled
|
||||
}}
|
||||
onPress={async () => {
|
||||
await chooseAndUploadAttachment({
|
||||
@ -258,9 +208,7 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
|
||||
<Button
|
||||
type='icon'
|
||||
content='upload-cloud'
|
||||
spacing='M'
|
||||
round
|
||||
overlay
|
||||
size='L'
|
||||
onPress={async () => {
|
||||
await chooseAndUploadAttachment({
|
||||
composeDispatch,
|
||||
@ -270,10 +218,11 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top:
|
||||
(DEFAULT_HEIGHT - StyleConstants.Spacing.M * 2 - StyleConstants.Font.Size.M) /
|
||||
2,
|
||||
(DEFAULT_WIDTH - StyleConstants.Spacing.M * 2 - StyleConstants.Font.Size.M) / 2,
|
||||
left:
|
||||
(DEFAULT_HEIGHT - StyleConstants.Spacing.M * 2 - StyleConstants.Font.Size.M) / 2
|
||||
(DEFAULT_WIDTH - StyleConstants.Spacing.M * 2 - StyleConstants.Font.Size.M) / 2,
|
||||
borderWidth: 0,
|
||||
backgroundColor: ''
|
||||
}}
|
||||
/>
|
||||
</Pressable>
|
||||
|
@ -38,8 +38,8 @@ const ComposePoll: React.FC = () => {
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderRadius: 6,
|
||||
borderWidth: 1,
|
||||
borderRadius: StyleConstants.BorderRadius,
|
||||
margin: StyleConstants.Spacing.Global.PagePadding,
|
||||
borderColor: colors.border
|
||||
}}
|
||||
@ -70,7 +70,7 @@ const ComposePoll: React.FC = () => {
|
||||
flex: 1,
|
||||
padding: StyleConstants.Spacing.S,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderRadius: 6,
|
||||
borderRadius: StyleConstants.BorderRadius,
|
||||
...StyleConstants.FontStyle.M,
|
||||
marginLeft: StyleConstants.Spacing.S,
|
||||
borderColor: colors.border,
|
||||
|
@ -17,7 +17,7 @@ const ComposeReply: React.FC = () => {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
borderWidth: 1,
|
||||
borderRadius: StyleConstants.Spacing.S,
|
||||
borderRadius: StyleConstants.BorderRadius,
|
||||
overflow: 'hidden',
|
||||
borderColor: colors.border,
|
||||
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
|
||||
|
@ -71,7 +71,7 @@ const ComposeTextInput: React.FC = () => {
|
||||
scrollEnabled={false}
|
||||
disableCopyPaste={false}
|
||||
onPaste={(error: string | null | undefined, files: PastedFile[]) => {
|
||||
if (composeState.attachments.uploads.length + files.length > MAX_MEDIA_ATTACHMENTS) {
|
||||
if (composeState.attachments.uploads.length + files.length > MAX_MEDIA_ATTACHMENTS()) {
|
||||
Alert.alert(
|
||||
t('screenCompose:content.root.header.textInput.keyboardImage.exceedMaximum.title'),
|
||||
undefined,
|
||||
|
@ -28,7 +28,39 @@ const composeInitialState: Omit<ComposeState, 'timestamp'> = {
|
||||
},
|
||||
attachments: {
|
||||
sensitive: false,
|
||||
uploads: []
|
||||
uploads: [
|
||||
// Test images
|
||||
// {
|
||||
// remote: {
|
||||
// id: '01',
|
||||
// type: 'image',
|
||||
// url: 'https://images.unsplash.com/photo-1669311540088-8d44f4ab2cd7',
|
||||
// preview_url: 'https://images.unsplash.com/photo-1669311540088-8d44f4ab2cd7'
|
||||
// },
|
||||
// local: undefined,
|
||||
// uploading: false
|
||||
// },
|
||||
// {
|
||||
// remote: {
|
||||
// id: '02',
|
||||
// type: 'image',
|
||||
// url: 'https://images.unsplash.com/photo-1669311605888-07172f42cb35',
|
||||
// preview_url: 'https://images.unsplash.com/photo-1669311605888-07172f42cb35'
|
||||
// },
|
||||
// local: undefined,
|
||||
// uploading: false
|
||||
// },
|
||||
// {
|
||||
// remote: {
|
||||
// id: '03',
|
||||
// type: 'image',
|
||||
// url: 'https://images.unsplash.com/photo-1669311576866-d77abb31f4ce',
|
||||
// preview_url: 'https://images.unsplash.com/photo-1669311576866-d77abb31f4ce'
|
||||
// },
|
||||
// local: undefined,
|
||||
// uploading: false
|
||||
// }
|
||||
]
|
||||
},
|
||||
visibility: 'public',
|
||||
visibilityLock: false,
|
||||
|
@ -1,25 +1,23 @@
|
||||
import { discardConfirmation } from '@components/discardConfirmation'
|
||||
import { EmojisState } from '@components/Emojis/Context'
|
||||
import haptics from '@components/haptics'
|
||||
import { HeaderLeft, HeaderRight } from '@components/Header'
|
||||
import ComponentInput from '@components/Input'
|
||||
import { displayMessage, Message } from '@components/Message'
|
||||
import Selections from '@components/Selections'
|
||||
import CustomText from '@components/Text'
|
||||
import { CommonActions } from '@react-navigation/native'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { TabMeStackScreenProps } from '@utils/navigation/navigators'
|
||||
import { QueryKeyLists, useListsMutation } from '@utils/queryHooks/lists'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Alert, ScrollView, TextInput } from 'react-native'
|
||||
import { ScrollView, TextInput } from 'react-native'
|
||||
|
||||
const TabMeListEdit: React.FC<TabMeStackScreenProps<'Tab-Me-List-Edit'>> = ({
|
||||
navigation,
|
||||
route: { params }
|
||||
}) => {
|
||||
const { colors } = useTheme()
|
||||
const { t } = useTranslation(['common', 'screenTabs'])
|
||||
|
||||
const messageRef = useRef(null)
|
||||
@ -92,21 +90,10 @@ const TabMeListEdit: React.FC<TabMeStackScreenProps<'Tab-Me-List-Edit'>> = ({
|
||||
<HeaderLeft
|
||||
content='x'
|
||||
onPress={() => {
|
||||
if (params.type === 'edit' ? params.payload.title !== title : title.length) {
|
||||
Alert.alert(t('common:discard.title'), t('common:discard.message'), [
|
||||
{
|
||||
text: t('common:buttons.discard'),
|
||||
style: 'destructive',
|
||||
onPress: () => navigation.pop(1)
|
||||
},
|
||||
{
|
||||
text: t('common:buttons.cancel'),
|
||||
style: 'default'
|
||||
}
|
||||
])
|
||||
} else {
|
||||
navigation.pop(1)
|
||||
}
|
||||
discardConfirmation({
|
||||
condition: params.type === 'edit' ? params.payload.title !== title : !!title.length,
|
||||
action: () => navigation.pop(1)
|
||||
})
|
||||
}}
|
||||
/>
|
||||
),
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { discardConfirmation } from '@components/discardConfirmation'
|
||||
import { ComponentEmojis } from '@components/Emojis'
|
||||
import { EmojisState } from '@components/Emojis/Context'
|
||||
import { HeaderLeft, HeaderRight } from '@components/Header'
|
||||
@ -9,7 +10,7 @@ import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { Dispatch, RefObject, SetStateAction, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Alert, ScrollView, TextInput } from 'react-native'
|
||||
import { ScrollView, TextInput } from 'react-native'
|
||||
import FlashMessage from 'react-native-flash-message'
|
||||
|
||||
const Field: React.FC<{
|
||||
@ -92,21 +93,10 @@ const TabMeProfileFields: React.FC<
|
||||
<HeaderLeft
|
||||
content='chevron-left'
|
||||
onPress={() => {
|
||||
if (dirty) {
|
||||
Alert.alert(t('common:discard.title'), t('common:discard.message'), [
|
||||
{
|
||||
text: t('common:buttons.discard'),
|
||||
style: 'destructive',
|
||||
onPress: () => navigation.navigate('Tab-Me-Profile-Root')
|
||||
},
|
||||
{
|
||||
text: t('common:buttons.cancel'),
|
||||
style: 'default'
|
||||
}
|
||||
])
|
||||
} else {
|
||||
navigation.navigate('Tab-Me-Profile-Root')
|
||||
}
|
||||
discardConfirmation({
|
||||
condition: dirty,
|
||||
action: () => navigation.navigate('Tab-Me-Profile-Root')
|
||||
})
|
||||
}}
|
||||
/>
|
||||
),
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { discardConfirmation } from '@components/discardConfirmation'
|
||||
import { ComponentEmojis } from '@components/Emojis'
|
||||
import { EmojisState } from '@components/Emojis/Context'
|
||||
import { HeaderLeft, HeaderRight } from '@components/Header'
|
||||
@ -7,8 +8,7 @@ import { useProfileMutation } from '@utils/queryHooks/profile'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { RefObject, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Alert, ScrollView, TextInput } from 'react-native'
|
||||
import { ScrollView, TextInput } from 'react-native'
|
||||
import FlashMessage from 'react-native-flash-message'
|
||||
|
||||
const TabMeProfileName: React.FC<
|
||||
@ -23,7 +23,6 @@ const TabMeProfileName: React.FC<
|
||||
navigation
|
||||
}) => {
|
||||
const { theme } = useTheme()
|
||||
const { t } = useTranslation(['common'])
|
||||
const { mutateAsync, status } = useProfileMutation()
|
||||
|
||||
const [value, setValue] = useState(display_name)
|
||||
@ -46,21 +45,10 @@ const TabMeProfileName: React.FC<
|
||||
<HeaderLeft
|
||||
content='chevron-left'
|
||||
onPress={() => {
|
||||
if (dirty) {
|
||||
Alert.alert(t('common:discard.title'), t('common:discard.message'), [
|
||||
{
|
||||
text: t('common:buttons.discard'),
|
||||
style: 'destructive',
|
||||
onPress: () => navigation.navigate('Tab-Me-Profile-Root')
|
||||
},
|
||||
{
|
||||
text: t('common:buttons.cancel'),
|
||||
style: 'default'
|
||||
}
|
||||
])
|
||||
} else {
|
||||
navigation.navigate('Tab-Me-Profile-Root')
|
||||
}
|
||||
discardConfirmation({
|
||||
condition: dirty,
|
||||
action: () => navigation.navigate('Tab-Me-Profile-Root')
|
||||
})
|
||||
}}
|
||||
/>
|
||||
),
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { discardConfirmation } from '@components/discardConfirmation'
|
||||
import { ComponentEmojis } from '@components/Emojis'
|
||||
import { EmojisState } from '@components/Emojis/Context'
|
||||
import { HeaderLeft, HeaderRight } from '@components/Header'
|
||||
@ -7,8 +8,7 @@ import { useProfileMutation } from '@utils/queryHooks/profile'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { RefObject, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Alert, ScrollView, TextInput } from 'react-native'
|
||||
import { ScrollView, TextInput } from 'react-native'
|
||||
import FlashMessage from 'react-native-flash-message'
|
||||
|
||||
const TabMeProfileNote: React.FC<
|
||||
@ -23,7 +23,6 @@ const TabMeProfileNote: React.FC<
|
||||
navigation
|
||||
}) => {
|
||||
const { theme } = useTheme()
|
||||
const { t } = useTranslation(['common'])
|
||||
const { mutateAsync, status } = useProfileMutation()
|
||||
|
||||
const [notes, setNotes] = useState(note)
|
||||
@ -46,21 +45,10 @@ const TabMeProfileNote: React.FC<
|
||||
<HeaderLeft
|
||||
content='chevron-left'
|
||||
onPress={() => {
|
||||
if (dirty) {
|
||||
Alert.alert(t('common:discard.title'), t('common:discard.message'), [
|
||||
{
|
||||
text: t('common:buttons.discard'),
|
||||
style: 'destructive',
|
||||
onPress: () => navigation.navigate('Tab-Me-Profile-Root')
|
||||
},
|
||||
{
|
||||
text: t('common:buttons.cancel'),
|
||||
style: 'default'
|
||||
}
|
||||
])
|
||||
} else {
|
||||
navigation.navigate('Tab-Me-Profile-Root')
|
||||
}
|
||||
discardConfirmation({
|
||||
condition: dirty,
|
||||
action: () => navigation.navigate('Tab-Me-Profile-Root')
|
||||
})
|
||||
}}
|
||||
/>
|
||||
),
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { discardConfirmation } from '@components/discardConfirmation'
|
||||
import { HeaderLeft, HeaderRight } from '@components/Header'
|
||||
import { MenuContainer, MenuRow } from '@components/Menu'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
@ -8,7 +9,6 @@ import { setAccountStorage, useAccountStorage } from '@utils/storage/actions'
|
||||
import { isEqual } from 'lodash'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Alert } from 'react-native'
|
||||
import { ScrollView } from 'react-native-gesture-handler'
|
||||
|
||||
const TabNotificationsFilters: React.FC<
|
||||
@ -28,21 +28,7 @@ const TabNotificationsFilters: React.FC<
|
||||
<HeaderLeft
|
||||
content='chevron-down'
|
||||
onPress={() => {
|
||||
if (changed) {
|
||||
Alert.alert(t('common:discard.title'), t('common:discard.message'), [
|
||||
{
|
||||
text: t('common:buttons.discard'),
|
||||
style: 'destructive',
|
||||
onPress: () => navigation.goBack()
|
||||
},
|
||||
{
|
||||
text: t('common:buttons.cancel'),
|
||||
style: 'default'
|
||||
}
|
||||
])
|
||||
} else {
|
||||
navigation.goBack()
|
||||
}
|
||||
discardConfirmation({ condition: changed, action: () => navigation.goBack() })
|
||||
}}
|
||||
/>
|
||||
),
|
||||
|
@ -1,26 +1,389 @@
|
||||
import { HeaderRight } from '@components/Header'
|
||||
import Button from '@components/Button'
|
||||
import { HeaderLeft, HeaderRight } from '@components/Header'
|
||||
import Icon from '@components/Icon'
|
||||
import CustomText from '@components/Text'
|
||||
import Timeline from '@components/Timeline'
|
||||
import SegmentedControl from '@react-native-segmented-control/segmented-control'
|
||||
import { useScrollToTop } from '@react-navigation/native'
|
||||
import { NativeStackScreenProps } from '@react-navigation/native-stack'
|
||||
import apiGeneral from '@utils/api/general'
|
||||
import { TabPublicStackParamList } from '@utils/navigation/navigators'
|
||||
import { useInstanceQuery } from '@utils/queryHooks/instance'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import { getGlobalStorage, setGlobalStorage } from '@utils/storage/actions'
|
||||
import { getGlobalStorage, setGlobalStorage, useGlobalStorage } from '@utils/storage/actions'
|
||||
import { StorageGlobal } from '@utils/storage/global'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import layoutAnimation from '@utils/styles/layoutAnimation'
|
||||
import { isLargeDevice } from '@utils/styles/scaling'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { debounce } from 'lodash'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Dimensions } from 'react-native'
|
||||
import { Dimensions, FlatList, Platform, Pressable, TextInput, View } from 'react-native'
|
||||
import { SceneMap, TabView } from 'react-native-tab-view'
|
||||
import { Placeholder, PlaceholderLine } from 'rn-placeholder'
|
||||
import * as DropdownMenu from 'zeego/dropdown-menu'
|
||||
|
||||
const Route = ({ route: { key: page } }: { route: any }) => {
|
||||
const Explore = ({ route: { key: page } }: { route: { key: 'Explore' } }) => {
|
||||
const { t } = useTranslation(['common', 'componentInstance', 'screenTabs'])
|
||||
const { colors, mode } = useTheme()
|
||||
|
||||
const [addingRemote, setAddingRemote] = useState(false)
|
||||
const [domain, setDomain] = useState<string>('')
|
||||
const [domainValid, setDomainValid] = useState<boolean>()
|
||||
const instanceQuery = useInstanceQuery({
|
||||
domain,
|
||||
options: {
|
||||
enabled: false,
|
||||
retry: false,
|
||||
keepPreviousData: false,
|
||||
cacheTime: 1000 * 30,
|
||||
onSuccess: () =>
|
||||
apiGeneral<Mastodon.Status[]>({
|
||||
method: 'get',
|
||||
domain: domain,
|
||||
url: 'api/v1/timelines/public',
|
||||
params: { local: 'true', limit: 1 }
|
||||
})
|
||||
.then(({ body }) => {
|
||||
if (Array.isArray(body)) {
|
||||
setDomainValid(true)
|
||||
} else {
|
||||
setDomainValid(false)
|
||||
}
|
||||
})
|
||||
.catch(() => setDomainValid(false))
|
||||
}
|
||||
})
|
||||
const debounceFetch = useCallback(
|
||||
debounce(() => {
|
||||
instanceQuery.refetch()
|
||||
}, 1000),
|
||||
[]
|
||||
)
|
||||
|
||||
const [accountActive] = useGlobalStorage.string('account.active')
|
||||
const [remoteActive, setRemoteActive] = useGlobalStorage.string('remote.active')
|
||||
const [remotes, setRemotes] = useGlobalStorage.object('remotes')
|
||||
|
||||
const flRef = useRef<FlatList>(null)
|
||||
const queryKey: QueryKeyTimeline = [
|
||||
'Timeline',
|
||||
{ page, ...(remoteActive && { domain: remoteActive }) }
|
||||
]
|
||||
|
||||
const info = ({
|
||||
heading,
|
||||
content,
|
||||
lines,
|
||||
potentialWidth = 6
|
||||
}: {
|
||||
heading: string
|
||||
content?: string
|
||||
lines?: number
|
||||
potentialWidth?: number
|
||||
}) => (
|
||||
<View style={{ flex: 1, marginTop: StyleConstants.Spacing.M }} accessible>
|
||||
<CustomText
|
||||
fontStyle='S'
|
||||
style={{
|
||||
marginBottom: StyleConstants.Spacing.XS,
|
||||
color: colors.primaryDefault
|
||||
}}
|
||||
fontWeight='Bold'
|
||||
children={heading}
|
||||
/>
|
||||
{content ? (
|
||||
<CustomText
|
||||
fontStyle='M'
|
||||
style={{ color: colors.primaryDefault }}
|
||||
children={content}
|
||||
numberOfLines={lines}
|
||||
/>
|
||||
) : (
|
||||
Array.from({ length: lines || 1 }).map((_, index) => (
|
||||
<PlaceholderLine
|
||||
key={index}
|
||||
width={potentialWidth ? potentialWidth * StyleConstants.Font.Size.M : undefined}
|
||||
height={StyleConstants.Font.LineHeight.M}
|
||||
color={colors.shimmerDefault}
|
||||
noMargin
|
||||
style={{ borderRadius: 0 }}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
|
||||
useScrollToTop(flRef)
|
||||
|
||||
return (
|
||||
<Timeline
|
||||
flRef={flRef}
|
||||
queryKey={queryKey}
|
||||
disableRefresh={!remoteActive}
|
||||
refreshAutoRefetch={false}
|
||||
customProps={{
|
||||
ListHeaderComponent: (
|
||||
<View
|
||||
style={{ backgroundColor: colors.backgroundDefault }}
|
||||
children={
|
||||
addingRemote ? (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding,
|
||||
marginTop: StyleConstants.Spacing.S,
|
||||
borderWidth: 1,
|
||||
borderRadius: StyleConstants.BorderRadius,
|
||||
marginHorizontal: StyleConstants.Spacing.Global.PagePadding,
|
||||
borderColor: colors.border
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: StyleConstants.Spacing.XS
|
||||
}}
|
||||
>
|
||||
<HeaderLeft
|
||||
onPress={() => {
|
||||
setDomain('')
|
||||
setAddingRemote(false)
|
||||
layoutAnimation().then(() =>
|
||||
flRef.current?.scrollToOffset({ animated: true, offset: 0 })
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<CustomText
|
||||
fontSize='M'
|
||||
fontWeight='Bold'
|
||||
style={{ color: colors.primaryDefault }}
|
||||
children={t('screenTabs:tabs.public.exploring.followRemote')}
|
||||
/>
|
||||
<HeaderRight type='text' content='' onPress={() => {}} />
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
accessible={false}
|
||||
accessibilityRole='none'
|
||||
style={{
|
||||
borderBottomWidth: 1,
|
||||
...StyleConstants.FontStyle.M,
|
||||
color: colors.primaryDefault,
|
||||
borderBottomColor:
|
||||
instanceQuery.isError ||
|
||||
(!!domain.length && instanceQuery.isFetched && domainValid === false)
|
||||
? colors.red
|
||||
: colors.border,
|
||||
paddingVertical: StyleConstants.Spacing.S,
|
||||
...(Platform.OS === 'android' && { paddingRight: 0 })
|
||||
}}
|
||||
editable={false}
|
||||
defaultValue='https://'
|
||||
/>
|
||||
<TextInput
|
||||
style={{
|
||||
flex: 1,
|
||||
borderBottomWidth: 1,
|
||||
marginRight: StyleConstants.Spacing.M,
|
||||
...StyleConstants.FontStyle.M,
|
||||
color: colors.primaryDefault,
|
||||
borderBottomColor:
|
||||
instanceQuery.isError ||
|
||||
(!!domain.length && instanceQuery.isFetched && domainValid === false)
|
||||
? colors.red
|
||||
: colors.border,
|
||||
paddingVertical: StyleConstants.Spacing.S,
|
||||
...(Platform.OS === 'android' && { paddingLeft: 0 })
|
||||
}}
|
||||
value={domain}
|
||||
onChangeText={text => {
|
||||
setDomain(text.replace(/^http(s)?\:\/\//i, ''))
|
||||
setDomainValid(undefined)
|
||||
debounceFetch()
|
||||
}}
|
||||
autoCapitalize='none'
|
||||
clearButtonMode='never'
|
||||
keyboardType='url'
|
||||
textContentType='URL'
|
||||
onSubmitEditing={() => instanceQuery.refetch()}
|
||||
placeholder={' ' + t('componentInstance:server.textInput.placeholder')}
|
||||
placeholderTextColor={colors.secondary}
|
||||
returnKeyType='go'
|
||||
keyboardAppearance={mode}
|
||||
autoCorrect={false}
|
||||
spellCheck={false}
|
||||
autoFocus
|
||||
/>
|
||||
<Button
|
||||
type='text'
|
||||
content={t('common:buttons.add')}
|
||||
loading={instanceQuery.isFetching}
|
||||
disabled={
|
||||
!!domain.length
|
||||
? domainValid === false ||
|
||||
accountActive === domain ||
|
||||
!!remotes?.find(r => r.domain === domain)
|
||||
: true
|
||||
}
|
||||
onPress={() => {
|
||||
setRemotes([
|
||||
...(remotes || []),
|
||||
{
|
||||
title:
|
||||
instanceQuery.data?.title ||
|
||||
t('screenTabs:tabs.public.exploring.noTitle'),
|
||||
domain
|
||||
}
|
||||
])
|
||||
setRemoteActive(domain)
|
||||
setAddingRemote(false)
|
||||
layoutAnimation().then(() =>
|
||||
flRef.current?.scrollToOffset({ animated: true, offset: 0 })
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Placeholder style={{ marginBottom: StyleConstants.Spacing.M }}>
|
||||
{info({
|
||||
heading: t('componentInstance:server.information.name'),
|
||||
content: !!domain.length ? instanceQuery.data?.title : undefined,
|
||||
potentialWidth: 2
|
||||
})}
|
||||
{info({
|
||||
heading: t('componentInstance:server.information.description'),
|
||||
content: !!domain.length
|
||||
? (instanceQuery.data as Mastodon.Instance_V1)?.short_description ||
|
||||
instanceQuery.data?.description
|
||||
: undefined,
|
||||
lines: isLargeDevice ? 1 : 2
|
||||
})}
|
||||
</Placeholder>
|
||||
</View>
|
||||
) : (
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Pressable
|
||||
style={{
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: StyleConstants.Spacing.XS,
|
||||
paddingTop: StyleConstants.Spacing.M
|
||||
}}
|
||||
>
|
||||
<CustomText
|
||||
fontSize='S'
|
||||
style={{ color: colors.secondary }}
|
||||
children={t('screenTabs:tabs.public.exploring.heading')}
|
||||
/>
|
||||
<CustomText
|
||||
fontSize='S'
|
||||
style={{ color: colors.primaryDefault }}
|
||||
children={
|
||||
!remoteActive
|
||||
? t('screenTabs:tabs.public.exploring.trending').toLocaleLowerCase()
|
||||
: remotes?.find(r => r.domain === remoteActive)?.title
|
||||
}
|
||||
/>
|
||||
<Icon
|
||||
name='chevron-down'
|
||||
color={colors.primaryDefault}
|
||||
size={StyleConstants.Font.Size.M}
|
||||
/>
|
||||
</Pressable>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Content>
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.CheckboxItem
|
||||
key={`explore_trending`}
|
||||
value={!remoteActive ? 'on' : 'off'}
|
||||
onValueChange={next => {
|
||||
if (next === 'on') {
|
||||
setRemoteActive(undefined)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.ItemIndicator />
|
||||
<DropdownMenu.ItemTitle
|
||||
children={t('screenTabs:tabs.public.exploring.trending')}
|
||||
/>
|
||||
</DropdownMenu.CheckboxItem>
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Group>
|
||||
{remotes?.map((item: NonNullable<StorageGlobal['remotes']>[0], index) => (
|
||||
<DropdownMenu.CheckboxItem
|
||||
key={`explore_${index}`}
|
||||
value={
|
||||
index === remotes?.findIndex(r => r.domain === remoteActive)
|
||||
? 'on'
|
||||
: 'off'
|
||||
}
|
||||
onValueChange={next => {
|
||||
if (next === 'on') {
|
||||
setRemoteActive(item.domain)
|
||||
} else if (next === 'off') {
|
||||
const nextRemotes = remotes?.filter(r => r.domain !== item.domain)
|
||||
setRemotes(nextRemotes)
|
||||
setRemoteActive(nextRemotes.at(-1)?.domain)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.ItemIndicator />
|
||||
<DropdownMenu.ItemTitle children={item.title} />
|
||||
<DropdownMenu.ItemSubtitle children={item.domain} />
|
||||
{index === remotes?.findIndex(r => r.domain === remoteActive) ? (
|
||||
<DropdownMenu.ItemIcon ios={{ name: 'trash' }} />
|
||||
) : null}
|
||||
</DropdownMenu.CheckboxItem>
|
||||
))}
|
||||
<DropdownMenu.Item
|
||||
key='explore_add'
|
||||
onSelect={() => {
|
||||
setDomain('')
|
||||
setAddingRemote(true)
|
||||
layoutAnimation().then(() =>
|
||||
flRef.current?.scrollToOffset({ animated: true, offset: 0 })
|
||||
)
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.ItemTitle
|
||||
children={t('screenTabs:tabs.public.exploring.followRemote')}
|
||||
/>
|
||||
<DropdownMenu.ItemIcon ios={{ name: 'plus' }} />
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const Route = ({ route: { key: page } }: { route: { key: any } }) => {
|
||||
const queryKey: QueryKeyTimeline = ['Timeline', { page }]
|
||||
return <Timeline queryKey={queryKey} disableRefresh={page === 'Trending'} />
|
||||
return <Timeline queryKey={queryKey} />
|
||||
}
|
||||
|
||||
const renderScene = SceneMap({
|
||||
Local: Route,
|
||||
LocalPublic: Route,
|
||||
Trending: Route
|
||||
Explore
|
||||
})
|
||||
|
||||
const Root: React.FC<NativeStackScreenProps<TabPublicStackParamList, 'Tab-Public-Root'>> = ({
|
||||
@ -29,7 +392,7 @@ const Root: React.FC<NativeStackScreenProps<TabPublicStackParamList, 'Tab-Public
|
||||
const { mode } = useTheme()
|
||||
const { t } = useTranslation('screenTabs')
|
||||
|
||||
const segments: StorageGlobal['app.prev_public_segment'][] = ['Local', 'LocalPublic', 'Trending']
|
||||
const segments: StorageGlobal['app.prev_public_segment'][] = ['Local', 'LocalPublic', 'Explore']
|
||||
const [segment, setSegment] = useState<number>(
|
||||
Math.max(
|
||||
0,
|
||||
@ -39,12 +402,19 @@ const Root: React.FC<NativeStackScreenProps<TabPublicStackParamList, 'Tab-Public
|
||||
const [routes] = useState([
|
||||
{ key: 'Local', title: t('tabs.public.segments.local') },
|
||||
{ key: 'LocalPublic', title: t('tabs.public.segments.federated') },
|
||||
{ key: 'Trending', title: t('tabs.public.segments.trending') }
|
||||
{ key: 'Explore', title: t('tabs.public.segments.explore') }
|
||||
])
|
||||
const [remoteActive] = useGlobalStorage.string('remote.active')
|
||||
useEffect(() => {
|
||||
const page = segments[segment]
|
||||
page && navigation.setParams({ queryKey: ['Timeline', { page }] })
|
||||
}, [segment])
|
||||
page &&
|
||||
navigation.setParams({
|
||||
queryKey: [
|
||||
'Timeline',
|
||||
{ page, ...(page === 'Explore' && remoteActive && { domain: remoteActive }) }
|
||||
]
|
||||
})
|
||||
}, [segment, remoteActive])
|
||||
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
|
@ -6,6 +6,7 @@ import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
||||
import { useTimelineQuery } from '@utils/queryHooks/timeline'
|
||||
import { flattenPages } from '@utils/queryHooks/utils'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { isLargeDevice } from '@utils/styles/scaling'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useContext } from 'react'
|
||||
import { Dimensions, Pressable, View } from 'react-native'
|
||||
@ -20,9 +21,11 @@ const AccountAttachments: React.FC = () => {
|
||||
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
|
||||
const { colors } = useTheme()
|
||||
|
||||
const DISPLAY_AMOUNT = 6
|
||||
const DISPLAY_AMOUNT = isLargeDevice ? 8 : 6
|
||||
|
||||
const width = (Dimensions.get('window').width - StyleConstants.Spacing.Global.PagePadding * 2) / 4
|
||||
const width =
|
||||
(Dimensions.get('window').width - StyleConstants.Spacing.Global.PagePadding * 2) /
|
||||
(DISPLAY_AMOUNT - 1)
|
||||
|
||||
const { data } = useTimelineQuery({
|
||||
page: 'Account',
|
||||
|
@ -14,7 +14,7 @@ const AccountInformationAvatar: React.FC = () => {
|
||||
|
||||
return (
|
||||
<GracefullyImage
|
||||
style={{ borderRadius: 8, overflow: 'hidden' }}
|
||||
style={{ borderRadius: StyleConstants.BorderRadius, overflow: 'hidden' }}
|
||||
dimension={{ width: StyleConstants.Avatar.L, height: StyleConstants.Avatar.L }}
|
||||
sources={{
|
||||
default: { uri: account?.avatar },
|
||||
|
@ -1,27 +1,147 @@
|
||||
import { ParseHTML } from '@components/Parse'
|
||||
import Button from '@components/Button'
|
||||
import { discardConfirmation } from '@components/discardConfirmation'
|
||||
import Icon from '@components/Icon'
|
||||
import CustomText from '@components/Text'
|
||||
import { queryClient } from '@utils/queryHooks'
|
||||
import { QueryKeyRelationship, useRelationshipMutation } from '@utils/queryHooks/relationship'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import layoutAnimation from '@utils/styles/layoutAnimation'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useContext } from 'react'
|
||||
import { View } from 'react-native'
|
||||
import React, { useContext, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Pressable, TextInput, View } from 'react-native'
|
||||
import AccountContext from '../Context'
|
||||
|
||||
const AccountInformationPrivateNote: React.FC = () => {
|
||||
const { relationship, pageMe } = useContext(AccountContext)
|
||||
if (pageMe) return null
|
||||
if (!relationship || pageMe) return null
|
||||
|
||||
const { colors } = useTheme()
|
||||
const { colors, mode } = useTheme()
|
||||
const { t } = useTranslation(['common', 'screenTabs'])
|
||||
|
||||
return relationship?.note ? (
|
||||
const [editing, setEditing] = useState(false)
|
||||
const [notes, setNotes] = useState(relationship?.note)
|
||||
|
||||
const queryKey: QueryKeyRelationship = ['Relationship', { id: relationship.id }]
|
||||
const mutation = useRelationshipMutation({
|
||||
onMutate: async vars => {
|
||||
await queryClient.cancelQueries({ queryKey })
|
||||
queryClient.setQueryData<Mastodon.Relationship[]>(queryKey, old => {
|
||||
return old
|
||||
? vars.type === 'note'
|
||||
? old.map(o => (o.id === relationship.id ? { ...o, note: notes } : o))
|
||||
: old
|
||||
: undefined
|
||||
})
|
||||
},
|
||||
onError: () => {
|
||||
queryClient.invalidateQueries(queryKey)
|
||||
}
|
||||
})
|
||||
const submit = () => {
|
||||
mutation.mutate({ id: relationship.id, type: 'note', payload: notes || '' })
|
||||
setEditing(!editing)
|
||||
layoutAnimation()
|
||||
}
|
||||
|
||||
return relationship?.following ? (
|
||||
editing ? (
|
||||
<View
|
||||
style={{
|
||||
marginBottom: StyleConstants.Spacing.L,
|
||||
padding: StyleConstants.Spacing.Global.PagePadding,
|
||||
borderWidth: 1,
|
||||
borderRadius: StyleConstants.BorderRadius,
|
||||
borderColor: colors.border
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
style={{
|
||||
flex: 1,
|
||||
borderBottomWidth: 1,
|
||||
...StyleConstants.FontStyle.M,
|
||||
color: colors.primaryDefault,
|
||||
borderBottomColor: colors.border,
|
||||
paddingVertical: StyleConstants.Spacing.S
|
||||
}}
|
||||
value={notes}
|
||||
onChangeText={setNotes}
|
||||
multiline
|
||||
textAlignVertical='top'
|
||||
clearButtonMode='never'
|
||||
onSubmitEditing={() => submit()}
|
||||
placeholder={t('screenTabs:shared.account.privateNote')}
|
||||
placeholderTextColor={colors.secondary}
|
||||
returnKeyType='done'
|
||||
keyboardAppearance={mode}
|
||||
autoFocus
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
marginTop: StyleConstants.Spacing.M,
|
||||
gap: StyleConstants.Spacing.S
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type='text'
|
||||
content={t('common:buttons.cancel')}
|
||||
onPress={() => {
|
||||
discardConfirmation({
|
||||
condition: notes != relationship?.note,
|
||||
action: () => {
|
||||
setEditing(false)
|
||||
layoutAnimation()
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<Button type='text' content={t('common:buttons.confirm')} onPress={() => submit()} />
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<Pressable
|
||||
style={{
|
||||
marginBottom: StyleConstants.Spacing.L,
|
||||
borderLeftColor: colors.border,
|
||||
borderLeftWidth: StyleConstants.Spacing.XS,
|
||||
paddingLeft: StyleConstants.Spacing.S
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
onPress={() => {
|
||||
setEditing(!editing)
|
||||
layoutAnimation()
|
||||
}}
|
||||
>
|
||||
<ParseHTML content={relationship.note} size={'S'} selectable numberOfLines={2} />
|
||||
</View>
|
||||
{!!relationship?.note.length ? (
|
||||
<CustomText
|
||||
fontSize='S'
|
||||
style={{
|
||||
color: colors.primaryDefault,
|
||||
paddingHorizontal: StyleConstants.Spacing.S,
|
||||
flexShrink: 1
|
||||
}}
|
||||
children={relationship.note}
|
||||
selectable
|
||||
numberOfLines={2}
|
||||
/>
|
||||
) : (
|
||||
<CustomText
|
||||
fontSize='S'
|
||||
style={{
|
||||
color: colors.secondary,
|
||||
paddingHorizontal: StyleConstants.Spacing.S,
|
||||
flexShrink: 1
|
||||
}}
|
||||
children={t('screenTabs:shared.account.privateNote')}
|
||||
selectable
|
||||
numberOfLines={2}
|
||||
/>
|
||||
)}
|
||||
<Icon name='edit' size={StyleConstants.Font.Size.M} color={colors.secondary} />
|
||||
</Pressable>
|
||||
)
|
||||
) : null
|
||||
}
|
||||
|
||||
|
@ -110,7 +110,7 @@ const TabSharedReport: React.FC<TabSharedStackScreenProps<'Tab-Shared-Report'>>
|
||||
margin: StyleConstants.Spacing.Global.PagePadding,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.red,
|
||||
borderRadius: 8
|
||||
borderRadius: StyleConstants.BorderRadius
|
||||
}}
|
||||
>
|
||||
<ComponentAccount account={account} props={{}} />
|
||||
|
@ -29,7 +29,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
|
||||
const { colors } = useTheme()
|
||||
const { t } = useTranslation(['componentTimeline', 'screenTabs'])
|
||||
|
||||
const [hasRemoteContent, setHasRemoteContent] = useState<boolean>(false)
|
||||
const [hasRemoteContent, setHasRemoteContent] = useState<boolean>(toot._remote || false)
|
||||
const queryKey: { local: QueryKeyTimeline; remote: QueryKeyTimeline } = {
|
||||
local: ['Timeline', { page: 'Toot', toot: toot.id, remote: false }],
|
||||
remote: ['Timeline', { page: 'Toot', toot: toot.id, remote: true }]
|
||||
@ -70,7 +70,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
|
||||
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />,
|
||||
headerBackVisible: false
|
||||
})
|
||||
navigation.setParams({ toot, queryKey: queryKey.local })
|
||||
navigation.setParams({ queryKey: queryKey.local })
|
||||
}, [hasRemoteContent])
|
||||
|
||||
const PREV_PER_BATCH = 1
|
||||
@ -236,12 +236,15 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
|
||||
return
|
||||
}
|
||||
|
||||
if ((query.data?.pages[0].body.length || 0) < data.length) {
|
||||
if ((query.data?.pages[0].body.length || 0) <= data.length) {
|
||||
if (!hasRemoteContent && (query.data?.pages[0].body.length || 0) <= data.length) {
|
||||
setHasRemoteContent(true)
|
||||
}
|
||||
|
||||
queryClient.cancelQueries(queryKey.local)
|
||||
queryClient.setQueryData<{ pages: { body: Mastodon.Status[] }[] }>(
|
||||
queryKey.local,
|
||||
old => {
|
||||
setHasRemoteContent(true)
|
||||
return {
|
||||
pages: [
|
||||
{
|
||||
@ -350,7 +353,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
|
||||
) : null}
|
||||
<TimelineDefault
|
||||
item={item}
|
||||
queryKey={item._remote ? queryKey.remote : queryKey.local}
|
||||
queryKey={queryKey.local}
|
||||
highlighted={toot.id === item.id}
|
||||
suppressSpoiler={
|
||||
toot.id !== item.id &&
|
||||
|
@ -79,7 +79,7 @@ const TabSharedUsers: React.FC<TabSharedStackScreenProps<'Tab-Shared-Users'>> =
|
||||
padding: StyleConstants.Spacing.S,
|
||||
borderColor: colors.border,
|
||||
borderWidth: 1,
|
||||
borderRadius: StyleConstants.Spacing.S
|
||||
borderRadius: StyleConstants.BorderRadius
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
|
@ -55,7 +55,7 @@ const ScreenTabs = () => {
|
||||
sources={{ default: { uri: avatarStatic } }}
|
||||
dimension={{ width: size, height: size }}
|
||||
style={{
|
||||
borderRadius: size,
|
||||
borderRadius: 99,
|
||||
overflow: 'hidden',
|
||||
borderWidth: focused ? 2 : 0,
|
||||
borderColor: focused ? colors.primaryDefault : color
|
||||
|
@ -54,13 +54,17 @@ const useInstanceQuery = (
|
||||
options?: UseQueryOptions<Mastodon.Instance<any>, AxiosError>
|
||||
}
|
||||
) => {
|
||||
const queryKey: QueryKeyInstance = params?.domain ? ['Instance', params] : ['Instance']
|
||||
const queryKey: QueryKeyInstance = params?.domain
|
||||
? ['Instance', { domain: params.domain }]
|
||||
: ['Instance']
|
||||
return useQuery(queryKey, queryFunction, {
|
||||
...params?.options,
|
||||
staleTime: Infinity,
|
||||
cacheTime: Infinity,
|
||||
...(!params?.domain && {
|
||||
onSuccess: data => setAccountStorage([{ key: 'version', value: data.version }])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export { useInstanceQuery }
|
||||
|
@ -65,6 +65,11 @@ type MutationVarsRelationship =
|
||||
notify?: boolean
|
||||
}
|
||||
}
|
||||
| {
|
||||
id: Mastodon.Account['id']
|
||||
type: 'note'
|
||||
payload: Mastodon.Relationship['note']
|
||||
}
|
||||
|
||||
const mutationFunction = async (params: MutationVarsRelationship) => {
|
||||
switch (params.type) {
|
||||
@ -83,11 +88,17 @@ const mutationFunction = async (params: MutationVarsRelationship) => {
|
||||
url: `accounts/${params.id}/${params.payload.state ? 'un' : ''}${params.payload.action}`,
|
||||
body
|
||||
}).then(res => res.body)
|
||||
case 'note':
|
||||
return apiInstance<Mastodon.Relationship>({
|
||||
method: 'post',
|
||||
url: `accounts/${params.id}/note`,
|
||||
body: { comment: params.payload }
|
||||
}).then(res => res.body)
|
||||
}
|
||||
}
|
||||
|
||||
const useRelationshipMutation = (
|
||||
options: UseMutationOptions<Mastodon.Relationship, AxiosError, MutationVarsRelationship>
|
||||
options?: UseMutationOptions<Mastodon.Relationship, AxiosError, MutationVarsRelationship>
|
||||
) => {
|
||||
return useMutation(mutationFunction, options)
|
||||
}
|
||||
|
@ -6,8 +6,10 @@ import {
|
||||
UseInfiniteQueryOptions,
|
||||
useMutation
|
||||
} from '@tanstack/react-query'
|
||||
import apiGeneral from '@utils/api/general'
|
||||
import { PagedResponse } from '@utils/api/helpers'
|
||||
import apiInstance from '@utils/api/instance'
|
||||
import { appendRemote } from '@utils/helpers/appendRemote'
|
||||
import { featureCheck } from '@utils/helpers/featureCheck'
|
||||
import { useNavState } from '@utils/navigation/navigators'
|
||||
import { queryClient } from '@utils/queryHooks'
|
||||
@ -24,7 +26,7 @@ export type QueryKeyTimeline = [
|
||||
'Timeline',
|
||||
(
|
||||
| {
|
||||
page: Exclude<App.Pages, 'Following' | 'Hashtag' | 'List' | 'Toot' | 'Account'>
|
||||
page: Exclude<App.Pages, 'Following' | 'Hashtag' | 'List' | 'Toot' | 'Account' | 'Explore'>
|
||||
}
|
||||
| {
|
||||
page: 'Following'
|
||||
@ -50,6 +52,7 @@ export type QueryKeyTimeline = [
|
||||
toot: Mastodon.Status['id']
|
||||
remote: boolean
|
||||
}
|
||||
| { page: 'Explore'; domain?: string }
|
||||
)
|
||||
]
|
||||
|
||||
@ -117,12 +120,24 @@ export const queryFunctionTimeline = async ({
|
||||
params
|
||||
})
|
||||
|
||||
case 'Trending':
|
||||
case 'Explore':
|
||||
if (page.domain) {
|
||||
return apiGeneral<Mastodon.Status[]>({
|
||||
method: 'get',
|
||||
domain: page.domain,
|
||||
url: 'api/v1/timelines/public',
|
||||
params: {
|
||||
...params,
|
||||
local: 'true'
|
||||
}
|
||||
}).then(res => ({ ...res, body: res.body.map(status => appendRemote.status(status)) }))
|
||||
} else {
|
||||
return apiInstance<Mastodon.Status[]>({
|
||||
method: 'get',
|
||||
url: 'trends/statuses',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
case 'Notifications':
|
||||
const notificationsFilter = getAccountStorage.object('notifications')
|
||||
|
@ -5,7 +5,7 @@ export type GlobalV0 = {
|
||||
// string
|
||||
'app.expo_token'?: string
|
||||
'app.prev_tab'?: keyof ScreenTabsStackParamList
|
||||
'app.prev_public_segment'?: Extract<App.Pages, 'Local' | 'LocalPublic' | 'Trending'>
|
||||
'app.prev_public_segment'?: Extract<App.Pages, 'Local' | 'LocalPublic' | 'Explore'>
|
||||
'app.language'?: string
|
||||
'app.theme'?: 'light' | 'dark' | 'auto'
|
||||
'app.theme.dark'?: 'lighter' | 'darker'
|
||||
@ -24,4 +24,10 @@ export type GlobalV0 = {
|
||||
'account.active'?: string
|
||||
// object
|
||||
accounts?: string[]
|
||||
|
||||
//// remote
|
||||
// string
|
||||
'remote.active'?: string
|
||||
// object
|
||||
remotes?: { title: string; domain: string }[]
|
||||
}
|
||||
|
@ -21,5 +21,7 @@ export const StyleConstants = {
|
||||
Global: { PagePadding: Base * 4 }
|
||||
},
|
||||
|
||||
BorderRadius: Base * 2,
|
||||
|
||||
Avatar: { XS: 32, S: 40, M: 48, L: 96 }
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
const adaptiveScale = (size: number, factor: number = 0) =>
|
||||
import { Platform } from 'react-native'
|
||||
|
||||
export const adaptiveScale = (size: number, factor: number = 0) =>
|
||||
factor ? Math.round(size + size * (factor / 8)) : size
|
||||
|
||||
export { adaptiveScale }
|
||||
export const isLargeDevice = (Platform.OS === 'ios' && Platform.isPad) || Platform.OS === 'macos'
|
||||
|
@ -83,9 +83,9 @@ const themeColors: {
|
||||
dark_darker: 'rgba(18, 18, 18, 0.5)'
|
||||
},
|
||||
backgroundOverlayInvert: {
|
||||
light: 'rgba(25, 25, 25, 0.5)',
|
||||
dark_lighter: 'rgba(0, 0, 0, 0.5)',
|
||||
dark_darker: 'rgba(0, 0, 0, 0.5)'
|
||||
light: 'rgba(25, 25, 25, 0.75)',
|
||||
dark_lighter: 'rgba(0, 0, 0, 0.75)',
|
||||
dark_darker: 'rgba(0, 0, 0, 0.75)'
|
||||
},
|
||||
|
||||
border: {
|
||||
|
Reference in New Issue
Block a user