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

Merge branch 'main' into candidate

This commit is contained in:
xmflsct
2023-02-12 19:50:29 +01:00
57 changed files with 830 additions and 332 deletions

View File

@ -1,9 +1,3 @@
Enjoy toooting! This version includes following improvements and fixes: Enjoy toooting! This version includes following improvements and fixes:
- Auto fetch remote content in conversations! - Added following remote instance
- Remember last read position in timeline! - Added set note of followed users
- 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

View File

@ -1,9 +1,3 @@
toooting愉快此版本包括以下改进和修复 toooting愉快此版本包括以下改进和修复
- 主动获取对话的远程内容 - 新增关注远程实例功能
- 自动加载上次我的关注的阅读位置 - 新增关注用户备注功能
- 用其它已登陆的账户关注用户
- 可添加举报细节
- 新增暂停自动播放gif动画选项
- 隐藏用户的转嘟
- 下划线高亮正在关注的话题标签
- 支持GoToSocial

View File

@ -68,7 +68,7 @@
<key>NSPhotoLibraryUsageDescription</key> <key>NSPhotoLibraryUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your camera roll to attach photos or videos to your toot</string> <string>Allow $(PRODUCT_NAME) to access your camera roll to attach photos or videos to your toot</string>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>SplashScreen.storyboard</string> <string>SplashScreen</string>
<key>UIRequiredDeviceCapabilities</key> <key>UIRequiredDeviceCapabilities</key>
<array> <array>
<string>armv7</string> <string>armv7</string>

View File

@ -1,6 +1,6 @@
{ {
"name": "tooot", "name": "tooot",
"version": "4.8.9", "version": "4.9.0",
"description": "tooot for Mastodon", "description": "tooot for Mastodon",
"author": "xmflsct <me@xmflsct.com>", "author": "xmflsct <me@xmflsct.com>",
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",

2
src/@types/app.d.ts vendored
View File

@ -3,7 +3,7 @@ declare namespace App {
| 'Following' | 'Following'
| 'Local' | 'Local'
| 'LocalPublic' | 'LocalPublic'
| 'Trending' | 'Explore'
| 'Notifications' | 'Notifications'
| 'Hashtag' | 'Hashtag'
| 'List' | 'List'

View File

@ -42,7 +42,7 @@ const ComponentAccount: React.FC<PropsWithChildren & Props> = ({ account, props,
style={{ style={{
width: StyleConstants.Avatar.S, width: StyleConstants.Avatar.S,
height: StyleConstants.Avatar.S, height: StyleConstants.Avatar.S,
borderRadius: 8, borderRadius: StyleConstants.BorderRadius,
marginRight: StyleConstants.Spacing.S marginRight: StyleConstants.Spacing.S
}} }}
dim dim

View File

@ -45,7 +45,7 @@ const AccountButton: React.FC<Props> = ({ account, additionalActions }) => {
width: StyleConstants.Font.Size.L, width: StyleConstants.Font.Size.L,
height: StyleConstants.Font.Size.L height: StyleConstants.Font.Size.L
}} }}
style={{ borderRadius: StyleConstants.Font.Size.L / 2, overflow: 'hidden' }} style={{ borderRadius: 99, overflow: 'hidden' }}
/> />
<CustomText <CustomText
fontStyle='M' fontStyle='M'

View File

@ -116,7 +116,7 @@ const Button: React.FC<Props> = ({
}} }}
style={[ style={[
{ {
borderRadius: 100, borderRadius: 99,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
borderWidth: overlay ? 0 : selected ? 1.5 : 1, borderWidth: overlay ? 0 : selected ? 1.5 : 1,

View File

@ -35,7 +35,7 @@ const EmojisButton: React.FC = () => {
borderWidth: 2, borderWidth: 2,
borderColor: colors.primaryDefault, borderColor: colors.primaryDefault,
padding: StyleConstants.Spacing.Global.PagePadding / 2, padding: StyleConstants.Spacing.Global.PagePadding / 2,
borderRadius: 100 borderRadius: 99
}} }}
> >
<Icon <Icon

View File

@ -51,7 +51,7 @@ const HeaderLeft: React.FC<Props> = ({
minWidth: 44, minWidth: 44,
marginLeft: native ? -StyleConstants.Spacing.S : StyleConstants.Spacing.S, marginLeft: native ? -StyleConstants.Spacing.S : StyleConstants.Spacing.S,
...(type === undefined && { ...(type === undefined && {
borderRadius: 100 borderRadius: 99
}), }),
...(type === 'text' && { ...(type === 'text' && {
paddingHorizontal: StyleConstants.Spacing.S paddingHorizontal: StyleConstants.Spacing.S

View File

@ -98,9 +98,7 @@ const HeaderRight: React.FC<Props> = ({
minHeight: 44, minHeight: 44,
minWidth: 44, minWidth: 44,
marginRight: native ? -StyleConstants.Spacing.S : StyleConstants.Spacing.S, marginRight: native ? -StyleConstants.Spacing.S : StyleConstants.Spacing.S,
...(type === undefined && { ...(type === undefined && { borderRadius: 99 }),
borderRadius: 100
}),
...(type === 'text' && { ...(type === 'text' && {
paddingHorizontal: StyleConstants.Spacing.S paddingHorizontal: StyleConstants.Spacing.S
}) })

View File

@ -22,7 +22,7 @@ export interface Props {
switchDisabled?: boolean switchDisabled?: boolean
switchOnValueChange?: () => void switchOnValueChange?: () => void
iconBack?: 'chevron-right' | 'external-link' | 'check' iconBack?: 'chevron-right' | 'chevron-down' | 'external-link' | 'check'
iconBackColor?: ColorDefinitions iconBackColor?: ColorDefinitions
loading?: boolean loading?: boolean
@ -66,14 +66,7 @@ const MenuRow: React.FC<Props> = ({
}} }}
> >
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<View <View style={{ flex: 1, flexDirection: 'row', justifyContent: 'space-between' }}>
style={{
flex: 1,
flexDirection: 'row',
justifyContent: 'space-between',
marginTop: StyleConstants.Spacing.S
}}
>
<View <View
style={{ style={{
flexShrink: 3, flexShrink: 3,
@ -96,7 +89,7 @@ const MenuRow: React.FC<Props> = ({
width: 8, width: 8,
height: 8, height: 8,
backgroundColor: colors.red, backgroundColor: colors.red,
borderRadius: 8, borderRadius: StyleConstants.BorderRadius,
marginRight: StyleConstants.Spacing.S marginRight: StyleConstants.Spacing.S
}} }}
/> />

View File

@ -29,20 +29,20 @@ const RelationshipOutgoing: React.FC<Props> = ({ id }: Props) => {
const queryKeyRelationship: QueryKeyRelationship = ['Relationship', { id }] const queryKeyRelationship: QueryKeyRelationship = ['Relationship', { id }]
const queryClient = useQueryClient() const queryClient = useQueryClient()
const mutation = useRelationshipMutation({ const mutation = useRelationshipMutation({
onSuccess: (res, { payload: { action } }) => { onSuccess: (res, vars) => {
haptics('Success') haptics('Success')
queryClient.setQueryData<Mastodon.Relationship[]>(queryKeyRelationship, [res]) queryClient.setQueryData<Mastodon.Relationship[]>(queryKeyRelationship, [res])
if (action === 'block') { if (vars.type === 'outgoing' && vars.payload.action === 'block') {
const queryKey = ['Timeline', { page: 'Following' }] const queryKey = ['Timeline', { page: 'Following' }]
queryClient.invalidateQueries({ queryKey, exact: false }) queryClient.invalidateQueries({ queryKey, exact: false })
} }
}, },
onError: (err: any, { payload: { action } }) => { onError: (err: any, vars) => {
displayMessage({ displayMessage({
theme, theme,
type: 'error', type: 'error',
message: t('common:message.error.message', { 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 && ...(err.status &&
typeof err.status === 'number' && typeof err.status === 'number' &&

View File

@ -31,9 +31,11 @@ export const SwipeToActions = <T extends unknown>({
haptics(action.haptic || 'Light') haptics(action.haptic || 'Light')
action.onPress({ item }) action.onPress({ item })
}} }}
style={{ backgroundColor: 'rgba(0, 255, 0, 0.2)' }}
> >
<View <View
style={{ style={{
flex: 1,
paddingHorizontal: StyleConstants.Spacing.L, paddingHorizontal: StyleConstants.Spacing.L,
flexBasis: perActionWidth, flexBasis: perActionWidth,
backgroundColor: action.color, backgroundColor: action.color,

View File

@ -67,7 +67,7 @@ const TimelineConversation: React.FC<Props> = ({ conversation, queryKey, highlig
<View style={{ flex: 1, width: '100%', flexDirection: 'row' }}> <View style={{ flex: 1, width: '100%', flexDirection: 'row' }}>
<View <View
style={{ style={{
borderRadius: 4, borderRadius: StyleConstants.BorderRadius,
overflow: 'hidden', overflow: 'hidden',
marginRight: StyleConstants.Spacing.S, marginRight: StyleConstants.Spacing.S,
width: StyleConstants.Avatar.M, width: StyleConstants.Avatar.M,

View File

@ -118,7 +118,7 @@ const AttachmentAudio: React.FC<Props> = ({ total, index, sensitiveShown, audio
width: '100%', width: '100%',
height: StyleConstants.Spacing.M + StyleConstants.Spacing.S * 2, height: StyleConstants.Spacing.M + StyleConstants.Spacing.S * 2,
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding, paddingHorizontal: StyleConstants.Spacing.Global.PagePadding,
borderRadius: 100, borderRadius: 99,
opacity: sensitiveShown ? 0.35 : undefined opacity: sensitiveShown ? 0.35 : undefined
}} }}
> >

View File

@ -9,8 +9,9 @@ import { StackNavigationProp } from '@react-navigation/stack'
import { RootStackParamList } from '@utils/navigation/navigators' import { RootStackParamList } from '@utils/navigation/navigators'
import { usePreferencesQuery } from '@utils/queryHooks/preferences' import { usePreferencesQuery } from '@utils/queryHooks/preferences'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { isLargeDevice } from '@utils/styles/scaling'
import { chunk } from 'lodash' import { chunk } from 'lodash'
import React, { useContext, useState } from 'react' import React, { Fragment, useContext, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Pressable, View } from 'react-native' import { Pressable, View } from 'react-native'
import StatusContext from '../Context' import StatusContext from '../Context'
@ -71,14 +72,9 @@ const TimelineAttachment = () => {
} }
default: default:
if ( if (
attachment.preview_url?.endsWith('.jpg') || // https://docs.expo.dev/versions/unversioned/sdk/image/#supported-image-formats
attachment.preview_url?.endsWith('.jpeg') || attachment.preview_url?.match(/.(?:a?png|jpe?g|webp|avif|heic|gif|svg|ico|icns)$/i) ||
attachment.preview_url?.endsWith('.png') || attachment.remote_url?.match(/.(?:a?png|jpe?g|webp|avif|heic|gif|svg|ico|icns)$/i)
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')
) { ) {
return { return {
id: attachment.id, id: attachment.id,
@ -179,11 +175,13 @@ const TimelineAttachment = () => {
style={{ style={{
marginTop: StyleConstants.Spacing.M, marginTop: StyleConstants.Spacing.M,
flex: 1, flex: 1,
gap: StyleConstants.Spacing.XS gap: StyleConstants.Spacing.XS,
...(isLargeDevice && { maxWidth: 375 })
}} }}
> >
{chunk(status.media_attachments, 2).map((chunk, chunkIndex) => ( {chunk(status.media_attachments, 2).map((chunk, chunkIndex) => (
<View <View
key={chunkIndex}
style={{ style={{
flex: 1, flex: 1,
flexDirection: 'row', flexDirection: 'row',
@ -193,7 +191,9 @@ const TimelineAttachment = () => {
gap: StyleConstants.Spacing.XS 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> </View>
))} ))}

View File

@ -49,7 +49,7 @@ const TimelineAvatar: React.FC<Props> = ({ account }) => {
} }
} }
style={{ style={{
borderRadius: StyleConstants.Avatar.M, borderRadius: 99,
overflow: 'hidden', overflow: 'hidden',
marginRight: StyleConstants.Spacing.S marginRight: StyleConstants.Spacing.S
}} }}

View File

@ -137,7 +137,7 @@ const TimelineCard: React.FC = () => {
flexDirection: 'row', flexDirection: 'row',
marginTop: StyleConstants.Spacing.M, marginTop: StyleConstants.Spacing.M,
borderWidth: 1, borderWidth: 1,
borderRadius: StyleConstants.Spacing.S, borderRadius: StyleConstants.BorderRadius,
overflow: 'hidden', overflow: 'hidden',
borderColor: colors.border borderColor: colors.border
}} }}

View File

@ -42,6 +42,7 @@ export interface Props {
'notifyOnChangeProps' | 'getNextPageParam' | 'getPreviousPageParam' | 'select' | 'onSuccess' 'notifyOnChangeProps' | 'getNextPageParam' | 'getPreviousPageParam' | 'select' | 'onSuccess'
> >
disableRefresh?: boolean disableRefresh?: boolean
refreshAutoRefetch?: boolean
disableInfinity?: boolean disableInfinity?: boolean
readMarker?: 'read_marker_following' readMarker?: 'read_marker_following'
customProps?: Partial<FlatListProps<any>> customProps?: Partial<FlatListProps<any>>
@ -52,6 +53,7 @@ const Timeline: React.FC<Props> = ({
queryKey, queryKey,
queryOptions, queryOptions,
disableRefresh = false, disableRefresh = false,
refreshAutoRefetch = true,
disableInfinity = false, disableInfinity = false,
readMarker = undefined, readMarker = undefined,
customProps customProps
@ -154,6 +156,7 @@ const Timeline: React.FC<Props> = ({
if ( if (
curr === true && curr === true &&
prev === false && prev === false &&
refreshAutoRefetch &&
!isFetchingPrev.value && !isFetchingPrev.value &&
fetchingType.value === 0 && fetchingType.value === 0 &&
shouldAutoFetch.value && shouldAutoFetch.value &&

View File

@ -105,21 +105,21 @@ const menuAccount = ({
}) })
const queryKeyRelationship: QueryKeyRelationship = ['Relationship', { id: actualAccount?.id }] const queryKeyRelationship: QueryKeyRelationship = ['Relationship', { id: actualAccount?.id }]
const relationshipMutation = useRelationshipMutation({ const relationshipMutation = useRelationshipMutation({
onSuccess: (res, { payload: { action } }) => { onSuccess: (res, vars) => {
haptics('Success') haptics('Success')
queryClient.setQueryData<Mastodon.Relationship[]>(queryKeyRelationship, [res]) queryClient.setQueryData<Mastodon.Relationship[]>(queryKeyRelationship, [res])
if (action === 'block') { if (vars.type === 'outgoing' && vars.payload.action === 'block') {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ['Timeline', { page: 'Following' }], queryKey: ['Timeline', { page: 'Following' }],
exact: false exact: false
}) })
} }
}, },
onError: (err: any, { payload: { action } }) => { onError: (err: any, vars) => {
displayMessage({ displayMessage({
type: 'danger', type: 'danger',
message: t('common:message.error.message', { 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 && ...(err.status &&
typeof err.status === 'number' && typeof err.status === 'number' &&

View 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()
}
}

View File

@ -4,9 +4,6 @@ import { Platform } from 'react-native'
const haptics = ( const haptics = (
type: 'Success' | 'Warning' | 'Error' | 'Light' | 'Medium' | 'Heavy' type: 'Success' | 'Warning' | 'Error' | 'Light' | 'Medium' | 'Heavy'
) => { ) => {
if (Platform.OS === 'ios' && parseInt(Platform.Version, 10) <= 12) {
return
}
if (Platform.OS === 'android') { if (Platform.OS === 'android') {
if (type === 'Error') { if (type === 'Error') {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle['Light']).catch(() => {}) Haptics.impactAsync(Haptics.ImpactFeedbackStyle['Light']).catch(() => {})

View File

@ -5,7 +5,7 @@ import i18next from 'i18next'
import { Asset, launchImageLibrary } from 'react-native-image-picker' import { Asset, launchImageLibrary } from 'react-native-image-picker'
const queryKeyInstance: QueryKeyInstance = ['Instance'] const queryKeyInstance: QueryKeyInstance = ['Instance']
export const MAX_MEDIA_ATTACHMENTS: number = export const MAX_MEDIA_ATTACHMENTS = (): number =>
queryClient.getQueryData<Mastodon.Instance<any>>(queryKeyInstance)?.configuration?.statuses queryClient.getQueryData<Mastodon.Instance<any>>(queryKeyInstance)?.configuration?.statuses
.max_media_attachments || 4 .max_media_attachments || 4
@ -27,7 +27,7 @@ const mediaSelector = async ({
indicateMaximum = false, indicateMaximum = false,
showActionSheetWithOptions showActionSheetWithOptions
}: Props): Promise<Asset[]> => { }: Props): Promise<Asset[]> => {
const _maximum = maximum || MAX_MEDIA_ATTACHMENTS const _maximum = maximum || MAX_MEDIA_ATTACHMENTS()
const options = () => { const options = () => {
switch (mediaType) { switch (mediaType) {

View File

@ -8,7 +8,8 @@
"create": "Create", "create": "Create",
"delete": "Delete", "delete": "Delete",
"done": "Done", "done": "Done",
"confirm": "Confirm" "confirm": "Confirm",
"add": "Add"
}, },
"customEmoji": { "customEmoji": {
"accessibilityLabel": "Custom emoji {{emoji}}" "accessibilityLabel": "Custom emoji {{emoji}}"

View File

@ -7,9 +7,7 @@
"button": "Login", "button": "Login",
"information": { "information": {
"name": "Name", "name": "Name",
"accounts": "Users", "description": "Description"
"statuses": "Toots",
"domains": "Universes"
}, },
"disclaimer": { "disclaimer": {
"base": "Logging in process uses system browser that, your account information won't be visible to tooot app." "base": "Logging in process uses system browser that, your account information won't be visible to tooot app."

View File

@ -11,7 +11,13 @@
"segments": { "segments": {
"federated": "Federated", "federated": "Federated",
"local": "Local", "local": "Local",
"trending": "Trending" "explore": "Explore"
},
"exploring": {
"heading": "Exploring",
"trending": "Trending",
"followRemote": "Follow remote instance",
"noTitle": "No Title"
} }
}, },
"notifications": { "notifications": {
@ -380,6 +386,7 @@
"accessibilityHint": "You can mute, block, report or share this user" "accessibilityHint": "You can mute, block, report or share this user"
}, },
"followed_by": " is following you", "followed_by": " is following you",
"privateNote": "Set private note",
"moved": "User moved", "moved": "User moved",
"created_at": "Joined: {{date}}", "created_at": "Joined: {{date}}",
"summary": { "summary": {

View File

@ -52,7 +52,7 @@ const Share = ({
padding: StyleConstants.Spacing.M, padding: StyleConstants.Spacing.M,
borderWidth: 1, borderWidth: 1,
borderColor: colors.shimmerHighlight, borderColor: colors.shimmerHighlight,
borderRadius: 8 borderRadius: StyleConstants.BorderRadius
}} }}
children={text} children={text}
/> />
@ -65,7 +65,7 @@ const Share = ({
padding: StyleConstants.Spacing.M, padding: StyleConstants.Spacing.M,
borderWidth: 1, borderWidth: 1,
borderColor: colors.shimmerHighlight, borderColor: colors.shimmerHighlight,
borderRadius: 8 borderRadius: StyleConstants.BorderRadius
}} }}
> >
<FlatList <FlatList

View File

@ -73,7 +73,7 @@ const ScreenAnnouncements: React.FC<RootStackScreenProps<'Screen-Announcements'>
padding: StyleConstants.Spacing.Global.PagePadding, padding: StyleConstants.Spacing.Global.PagePadding,
marginTop: StyleConstants.Spacing.Global.PagePadding, marginTop: StyleConstants.Spacing.Global.PagePadding,
borderWidth: 1, borderWidth: 1,
borderRadius: 6, borderRadius: StyleConstants.BorderRadius,
borderColor: colors.primaryDefault, borderColor: colors.primaryDefault,
backgroundColor: colors.backgroundDefault backgroundColor: colors.backgroundDefault
}} }}
@ -123,7 +123,7 @@ const ScreenAnnouncements: React.FC<RootStackScreenProps<'Screen-Announcements'>
marginTop: StyleConstants.Spacing.Global.PagePadding / 2, marginTop: StyleConstants.Spacing.Global.PagePadding / 2,
marginBottom: StyleConstants.Spacing.Global.PagePadding / 2, marginBottom: StyleConstants.Spacing.Global.PagePadding / 2,
marginRight: StyleConstants.Spacing.M, marginRight: StyleConstants.Spacing.M,
borderRadius: 6, borderRadius: StyleConstants.BorderRadius,
flexDirection: 'row', flexDirection: 'row',
borderColor: reaction.me ? colors.disabled : colors.primaryDefault, borderColor: reaction.me ? colors.disabled : colors.primaryDefault,
backgroundColor: reaction.me ? colors.disabled : colors.backgroundDefault backgroundColor: reaction.me ? colors.disabled : colors.backgroundDefault
@ -231,7 +231,7 @@ const ScreenAnnouncements: React.FC<RootStackScreenProps<'Screen-Announcements'>
style={{ style={{
width: StyleConstants.Spacing.S, width: StyleConstants.Spacing.S,
height: StyleConstants.Spacing.S, height: StyleConstants.Spacing.S,
borderRadius: StyleConstants.Spacing.S, borderRadius: StyleConstants.BorderRadius,
borderWidth: 1, borderWidth: 1,
borderColor: colors.primaryDefault, borderColor: colors.primaryDefault,
backgroundColor: i === index ? colors.primaryDefault : undefined, backgroundColor: i === index ? colors.primaryDefault : undefined,
@ -271,7 +271,7 @@ const ScreenAnnouncements: React.FC<RootStackScreenProps<'Screen-Announcements'>
style={{ style={{
width: StyleConstants.Spacing.S, width: StyleConstants.Spacing.S,
height: StyleConstants.Spacing.S, height: StyleConstants.Spacing.S,
borderRadius: StyleConstants.Spacing.S, borderRadius: StyleConstants.BorderRadius,
borderWidth: 1, borderWidth: 1,
borderColor: colors.primaryDefault, borderColor: colors.primaryDefault,
backgroundColor: i === index ? colors.primaryDefault : undefined, backgroundColor: i === index ? colors.primaryDefault : undefined,

View File

@ -56,7 +56,7 @@ const ComposeDraftsList: React.FC<ScreenComposeStackScreenProps<'Screen-Compose-
padding: StyleConstants.Spacing.S, padding: StyleConstants.Spacing.S,
borderColor: colors.border, borderColor: colors.border,
borderWidth: 1, borderWidth: 1,
borderRadius: StyleConstants.Spacing.S, borderRadius: StyleConstants.BorderRadius,
marginBottom: StyleConstants.Spacing.S marginBottom: StyleConstants.Spacing.S
}} }}
> >

View File

@ -1,3 +1,4 @@
import { discardConfirmation } from '@components/discardConfirmation'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import { HeaderLeft, HeaderRight } from '@components/Header' import { HeaderLeft, HeaderRight } from '@components/Header'
import { ModalScrollView } from '@components/ModalScrollView' import { ModalScrollView } from '@components/ModalScrollView'
@ -6,9 +7,11 @@ import apiInstance from '@utils/api/instance'
import { ScreenComposeStackScreenProps } from '@utils/navigation/navigators' import { ScreenComposeStackScreenProps } from '@utils/navigation/navigators'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import { Image } from 'expo-image'
import React, { useContext, useEffect, useState } from 'react' import React, { useContext, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' 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' import ComposeContext from './utils/createContext'
const ComposeEditAttachment: React.FC< const ComposeEditAttachment: React.FC<
@ -20,7 +23,7 @@ const ComposeEditAttachment: React.FC<
} }
}) => { }) => {
const { colors } = useTheme() const { colors } = useTheme()
const { t } = useTranslation('screenCompose') const { t } = useTranslation(['common', 'screenCompose'])
const { composeState, composeDispatch } = useContext(ComposeContext) const { composeState, composeDispatch } = useContext(ComposeContext)
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
@ -31,55 +34,89 @@ const ComposeEditAttachment: React.FC<
return null return null
} }
const [altText, setAltText] = useState<string | undefined>(theAttachment.description)
useEffect(() => { useEffect(() => {
navigation.setOptions({ navigation.setOptions({
title: t('content.editAttachment.header.title'), title: t('screenCompose:content.editAttachment.header.title'),
headerLeft: () => <HeaderLeft content='chevron-down' onPress={() => navigation.goBack()} />, headerLeft: () => (
<HeaderLeft
content='chevron-down'
onPress={() => {
discardConfirmation({
condition: theAttachment.description != altText,
action: () => navigation.goBack()
})
}}
/>
),
headerRight: () => ( headerRight: () => (
<HeaderRight <HeaderRight
accessibilityLabel={t('content.editAttachment.header.right.accessibilityLabel')} accessibilityLabel={t(
content='save' 'screenCompose:content.editAttachment.header.right.accessibilityLabel'
)}
type='text'
content={t('common:buttons.apply')}
loading={isSubmitting} loading={isSubmitting}
onPress={() => { onPress={() => {
if (composeState.type === 'edit') { if (composeState.type === 'edit') {
composeDispatch({ type: 'attachment/edit', payload: { ...theAttachment } }) composeDispatch({
type: 'attachment/edit',
payload: { ...theAttachment, description: altText }
})
navigation.goBack() navigation.goBack()
return return
} }
setIsSubmitting(true)
const body = { description: theAttachment.description }
theAttachment?.id && theAttachment?.id &&
apiInstance<Mastodon.Attachment>({ apiInstance<Mastodon.Attachment>({
method: 'put', method: 'put',
url: `media/${theAttachment.id}`, url: `media/${theAttachment.id}`,
body body: { description: altText }
}) })
.then(() => { .then(res => {
setIsSubmitting(false)
haptics('Success') haptics('Success')
composeDispatch({
type: 'attachment/edit',
payload: res.body
})
navigation.goBack() navigation.goBack()
}) })
.catch(() => { .catch(() => {
setIsSubmitting(false) setIsSubmitting(false)
haptics('Error') haptics('Error')
Alert.alert(t('content.editAttachment.header.right.failed.title'), undefined, [ Alert.alert(
{ t('screenCompose:content.editAttachment.header.right.failed.title'),
text: t('content.editAttachment.header.right.failed.button'), undefined,
style: 'cancel' [
} {
]) text: t('screenCompose:content.editAttachment.header.right.failed.button'),
style: 'cancel'
}
]
)
}) })
}} }}
/> />
) )
}) })
}, [theAttachment]) }, [theAttachment, altText])
return ( return (
<ModalScrollView> <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'> <CustomText fontStyle='M' style={{ color: colors.primaryDefault }} fontWeight='Bold'>
{t('content.editAttachment.content.altText.heading')} {t('screenCompose:content.editAttachment.content.altText.heading')}
</CustomText> </CustomText>
<TextInput <TextInput
style={{ style={{
@ -94,18 +131,10 @@ const ComposeEditAttachment: React.FC<
}} }}
maxLength={1500} maxLength={1500}
multiline multiline
onChangeText={e => value={altText}
composeDispatch({ onChangeText={e => setAltText(e)}
type: 'attachment/edit', placeholder={t('screenCompose:content.editAttachment.content.altText.placeholder')}
payload: {
...theAttachment,
description: e
}
})
}
placeholder={t('content.editAttachment.content.altText.placeholder')}
placeholderTextColor={colors.secondary} placeholderTextColor={colors.secondary}
value={theAttachment.description}
/> />
<CustomText <CustomText
fontStyle='S' fontStyle='S'

View File

@ -30,7 +30,7 @@ const ComposeActions: React.FC = () => {
const attachmentOnPress = () => { const attachmentOnPress = () => {
if (composeState.poll.active) return 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 }) return chooseAndUploadAttachment({ composeDispatch, showActionSheetWithOptions })
} }
} }

View File

@ -12,19 +12,19 @@ import { StyleConstants } from '@utils/styles/constants'
import layoutAnimation from '@utils/styles/layoutAnimation' import layoutAnimation from '@utils/styles/layoutAnimation'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import { Image } from 'expo-image' 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 { useTranslation } from 'react-i18next'
import { FlatList, Pressable, StyleSheet, View } from 'react-native' import { FlatList, Pressable, StyleSheet, View } from 'react-native'
import ComposeContext from '../../utils/createContext' import ComposeContext from '../../utils/createContext'
import { ExtendedAttachment } from '../../utils/types' import { ExtendedAttachment } from '../../utils/types'
import chooseAndUploadAttachment from './addAttachment' import chooseAndUploadAttachment from './addAttachment'
export const DEFAULT_WIDTH = 150
export interface Props { export interface Props {
accessibleRefAttachments: RefObject<View> accessibleRefAttachments: RefObject<View>
} }
const DEFAULT_HEIGHT = 200
const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => { const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
const { showActionSheetWithOptions } = useActionSheet() const { showActionSheetWithOptions } = useActionSheet()
const { composeState, composeDispatch } = useContext(ComposeContext) const { composeState, composeDispatch } = useContext(ComposeContext)
@ -40,72 +40,22 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
payload: { sensitive: !composeState.attachments.sensitive } 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 }) => { const renderAttachment = ({ item, index }: { item: ExtendedAttachment; index: number }) => {
return ( return (
<View <View
key={index} key={index}
style={{ style={{
height: DEFAULT_HEIGHT,
marginLeft: StyleConstants.Spacing.Global.PagePadding, marginLeft: StyleConstants.Spacing.Global.PagePadding,
marginTop: StyleConstants.Spacing.Global.PagePadding, marginTop: StyleConstants.Spacing.Global.PagePadding,
marginBottom: StyleConstants.Spacing.Global.PagePadding, marginBottom: StyleConstants.Spacing.Global.PagePadding
width: calculateWidth(item)
}} }}
> >
<Image <Image
style={{ width: '100%', height: '100%' }} style={{
width: DEFAULT_WIDTH,
height: DEFAULT_WIDTH,
borderRadius: StyleConstants.BorderRadius
}}
source={ source={
item.local?.thumbnail item.local?.thumbnail
? { uri: item.local?.thumbnail } ? { uri: item.local?.thumbnail }
@ -123,7 +73,7 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
paddingRight: StyleConstants.Spacing.S, paddingRight: StyleConstants.Spacing.S,
paddingTop: StyleConstants.Spacing.XS, paddingTop: StyleConstants.Spacing.XS,
paddingBottom: StyleConstants.Spacing.XS, paddingBottom: StyleConstants.Spacing.XS,
color: colors.backgroundDefault, color: colors.primaryOverlay,
backgroundColor: colors.backgroundOverlayInvert backgroundColor: colors.backgroundOverlayInvert
}} }}
> >
@ -157,7 +107,7 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
})} })}
type='icon' type='icon'
content='x' content='x'
spacing='M' size='L'
round round
overlay overlay
onPress={() => { onPress={() => {
@ -175,11 +125,11 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
accessibilityLabel={t('content.root.footer.attachments.edit.accessibilityLabel', { accessibilityLabel={t('content.root.footer.attachments.edit.accessibilityLabel', {
attachment: index + 1 attachment: index + 1
})} })}
type='icon'
content='edit'
spacing='M'
round
overlay overlay
size='S'
type='text'
content={!!item.remote?.description?.length ? 'ALT ✓' : '+ ALT'}
fontBold
onPress={() => navigation.navigate('Screen-Compose-EditAttachment', { index })} onPress={() => navigation.navigate('Screen-Compose-EditAttachment', { index })}
/> />
) : null} ) : null}
@ -230,23 +180,23 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
pagingEnabled={false} pagingEnabled={false}
snapToAlignment='center' snapToAlignment='center'
renderItem={renderAttachment} renderItem={renderAttachment}
snapToOffsets={snapToOffsets()} snapToOffsets={new Array(composeState.attachments.uploads.length).fill(DEFAULT_WIDTH)}
keyboardShouldPersistTaps='always' keyboardShouldPersistTaps='always'
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}
data={composeState.attachments.uploads} data={composeState.attachments.uploads}
keyExtractor={item => item.local?.uri || item.remote?.url || Math.random().toString()} keyExtractor={item => item.local?.uri || item.remote?.url || Math.random().toString()}
ListFooterComponent={ ListFooterComponent={
composeState.attachments.uploads.length < MAX_MEDIA_ATTACHMENTS ? ( composeState.attachments.uploads.length < MAX_MEDIA_ATTACHMENTS() ? (
<Pressable <Pressable
accessible accessible
accessibilityLabel={t('content.root.footer.attachments.upload.accessibilityLabel')} accessibilityLabel={t('content.root.footer.attachments.upload.accessibilityLabel')}
style={{ style={{
height: DEFAULT_HEIGHT, width: DEFAULT_WIDTH,
height: DEFAULT_WIDTH,
marginLeft: StyleConstants.Spacing.Global.PagePadding, marginLeft: StyleConstants.Spacing.Global.PagePadding,
marginTop: StyleConstants.Spacing.Global.PagePadding, marginTop: StyleConstants.Spacing.Global.PagePadding,
marginBottom: StyleConstants.Spacing.Global.PagePadding, marginBottom: StyleConstants.Spacing.Global.PagePadding,
width: DEFAULT_HEIGHT, backgroundColor: colors.disabled
backgroundColor: colors.backgroundOverlayInvert
}} }}
onPress={async () => { onPress={async () => {
await chooseAndUploadAttachment({ await chooseAndUploadAttachment({
@ -258,9 +208,7 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
<Button <Button
type='icon' type='icon'
content='upload-cloud' content='upload-cloud'
spacing='M' size='L'
round
overlay
onPress={async () => { onPress={async () => {
await chooseAndUploadAttachment({ await chooseAndUploadAttachment({
composeDispatch, composeDispatch,
@ -270,10 +218,11 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
style={{ style={{
position: 'absolute', position: 'absolute',
top: top:
(DEFAULT_HEIGHT - StyleConstants.Spacing.M * 2 - StyleConstants.Font.Size.M) / (DEFAULT_WIDTH - StyleConstants.Spacing.M * 2 - StyleConstants.Font.Size.M) / 2,
2,
left: 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> </Pressable>

View File

@ -38,8 +38,8 @@ const ComposePoll: React.FC = () => {
<View <View
style={{ style={{
flex: 1, flex: 1,
borderWidth: StyleSheet.hairlineWidth, borderWidth: 1,
borderRadius: 6, borderRadius: StyleConstants.BorderRadius,
margin: StyleConstants.Spacing.Global.PagePadding, margin: StyleConstants.Spacing.Global.PagePadding,
borderColor: colors.border borderColor: colors.border
}} }}
@ -70,7 +70,7 @@ const ComposePoll: React.FC = () => {
flex: 1, flex: 1,
padding: StyleConstants.Spacing.S, padding: StyleConstants.Spacing.S,
borderWidth: StyleSheet.hairlineWidth, borderWidth: StyleSheet.hairlineWidth,
borderRadius: 6, borderRadius: StyleConstants.BorderRadius,
...StyleConstants.FontStyle.M, ...StyleConstants.FontStyle.M,
marginLeft: StyleConstants.Spacing.S, marginLeft: StyleConstants.Spacing.S,
borderColor: colors.border, borderColor: colors.border,

View File

@ -17,7 +17,7 @@ const ComposeReply: React.FC = () => {
flex: 1, flex: 1,
flexDirection: 'row', flexDirection: 'row',
borderWidth: 1, borderWidth: 1,
borderRadius: StyleConstants.Spacing.S, borderRadius: StyleConstants.BorderRadius,
overflow: 'hidden', overflow: 'hidden',
borderColor: colors.border, borderColor: colors.border,
marginHorizontal: StyleConstants.Spacing.Global.PagePadding, marginHorizontal: StyleConstants.Spacing.Global.PagePadding,

View File

@ -71,7 +71,7 @@ const ComposeTextInput: React.FC = () => {
scrollEnabled={false} scrollEnabled={false}
disableCopyPaste={false} disableCopyPaste={false}
onPaste={(error: string | null | undefined, files: PastedFile[]) => { 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( Alert.alert(
t('screenCompose:content.root.header.textInput.keyboardImage.exceedMaximum.title'), t('screenCompose:content.root.header.textInput.keyboardImage.exceedMaximum.title'),
undefined, undefined,

View File

@ -28,7 +28,39 @@ const composeInitialState: Omit<ComposeState, 'timestamp'> = {
}, },
attachments: { attachments: {
sensitive: false, 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', visibility: 'public',
visibilityLock: false, visibilityLock: false,

View File

@ -1,25 +1,23 @@
import { discardConfirmation } from '@components/discardConfirmation'
import { EmojisState } from '@components/Emojis/Context' import { EmojisState } from '@components/Emojis/Context'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import { HeaderLeft, HeaderRight } from '@components/Header' import { HeaderLeft, HeaderRight } from '@components/Header'
import ComponentInput from '@components/Input' import ComponentInput from '@components/Input'
import { displayMessage, Message } from '@components/Message' import { displayMessage, Message } from '@components/Message'
import Selections from '@components/Selections' import Selections from '@components/Selections'
import CustomText from '@components/Text'
import { CommonActions } from '@react-navigation/native' import { CommonActions } from '@react-navigation/native'
import { useQueryClient } from '@tanstack/react-query' import { useQueryClient } from '@tanstack/react-query'
import { TabMeStackScreenProps } from '@utils/navigation/navigators' import { TabMeStackScreenProps } from '@utils/navigation/navigators'
import { QueryKeyLists, useListsMutation } from '@utils/queryHooks/lists' import { QueryKeyLists, useListsMutation } from '@utils/queryHooks/lists'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' 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'>> = ({ const TabMeListEdit: React.FC<TabMeStackScreenProps<'Tab-Me-List-Edit'>> = ({
navigation, navigation,
route: { params } route: { params }
}) => { }) => {
const { colors } = useTheme()
const { t } = useTranslation(['common', 'screenTabs']) const { t } = useTranslation(['common', 'screenTabs'])
const messageRef = useRef(null) const messageRef = useRef(null)
@ -92,21 +90,10 @@ const TabMeListEdit: React.FC<TabMeStackScreenProps<'Tab-Me-List-Edit'>> = ({
<HeaderLeft <HeaderLeft
content='x' content='x'
onPress={() => { onPress={() => {
if (params.type === 'edit' ? params.payload.title !== title : title.length) { discardConfirmation({
Alert.alert(t('common:discard.title'), t('common:discard.message'), [ condition: params.type === 'edit' ? params.payload.title !== title : !!title.length,
{ action: () => navigation.pop(1)
text: t('common:buttons.discard'), })
style: 'destructive',
onPress: () => navigation.pop(1)
},
{
text: t('common:buttons.cancel'),
style: 'default'
}
])
} else {
navigation.pop(1)
}
}} }}
/> />
), ),

View File

@ -1,3 +1,4 @@
import { discardConfirmation } from '@components/discardConfirmation'
import { ComponentEmojis } from '@components/Emojis' import { ComponentEmojis } from '@components/Emojis'
import { EmojisState } from '@components/Emojis/Context' import { EmojisState } from '@components/Emojis/Context'
import { HeaderLeft, HeaderRight } from '@components/Header' import { HeaderLeft, HeaderRight } from '@components/Header'
@ -9,7 +10,7 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { Dispatch, RefObject, SetStateAction, useEffect, useRef, useState } from 'react' import React, { Dispatch, RefObject, SetStateAction, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' 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' import FlashMessage from 'react-native-flash-message'
const Field: React.FC<{ const Field: React.FC<{
@ -92,21 +93,10 @@ const TabMeProfileFields: React.FC<
<HeaderLeft <HeaderLeft
content='chevron-left' content='chevron-left'
onPress={() => { onPress={() => {
if (dirty) { discardConfirmation({
Alert.alert(t('common:discard.title'), t('common:discard.message'), [ condition: dirty,
{ action: () => navigation.navigate('Tab-Me-Profile-Root')
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')
}
}} }}
/> />
), ),

View File

@ -1,3 +1,4 @@
import { discardConfirmation } from '@components/discardConfirmation'
import { ComponentEmojis } from '@components/Emojis' import { ComponentEmojis } from '@components/Emojis'
import { EmojisState } from '@components/Emojis/Context' import { EmojisState } from '@components/Emojis/Context'
import { HeaderLeft, HeaderRight } from '@components/Header' import { HeaderLeft, HeaderRight } from '@components/Header'
@ -7,8 +8,7 @@ import { useProfileMutation } from '@utils/queryHooks/profile'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { RefObject, useEffect, useRef, useState } from 'react' import React, { RefObject, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { ScrollView, TextInput } from 'react-native'
import { Alert, ScrollView, TextInput } from 'react-native'
import FlashMessage from 'react-native-flash-message' import FlashMessage from 'react-native-flash-message'
const TabMeProfileName: React.FC< const TabMeProfileName: React.FC<
@ -23,7 +23,6 @@ const TabMeProfileName: React.FC<
navigation navigation
}) => { }) => {
const { theme } = useTheme() const { theme } = useTheme()
const { t } = useTranslation(['common'])
const { mutateAsync, status } = useProfileMutation() const { mutateAsync, status } = useProfileMutation()
const [value, setValue] = useState(display_name) const [value, setValue] = useState(display_name)
@ -46,21 +45,10 @@ const TabMeProfileName: React.FC<
<HeaderLeft <HeaderLeft
content='chevron-left' content='chevron-left'
onPress={() => { onPress={() => {
if (dirty) { discardConfirmation({
Alert.alert(t('common:discard.title'), t('common:discard.message'), [ condition: dirty,
{ action: () => navigation.navigate('Tab-Me-Profile-Root')
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')
}
}} }}
/> />
), ),

View File

@ -1,3 +1,4 @@
import { discardConfirmation } from '@components/discardConfirmation'
import { ComponentEmojis } from '@components/Emojis' import { ComponentEmojis } from '@components/Emojis'
import { EmojisState } from '@components/Emojis/Context' import { EmojisState } from '@components/Emojis/Context'
import { HeaderLeft, HeaderRight } from '@components/Header' import { HeaderLeft, HeaderRight } from '@components/Header'
@ -7,8 +8,7 @@ import { useProfileMutation } from '@utils/queryHooks/profile'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { RefObject, useEffect, useRef, useState } from 'react' import React, { RefObject, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { ScrollView, TextInput } from 'react-native'
import { Alert, ScrollView, TextInput } from 'react-native'
import FlashMessage from 'react-native-flash-message' import FlashMessage from 'react-native-flash-message'
const TabMeProfileNote: React.FC< const TabMeProfileNote: React.FC<
@ -23,7 +23,6 @@ const TabMeProfileNote: React.FC<
navigation navigation
}) => { }) => {
const { theme } = useTheme() const { theme } = useTheme()
const { t } = useTranslation(['common'])
const { mutateAsync, status } = useProfileMutation() const { mutateAsync, status } = useProfileMutation()
const [notes, setNotes] = useState(note) const [notes, setNotes] = useState(note)
@ -46,21 +45,10 @@ const TabMeProfileNote: React.FC<
<HeaderLeft <HeaderLeft
content='chevron-left' content='chevron-left'
onPress={() => { onPress={() => {
if (dirty) { discardConfirmation({
Alert.alert(t('common:discard.title'), t('common:discard.message'), [ condition: dirty,
{ action: () => navigation.navigate('Tab-Me-Profile-Root')
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')
}
}} }}
/> />
), ),

View File

@ -1,3 +1,4 @@
import { discardConfirmation } from '@components/discardConfirmation'
import { HeaderLeft, HeaderRight } from '@components/Header' import { HeaderLeft, HeaderRight } from '@components/Header'
import { MenuContainer, MenuRow } from '@components/Menu' import { MenuContainer, MenuRow } from '@components/Menu'
import { useQueryClient } from '@tanstack/react-query' import { useQueryClient } from '@tanstack/react-query'
@ -8,7 +9,6 @@ import { setAccountStorage, useAccountStorage } from '@utils/storage/actions'
import { isEqual } from 'lodash' import { isEqual } from 'lodash'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Alert } from 'react-native'
import { ScrollView } from 'react-native-gesture-handler' import { ScrollView } from 'react-native-gesture-handler'
const TabNotificationsFilters: React.FC< const TabNotificationsFilters: React.FC<
@ -28,21 +28,7 @@ const TabNotificationsFilters: React.FC<
<HeaderLeft <HeaderLeft
content='chevron-down' content='chevron-down'
onPress={() => { onPress={() => {
if (changed) { discardConfirmation({ condition: changed, action: () => navigation.goBack() })
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()
}
}} }}
/> />
), ),

View File

@ -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 Timeline from '@components/Timeline'
import SegmentedControl from '@react-native-segmented-control/segmented-control' import SegmentedControl from '@react-native-segmented-control/segmented-control'
import { useScrollToTop } from '@react-navigation/native'
import { NativeStackScreenProps } from '@react-navigation/native-stack' import { NativeStackScreenProps } from '@react-navigation/native-stack'
import apiGeneral from '@utils/api/general'
import { TabPublicStackParamList } from '@utils/navigation/navigators' import { TabPublicStackParamList } from '@utils/navigation/navigators'
import { useInstanceQuery } from '@utils/queryHooks/instance'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' 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 { 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 { 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 { 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 { 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 }] const queryKey: QueryKeyTimeline = ['Timeline', { page }]
return <Timeline queryKey={queryKey} disableRefresh={page === 'Trending'} /> return <Timeline queryKey={queryKey} />
} }
const renderScene = SceneMap({ const renderScene = SceneMap({
Local: Route, Local: Route,
LocalPublic: Route, LocalPublic: Route,
Trending: Route Explore
}) })
const Root: React.FC<NativeStackScreenProps<TabPublicStackParamList, 'Tab-Public-Root'>> = ({ 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 { mode } = useTheme()
const { t } = useTranslation('screenTabs') 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>( const [segment, setSegment] = useState<number>(
Math.max( Math.max(
0, 0,
@ -39,12 +402,19 @@ const Root: React.FC<NativeStackScreenProps<TabPublicStackParamList, 'Tab-Public
const [routes] = useState([ const [routes] = useState([
{ key: 'Local', title: t('tabs.public.segments.local') }, { key: 'Local', title: t('tabs.public.segments.local') },
{ key: 'LocalPublic', title: t('tabs.public.segments.federated') }, { 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(() => { useEffect(() => {
const page = segments[segment] const page = segments[segment]
page && navigation.setParams({ queryKey: ['Timeline', { page }] }) page &&
}, [segment]) navigation.setParams({
queryKey: [
'Timeline',
{ page, ...(page === 'Explore' && remoteActive && { domain: remoteActive }) }
]
})
}, [segment, remoteActive])
useEffect(() => { useEffect(() => {
navigation.setOptions({ navigation.setOptions({

View File

@ -6,6 +6,7 @@ import { TabLocalStackParamList } from '@utils/navigation/navigators'
import { useTimelineQuery } from '@utils/queryHooks/timeline' import { useTimelineQuery } from '@utils/queryHooks/timeline'
import { flattenPages } from '@utils/queryHooks/utils' import { flattenPages } from '@utils/queryHooks/utils'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { isLargeDevice } from '@utils/styles/scaling'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext } from 'react' import React, { useContext } from 'react'
import { Dimensions, Pressable, View } from 'react-native' import { Dimensions, Pressable, View } from 'react-native'
@ -20,9 +21,11 @@ const AccountAttachments: React.FC = () => {
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>() const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const { colors } = useTheme() 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({ const { data } = useTimelineQuery({
page: 'Account', page: 'Account',

View File

@ -14,7 +14,7 @@ const AccountInformationAvatar: React.FC = () => {
return ( return (
<GracefullyImage <GracefullyImage
style={{ borderRadius: 8, overflow: 'hidden' }} style={{ borderRadius: StyleConstants.BorderRadius, overflow: 'hidden' }}
dimension={{ width: StyleConstants.Avatar.L, height: StyleConstants.Avatar.L }} dimension={{ width: StyleConstants.Avatar.L, height: StyleConstants.Avatar.L }}
sources={{ sources={{
default: { uri: account?.avatar }, default: { uri: account?.avatar },

View File

@ -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 { StyleConstants } from '@utils/styles/constants'
import layoutAnimation from '@utils/styles/layoutAnimation'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext } from 'react' import React, { useContext, useState } from 'react'
import { View } from 'react-native' import { useTranslation } from 'react-i18next'
import { Pressable, TextInput, View } from 'react-native'
import AccountContext from '../Context' import AccountContext from '../Context'
const AccountInformationPrivateNote: React.FC = () => { const AccountInformationPrivateNote: React.FC = () => {
const { relationship, pageMe } = useContext(AccountContext) 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)
<View const [notes, setNotes] = useState(relationship?.note)
style={{
marginBottom: StyleConstants.Spacing.L, const queryKey: QueryKeyRelationship = ['Relationship', { id: relationship.id }]
borderLeftColor: colors.border, const mutation = useRelationshipMutation({
borderLeftWidth: StyleConstants.Spacing.XS, onMutate: async vars => {
paddingLeft: StyleConstants.Spacing.S await queryClient.cancelQueries({ queryKey })
}} queryClient.setQueryData<Mastodon.Relationship[]>(queryKey, old => {
> return old
<ParseHTML content={relationship.note} size={'S'} selectable numberOfLines={2} /> ? vars.type === 'note'
</View> ? 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,
flexDirection: 'row',
alignItems: 'center'
}}
onPress={() => {
setEditing(!editing)
layoutAnimation()
}}
>
{!!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 ) : null
} }

View File

@ -110,7 +110,7 @@ const TabSharedReport: React.FC<TabSharedStackScreenProps<'Tab-Shared-Report'>>
margin: StyleConstants.Spacing.Global.PagePadding, margin: StyleConstants.Spacing.Global.PagePadding,
borderWidth: 1, borderWidth: 1,
borderColor: colors.red, borderColor: colors.red,
borderRadius: 8 borderRadius: StyleConstants.BorderRadius
}} }}
> >
<ComponentAccount account={account} props={{}} /> <ComponentAccount account={account} props={{}} />

View File

@ -29,7 +29,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
const { colors } = useTheme() const { colors } = useTheme()
const { t } = useTranslation(['componentTimeline', 'screenTabs']) 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 } = { const queryKey: { local: QueryKeyTimeline; remote: QueryKeyTimeline } = {
local: ['Timeline', { page: 'Toot', toot: toot.id, remote: false }], local: ['Timeline', { page: 'Toot', toot: toot.id, remote: false }],
remote: ['Timeline', { page: 'Toot', toot: toot.id, remote: true }] 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()} />, headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />,
headerBackVisible: false headerBackVisible: false
}) })
navigation.setParams({ toot, queryKey: queryKey.local }) navigation.setParams({ queryKey: queryKey.local })
}, [hasRemoteContent]) }, [hasRemoteContent])
const PREV_PER_BATCH = 1 const PREV_PER_BATCH = 1
@ -236,12 +236,15 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
return 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.cancelQueries(queryKey.local)
queryClient.setQueryData<{ pages: { body: Mastodon.Status[] }[] }>( queryClient.setQueryData<{ pages: { body: Mastodon.Status[] }[] }>(
queryKey.local, queryKey.local,
old => { old => {
setHasRemoteContent(true)
return { return {
pages: [ pages: [
{ {
@ -350,7 +353,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
) : null} ) : null}
<TimelineDefault <TimelineDefault
item={item} item={item}
queryKey={item._remote ? queryKey.remote : queryKey.local} queryKey={queryKey.local}
highlighted={toot.id === item.id} highlighted={toot.id === item.id}
suppressSpoiler={ suppressSpoiler={
toot.id !== item.id && toot.id !== item.id &&

View File

@ -79,7 +79,7 @@ const TabSharedUsers: React.FC<TabSharedStackScreenProps<'Tab-Shared-Users'>> =
padding: StyleConstants.Spacing.S, padding: StyleConstants.Spacing.S,
borderColor: colors.border, borderColor: colors.border,
borderWidth: 1, borderWidth: 1,
borderRadius: StyleConstants.Spacing.S borderRadius: StyleConstants.BorderRadius
}} }}
> >
<Icon <Icon

View File

@ -55,7 +55,7 @@ const ScreenTabs = () => {
sources={{ default: { uri: avatarStatic } }} sources={{ default: { uri: avatarStatic } }}
dimension={{ width: size, height: size }} dimension={{ width: size, height: size }}
style={{ style={{
borderRadius: size, borderRadius: 99,
overflow: 'hidden', overflow: 'hidden',
borderWidth: focused ? 2 : 0, borderWidth: focused ? 2 : 0,
borderColor: focused ? colors.primaryDefault : color borderColor: focused ? colors.primaryDefault : color

View File

@ -54,12 +54,16 @@ const useInstanceQuery = (
options?: UseQueryOptions<Mastodon.Instance<any>, AxiosError> 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, { return useQuery(queryKey, queryFunction, {
...params?.options, ...params?.options,
staleTime: Infinity, staleTime: Infinity,
cacheTime: Infinity, cacheTime: Infinity,
onSuccess: data => setAccountStorage([{ key: 'version', value: data.version }]) ...(!params?.domain && {
onSuccess: data => setAccountStorage([{ key: 'version', value: data.version }])
})
}) })
} }

View File

@ -65,6 +65,11 @@ type MutationVarsRelationship =
notify?: boolean notify?: boolean
} }
} }
| {
id: Mastodon.Account['id']
type: 'note'
payload: Mastodon.Relationship['note']
}
const mutationFunction = async (params: MutationVarsRelationship) => { const mutationFunction = async (params: MutationVarsRelationship) => {
switch (params.type) { switch (params.type) {
@ -83,11 +88,17 @@ const mutationFunction = async (params: MutationVarsRelationship) => {
url: `accounts/${params.id}/${params.payload.state ? 'un' : ''}${params.payload.action}`, url: `accounts/${params.id}/${params.payload.state ? 'un' : ''}${params.payload.action}`,
body body
}).then(res => res.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 = ( const useRelationshipMutation = (
options: UseMutationOptions<Mastodon.Relationship, AxiosError, MutationVarsRelationship> options?: UseMutationOptions<Mastodon.Relationship, AxiosError, MutationVarsRelationship>
) => { ) => {
return useMutation(mutationFunction, options) return useMutation(mutationFunction, options)
} }

View File

@ -6,8 +6,10 @@ import {
UseInfiniteQueryOptions, UseInfiniteQueryOptions,
useMutation useMutation
} from '@tanstack/react-query' } from '@tanstack/react-query'
import apiGeneral from '@utils/api/general'
import { PagedResponse } from '@utils/api/helpers' import { PagedResponse } from '@utils/api/helpers'
import apiInstance from '@utils/api/instance' import apiInstance from '@utils/api/instance'
import { appendRemote } from '@utils/helpers/appendRemote'
import { featureCheck } from '@utils/helpers/featureCheck' import { featureCheck } from '@utils/helpers/featureCheck'
import { useNavState } from '@utils/navigation/navigators' import { useNavState } from '@utils/navigation/navigators'
import { queryClient } from '@utils/queryHooks' import { queryClient } from '@utils/queryHooks'
@ -24,7 +26,7 @@ export type QueryKeyTimeline = [
'Timeline', 'Timeline',
( (
| { | {
page: Exclude<App.Pages, 'Following' | 'Hashtag' | 'List' | 'Toot' | 'Account'> page: Exclude<App.Pages, 'Following' | 'Hashtag' | 'List' | 'Toot' | 'Account' | 'Explore'>
} }
| { | {
page: 'Following' page: 'Following'
@ -50,6 +52,7 @@ export type QueryKeyTimeline = [
toot: Mastodon.Status['id'] toot: Mastodon.Status['id']
remote: boolean remote: boolean
} }
| { page: 'Explore'; domain?: string }
) )
] ]
@ -117,12 +120,24 @@ export const queryFunctionTimeline = async ({
params params
}) })
case 'Trending': case 'Explore':
return apiInstance<Mastodon.Status[]>({ if (page.domain) {
method: 'get', return apiGeneral<Mastodon.Status[]>({
url: 'trends/statuses', method: 'get',
params 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': case 'Notifications':
const notificationsFilter = getAccountStorage.object('notifications') const notificationsFilter = getAccountStorage.object('notifications')

View File

@ -5,7 +5,7 @@ export type GlobalV0 = {
// string // string
'app.expo_token'?: string 'app.expo_token'?: string
'app.prev_tab'?: keyof ScreenTabsStackParamList '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.language'?: string
'app.theme'?: 'light' | 'dark' | 'auto' 'app.theme'?: 'light' | 'dark' | 'auto'
'app.theme.dark'?: 'lighter' | 'darker' 'app.theme.dark'?: 'lighter' | 'darker'
@ -24,4 +24,10 @@ export type GlobalV0 = {
'account.active'?: string 'account.active'?: string
// object // object
accounts?: string[] accounts?: string[]
//// remote
// string
'remote.active'?: string
// object
remotes?: { title: string; domain: string }[]
} }

View File

@ -21,5 +21,7 @@ export const StyleConstants = {
Global: { PagePadding: Base * 4 } Global: { PagePadding: Base * 4 }
}, },
BorderRadius: Base * 2,
Avatar: { XS: 32, S: 40, M: 48, L: 96 } Avatar: { XS: 32, S: 40, M: 48, L: 96 }
} }

View File

@ -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 factor ? Math.round(size + size * (factor / 8)) : size
export { adaptiveScale } export const isLargeDevice = (Platform.OS === 'ios' && Platform.isPad) || Platform.OS === 'macos'

View File

@ -83,9 +83,9 @@ const themeColors: {
dark_darker: 'rgba(18, 18, 18, 0.5)' dark_darker: 'rgba(18, 18, 18, 0.5)'
}, },
backgroundOverlayInvert: { backgroundOverlayInvert: {
light: 'rgba(25, 25, 25, 0.5)', light: 'rgba(25, 25, 25, 0.75)',
dark_lighter: 'rgba(0, 0, 0, 0.5)', dark_lighter: 'rgba(0, 0, 0, 0.75)',
dark_darker: 'rgba(0, 0, 0, 0.5)' dark_darker: 'rgba(0, 0, 0, 0.75)'
}, },
border: { border: {