diff --git a/fastlane/metadata/en-US/release_notes.txt b/fastlane/metadata/en-US/release_notes.txt
index 46181811..5e983aa2 100644
--- a/fastlane/metadata/en-US/release_notes.txt
+++ b/fastlane/metadata/en-US/release_notes.txt
@@ -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
\ No newline at end of file
+- Added following remote instance
+- Added set note of followed users
\ No newline at end of file
diff --git a/fastlane/metadata/zh-Hans/release_notes.txt b/fastlane/metadata/zh-Hans/release_notes.txt
index df4f1cc5..8d2d6a1b 100644
--- a/fastlane/metadata/zh-Hans/release_notes.txt
+++ b/fastlane/metadata/zh-Hans/release_notes.txt
@@ -1,9 +1,3 @@
toooting愉快!此版本包括以下改进和修复:
-- 主动获取对话的远程内容
-- 自动加载上次我的关注的阅读位置
-- 用其它已登陆的账户关注用户
-- 可添加举报细节
-- 新增暂停自动播放gif动画选项
-- 隐藏用户的转嘟
-- 下划线高亮正在关注的话题标签
-- 支持GoToSocial
\ No newline at end of file
+- 新增关注远程实例功能
+- 新增关注用户备注功能
\ No newline at end of file
diff --git a/ios/tooot/Info.plist b/ios/tooot/Info.plist
index 1cc5b7b4..a70dccd6 100644
--- a/ios/tooot/Info.plist
+++ b/ios/tooot/Info.plist
@@ -68,7 +68,7 @@
NSPhotoLibraryUsageDescription
Allow $(PRODUCT_NAME) to access your camera roll to attach photos or videos to your toot
UILaunchStoryboardName
- SplashScreen.storyboard
+ SplashScreen
UIRequiredDeviceCapabilities
armv7
diff --git a/package.json b/package.json
index ed39a02e..96cfbca4 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "tooot",
- "version": "4.8.9",
+ "version": "4.9.0",
"description": "tooot for Mastodon",
"author": "xmflsct ",
"license": "GPL-3.0-or-later",
diff --git a/src/@types/app.d.ts b/src/@types/app.d.ts
index 3b2b9525..36b6ff82 100644
--- a/src/@types/app.d.ts
+++ b/src/@types/app.d.ts
@@ -3,7 +3,7 @@ declare namespace App {
| 'Following'
| 'Local'
| 'LocalPublic'
- | 'Trending'
+ | 'Explore'
| 'Notifications'
| 'Hashtag'
| 'List'
diff --git a/src/components/Account.tsx b/src/components/Account.tsx
index b0b9fdba..75ddfac4 100644
--- a/src/components/Account.tsx
+++ b/src/components/Account.tsx
@@ -42,7 +42,7 @@ const ComponentAccount: React.FC = ({ account, props,
style={{
width: StyleConstants.Avatar.S,
height: StyleConstants.Avatar.S,
- borderRadius: 8,
+ borderRadius: StyleConstants.BorderRadius,
marginRight: StyleConstants.Spacing.S
}}
dim
diff --git a/src/components/AccountButton.tsx b/src/components/AccountButton.tsx
index 9e481745..f6ea0a49 100644
--- a/src/components/AccountButton.tsx
+++ b/src/components/AccountButton.tsx
@@ -45,7 +45,7 @@ const AccountButton: React.FC = ({ 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' }}
/>
= ({
}}
style={[
{
- borderRadius: 100,
+ borderRadius: 99,
justifyContent: 'center',
alignItems: 'center',
borderWidth: overlay ? 0 : selected ? 1.5 : 1,
diff --git a/src/components/Emojis/Button.tsx b/src/components/Emojis/Button.tsx
index 98ca98d6..f9776ee5 100644
--- a/src/components/Emojis/Button.tsx
+++ b/src/components/Emojis/Button.tsx
@@ -35,7 +35,7 @@ const EmojisButton: React.FC = () => {
borderWidth: 2,
borderColor: colors.primaryDefault,
padding: StyleConstants.Spacing.Global.PagePadding / 2,
- borderRadius: 100
+ borderRadius: 99
}}
>
= ({
minWidth: 44,
marginLeft: native ? -StyleConstants.Spacing.S : StyleConstants.Spacing.S,
...(type === undefined && {
- borderRadius: 100
+ borderRadius: 99
}),
...(type === 'text' && {
paddingHorizontal: StyleConstants.Spacing.S
diff --git a/src/components/Header/Right.tsx b/src/components/Header/Right.tsx
index 91ab09f2..a918cbf9 100644
--- a/src/components/Header/Right.tsx
+++ b/src/components/Header/Right.tsx
@@ -98,9 +98,7 @@ const HeaderRight: React.FC = ({
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
})
diff --git a/src/components/Menu/Row.tsx b/src/components/Menu/Row.tsx
index ffad2741..c381ccb7 100644
--- a/src/components/Menu/Row.tsx
+++ b/src/components/Menu/Row.tsx
@@ -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 = ({
}}
>
-
+
= ({
width: 8,
height: 8,
backgroundColor: colors.red,
- borderRadius: 8,
+ borderRadius: StyleConstants.BorderRadius,
marginRight: StyleConstants.Spacing.S
}}
/>
diff --git a/src/components/Relationship/Outgoing.tsx b/src/components/Relationship/Outgoing.tsx
index e4fabbf2..685a0518 100644
--- a/src/components/Relationship/Outgoing.tsx
+++ b/src/components/Relationship/Outgoing.tsx
@@ -29,20 +29,20 @@ const RelationshipOutgoing: React.FC = ({ id }: Props) => {
const queryKeyRelationship: QueryKeyRelationship = ['Relationship', { id }]
const queryClient = useQueryClient()
const mutation = useRelationshipMutation({
- onSuccess: (res, { payload: { action } }) => {
+ onSuccess: (res, vars) => {
haptics('Success')
queryClient.setQueryData(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' &&
diff --git a/src/components/SwipeToActions.tsx b/src/components/SwipeToActions.tsx
index 74f6e01f..4306f8d2 100644
--- a/src/components/SwipeToActions.tsx
+++ b/src/components/SwipeToActions.tsx
@@ -31,9 +31,11 @@ export const SwipeToActions = ({
haptics(action.haptic || 'Light')
action.onPress({ item })
}}
+ style={{ backgroundColor: 'rgba(0, 255, 0, 0.2)' }}
>
= ({ conversation, queryKey, highlig
= ({ 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
}}
>
diff --git a/src/components/Timeline/Shared/Attachment/index.tsx b/src/components/Timeline/Shared/Attachment/index.tsx
index 087754a0..6bf95481 100644
--- a/src/components/Timeline/Shared/Attachment/index.tsx
+++ b/src/components/Timeline/Shared/Attachment/index.tsx
@@ -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) => (
{
gap: StyleConstants.Spacing.XS
}}
>
- {chunk.map((a, aIndex) => mapAttachmentType(a, chunkIndex * 2 + aIndex))}
+ {chunk.map((a, aIndex) => (
+ {mapAttachmentType(a, chunkIndex * 2 + aIndex)}
+ ))}
))}
diff --git a/src/components/Timeline/Shared/Avatar.tsx b/src/components/Timeline/Shared/Avatar.tsx
index ff80d0ab..55aa6ed0 100644
--- a/src/components/Timeline/Shared/Avatar.tsx
+++ b/src/components/Timeline/Shared/Avatar.tsx
@@ -49,7 +49,7 @@ const TimelineAvatar: React.FC = ({ account }) => {
}
}
style={{
- borderRadius: StyleConstants.Avatar.M,
+ borderRadius: 99,
overflow: 'hidden',
marginRight: StyleConstants.Spacing.S
}}
diff --git a/src/components/Timeline/Shared/Card.tsx b/src/components/Timeline/Shared/Card.tsx
index 2f7adac7..86e2ae4e 100644
--- a/src/components/Timeline/Shared/Card.tsx
+++ b/src/components/Timeline/Shared/Card.tsx
@@ -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
}}
diff --git a/src/components/Timeline/index.tsx b/src/components/Timeline/index.tsx
index 13ff0573..84346e3b 100644
--- a/src/components/Timeline/index.tsx
+++ b/src/components/Timeline/index.tsx
@@ -42,6 +42,7 @@ export interface Props {
'notifyOnChangeProps' | 'getNextPageParam' | 'getPreviousPageParam' | 'select' | 'onSuccess'
>
disableRefresh?: boolean
+ refreshAutoRefetch?: boolean
disableInfinity?: boolean
readMarker?: 'read_marker_following'
customProps?: Partial>
@@ -52,6 +53,7 @@ const Timeline: React.FC = ({
queryKey,
queryOptions,
disableRefresh = false,
+ refreshAutoRefetch = true,
disableInfinity = false,
readMarker = undefined,
customProps
@@ -154,6 +156,7 @@ const Timeline: React.FC = ({
if (
curr === true &&
prev === false &&
+ refreshAutoRefetch &&
!isFetchingPrev.value &&
fetchingType.value === 0 &&
shouldAutoFetch.value &&
diff --git a/src/components/contextMenu/account.ts b/src/components/contextMenu/account.ts
index 413d5e94..4da0a947 100644
--- a/src/components/contextMenu/account.ts
+++ b/src/components/contextMenu/account.ts
@@ -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(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' &&
diff --git a/src/components/discardConfirmation.ts b/src/components/discardConfirmation.ts
new file mode 100644
index 00000000..f3f4fbb4
--- /dev/null
+++ b/src/components/discardConfirmation.ts
@@ -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()
+ }
+}
diff --git a/src/components/haptics.ts b/src/components/haptics.ts
index 57148017..ef77d42a 100644
--- a/src/components/haptics.ts
+++ b/src/components/haptics.ts
@@ -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(() => {})
diff --git a/src/components/mediaSelector.ts b/src/components/mediaSelector.ts
index 4ef0ccfd..e05aedde 100644
--- a/src/components/mediaSelector.ts
+++ b/src/components/mediaSelector.ts
@@ -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>(queryKeyInstance)?.configuration?.statuses
.max_media_attachments || 4
@@ -27,7 +27,7 @@ const mediaSelector = async ({
indicateMaximum = false,
showActionSheetWithOptions
}: Props): Promise => {
- const _maximum = maximum || MAX_MEDIA_ATTACHMENTS
+ const _maximum = maximum || MAX_MEDIA_ATTACHMENTS()
const options = () => {
switch (mediaType) {
diff --git a/src/i18n/en/common.json b/src/i18n/en/common.json
index e8f0f972..368a8baa 100644
--- a/src/i18n/en/common.json
+++ b/src/i18n/en/common.json
@@ -8,7 +8,8 @@
"create": "Create",
"delete": "Delete",
"done": "Done",
- "confirm": "Confirm"
+ "confirm": "Confirm",
+ "add": "Add"
},
"customEmoji": {
"accessibilityLabel": "Custom emoji {{emoji}}"
diff --git a/src/i18n/en/components/instance.json b/src/i18n/en/components/instance.json
index 05a71792..c9d1eb14 100644
--- a/src/i18n/en/components/instance.json
+++ b/src/i18n/en/components/instance.json
@@ -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."
diff --git a/src/i18n/en/screens/tabs.json b/src/i18n/en/screens/tabs.json
index 3c27bd0b..615c4db5 100644
--- a/src/i18n/en/screens/tabs.json
+++ b/src/i18n/en/screens/tabs.json
@@ -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": {
diff --git a/src/screens/AccountSelection.tsx b/src/screens/AccountSelection.tsx
index 7681ca8f..1e3ce4e1 100644
--- a/src/screens/AccountSelection.tsx
+++ b/src/screens/AccountSelection.tsx
@@ -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
}}
>
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
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
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
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,
diff --git a/src/screens/Compose/DraftsList.tsx b/src/screens/Compose/DraftsList.tsx
index fba9e684..0d4ea949 100644
--- a/src/screens/Compose/DraftsList.tsx
+++ b/src/screens/Compose/DraftsList.tsx
@@ -56,7 +56,7 @@ const ComposeDraftsList: React.FC
diff --git a/src/screens/Compose/EditAttachment.tsx b/src/screens/Compose/EditAttachment.tsx
index c7a9b096..7c1a3e54 100644
--- a/src/screens/Compose/EditAttachment.tsx
+++ b/src/screens/Compose/EditAttachment.tsx
@@ -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(theAttachment.description)
+
useEffect(() => {
navigation.setOptions({
- title: t('content.editAttachment.header.title'),
- headerLeft: () => navigation.goBack()} />,
+ title: t('screenCompose:content.editAttachment.header.title'),
+ headerLeft: () => (
+ {
+ discardConfirmation({
+ condition: theAttachment.description != altText,
+ action: () => navigation.goBack()
+ })
+ }}
+ />
+ ),
headerRight: () => (
{
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({
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, [
- {
- text: t('content.editAttachment.header.right.failed.button'),
- style: 'cancel'
- }
- ])
+ Alert.alert(
+ t('screenCompose:content.editAttachment.header.right.failed.title'),
+ undefined,
+ [
+ {
+ text: t('screenCompose:content.editAttachment.header.right.failed.button'),
+ style: 'cancel'
+ }
+ ]
+ )
})
}}
/>
)
})
- }, [theAttachment])
+ }, [theAttachment, altText])
return (
+
+
+
- {t('content.editAttachment.content.altText.heading')}
+ {t('screenCompose:content.editAttachment.content.altText.heading')}
- 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}
/>
{
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 })
}
}
diff --git a/src/screens/Compose/Root/Footer/Attachments.tsx b/src/screens/Compose/Root/Footer/Attachments.tsx
index d1db2c3a..f84f6411 100644
--- a/src/screens/Compose/Root/Footer/Attachments.tsx
+++ b/src/screens/Compose/Root/Footer/Attachments.tsx
@@ -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
}
-const DEFAULT_HEIGHT = 200
-
const ComposeAttachments: React.FC = ({ accessibleRefAttachments }) => {
const { showActionSheetWithOptions } = useActionSheet()
const { composeState, composeDispatch } = useContext(ComposeContext)
@@ -40,72 +40,22 @@ const ComposeAttachments: React.FC = ({ 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()
- 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 (
= ({ 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 = ({ accessibleRefAttachments }) => {
})}
type='icon'
content='x'
- spacing='M'
+ size='L'
round
overlay
onPress={() => {
@@ -175,11 +125,11 @@ const ComposeAttachments: React.FC = ({ 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 = ({ 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() ? (
{
await chooseAndUploadAttachment({
@@ -258,9 +208,7 @@ const ComposeAttachments: React.FC = ({ accessibleRefAttachments }) => {
diff --git a/src/screens/Compose/Root/Footer/Poll.tsx b/src/screens/Compose/Root/Footer/Poll.tsx
index 0c03d9fe..02016345 100644
--- a/src/screens/Compose/Root/Footer/Poll.tsx
+++ b/src/screens/Compose/Root/Footer/Poll.tsx
@@ -38,8 +38,8 @@ 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,
diff --git a/src/screens/Compose/Root/Footer/Reply.tsx b/src/screens/Compose/Root/Footer/Reply.tsx
index c151dcc2..1862f27b 100644
--- a/src/screens/Compose/Root/Footer/Reply.tsx
+++ b/src/screens/Compose/Root/Footer/Reply.tsx
@@ -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,
diff --git a/src/screens/Compose/Root/Header/TextInput.tsx b/src/screens/Compose/Root/Header/TextInput.tsx
index 92b219e0..66a06157 100644
--- a/src/screens/Compose/Root/Header/TextInput.tsx
+++ b/src/screens/Compose/Root/Header/TextInput.tsx
@@ -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,
diff --git a/src/screens/Compose/utils/initialState.ts b/src/screens/Compose/utils/initialState.ts
index 190979a7..790dd21c 100644
--- a/src/screens/Compose/utils/initialState.ts
+++ b/src/screens/Compose/utils/initialState.ts
@@ -28,7 +28,39 @@ const composeInitialState: Omit = {
},
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,
diff --git a/src/screens/Tabs/Me/List/Edit.tsx b/src/screens/Tabs/Me/List/Edit.tsx
index 2dc750b5..da8999a6 100644
--- a/src/screens/Tabs/Me/List/Edit.tsx
+++ b/src/screens/Tabs/Me/List/Edit.tsx
@@ -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> = ({
navigation,
route: { params }
}) => {
- const { colors } = useTheme()
const { t } = useTranslation(['common', 'screenTabs'])
const messageRef = useRef(null)
@@ -92,21 +90,10 @@ const TabMeListEdit: React.FC> = ({
{
- 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)
+ })
}}
/>
),
diff --git a/src/screens/Tabs/Me/Profile/Fields.tsx b/src/screens/Tabs/Me/Profile/Fields.tsx
index 4ea66d80..f113d5ff 100644
--- a/src/screens/Tabs/Me/Profile/Fields.tsx
+++ b/src/screens/Tabs/Me/Profile/Fields.tsx
@@ -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<
{
- 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')
+ })
}}
/>
),
diff --git a/src/screens/Tabs/Me/Profile/Name.tsx b/src/screens/Tabs/Me/Profile/Name.tsx
index c978ca92..282619d6 100644
--- a/src/screens/Tabs/Me/Profile/Name.tsx
+++ b/src/screens/Tabs/Me/Profile/Name.tsx
@@ -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<
{
- 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')
+ })
}}
/>
),
diff --git a/src/screens/Tabs/Me/Profile/Note.tsx b/src/screens/Tabs/Me/Profile/Note.tsx
index 12071660..7f495ecb 100644
--- a/src/screens/Tabs/Me/Profile/Note.tsx
+++ b/src/screens/Tabs/Me/Profile/Note.tsx
@@ -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<
{
- 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')
+ })
}}
/>
),
diff --git a/src/screens/Tabs/Notifications/Filters.tsx b/src/screens/Tabs/Notifications/Filters.tsx
index fb80a7e0..3ca6bb76 100644
--- a/src/screens/Tabs/Notifications/Filters.tsx
+++ b/src/screens/Tabs/Notifications/Filters.tsx
@@ -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<
{
- 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() })
}}
/>
),
diff --git a/src/screens/Tabs/Public/Root.tsx b/src/screens/Tabs/Public/Root.tsx
index 95540a5f..2aed4ada 100644
--- a/src/screens/Tabs/Public/Root.tsx
+++ b/src/screens/Tabs/Public/Root.tsx
@@ -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('')
+ const [domainValid, setDomainValid] = useState()
+ const instanceQuery = useInstanceQuery({
+ domain,
+ options: {
+ enabled: false,
+ retry: false,
+ keepPreviousData: false,
+ cacheTime: 1000 * 30,
+ onSuccess: () =>
+ apiGeneral({
+ 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(null)
+ const queryKey: QueryKeyTimeline = [
+ 'Timeline',
+ { page, ...(remoteActive && { domain: remoteActive }) }
+ ]
+
+ const info = ({
+ heading,
+ content,
+ lines,
+ potentialWidth = 6
+ }: {
+ heading: string
+ content?: string
+ lines?: number
+ potentialWidth?: number
+ }) => (
+
+
+ {content ? (
+
+ ) : (
+ Array.from({ length: lines || 1 }).map((_, index) => (
+
+ ))
+ )}
+
+ )
+
+ useScrollToTop(flRef)
+
+ return (
+
+
+ {
+ setDomain('')
+ setAddingRemote(false)
+ layoutAnimation().then(() =>
+ flRef.current?.scrollToOffset({ animated: true, offset: 0 })
+ )
+ }}
+ />
+
+ {}} />
+
+
+
+ {
+ 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
+ />
+
+
+ {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
+ })}
+
+
+ ) : (
+
+
+
+
+ r.domain === remoteActive)?.title
+ }
+ />
+
+
+
+
+
+
+ {
+ if (next === 'on') {
+ setRemoteActive(undefined)
+ }
+ }}
+ >
+
+
+
+
+
+ {remotes?.map((item: NonNullable[0], index) => (
+ 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)
+ }
+ }}
+ >
+
+
+
+ {index === remotes?.findIndex(r => r.domain === remoteActive) ? (
+
+ ) : null}
+
+ ))}
+ {
+ setDomain('')
+ setAddingRemote(true)
+ layoutAnimation().then(() =>
+ flRef.current?.scrollToOffset({ animated: true, offset: 0 })
+ )
+ }}
+ >
+
+
+
+
+
+
+ )
+ }
+ />
+ )
+ }}
+ />
+ )
+}
+
+const Route = ({ route: { key: page } }: { route: { key: any } }) => {
const queryKey: QueryKeyTimeline = ['Timeline', { page }]
- return
+ return
}
const renderScene = SceneMap({
Local: Route,
LocalPublic: Route,
- Trending: Route
+ Explore
})
const Root: React.FC> = ({
@@ -29,7 +392,7 @@ const Root: React.FC(
Math.max(
0,
@@ -39,12 +402,19 @@ const Root: React.FC {
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({
diff --git a/src/screens/Tabs/Shared/Account/Attachments.tsx b/src/screens/Tabs/Shared/Account/Attachments.tsx
index deb5cc5a..1fe8546a 100644
--- a/src/screens/Tabs/Shared/Account/Attachments.tsx
+++ b/src/screens/Tabs/Shared/Account/Attachments.tsx
@@ -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>()
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',
diff --git a/src/screens/Tabs/Shared/Account/Information/Avatar.tsx b/src/screens/Tabs/Shared/Account/Information/Avatar.tsx
index ad6dbdf9..3413859d 100644
--- a/src/screens/Tabs/Shared/Account/Information/Avatar.tsx
+++ b/src/screens/Tabs/Shared/Account/Information/Avatar.tsx
@@ -14,7 +14,7 @@ const AccountInformationAvatar: React.FC = () => {
return (
{
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(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 ? (
+
+ submit()}
+ placeholder={t('screenTabs:shared.account.privateNote')}
+ placeholderTextColor={colors.secondary}
+ returnKeyType='done'
+ keyboardAppearance={mode}
+ autoFocus
+ />
+
+
+
+ ) : (
+ {
+ setEditing(!editing)
+ layoutAnimation()
+ }}
+ >
+ {!!relationship?.note.length ? (
+
+ ) : (
+
+ )}
+
+
+ )
) : null
}
diff --git a/src/screens/Tabs/Shared/Report.tsx b/src/screens/Tabs/Shared/Report.tsx
index 78c7f1ed..b34ec454 100644
--- a/src/screens/Tabs/Shared/Report.tsx
+++ b/src/screens/Tabs/Shared/Report.tsx
@@ -110,7 +110,7 @@ const TabSharedReport: React.FC>
margin: StyleConstants.Spacing.Global.PagePadding,
borderWidth: 1,
borderColor: colors.red,
- borderRadius: 8
+ borderRadius: StyleConstants.BorderRadius
}}
>
diff --git a/src/screens/Tabs/Shared/Toot.tsx b/src/screens/Tabs/Shared/Toot.tsx
index 93bc153e..c1446091 100644
--- a/src/screens/Tabs/Shared/Toot.tsx
+++ b/src/screens/Tabs/Shared/Toot.tsx
@@ -29,7 +29,7 @@ const TabSharedToot: React.FC> = ({
const { colors } = useTheme()
const { t } = useTranslation(['componentTimeline', 'screenTabs'])
- const [hasRemoteContent, setHasRemoteContent] = useState(false)
+ const [hasRemoteContent, setHasRemoteContent] = useState(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> = ({
headerLeft: () => 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> = ({
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> = ({
) : null}
> =
padding: StyleConstants.Spacing.S,
borderColor: colors.border,
borderWidth: 1,
- borderRadius: StyleConstants.Spacing.S
+ borderRadius: StyleConstants.BorderRadius
}}
>
{
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
diff --git a/src/utils/queryHooks/instance.ts b/src/utils/queryHooks/instance.ts
index 5562158a..253b02ac 100644
--- a/src/utils/queryHooks/instance.ts
+++ b/src/utils/queryHooks/instance.ts
@@ -54,12 +54,16 @@ const useInstanceQuery = (
options?: UseQueryOptions, 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,
- onSuccess: data => setAccountStorage([{ key: 'version', value: data.version }])
+ ...(!params?.domain && {
+ onSuccess: data => setAccountStorage([{ key: 'version', value: data.version }])
+ })
})
}
diff --git a/src/utils/queryHooks/relationship.ts b/src/utils/queryHooks/relationship.ts
index 4c811ce3..9d898743 100644
--- a/src/utils/queryHooks/relationship.ts
+++ b/src/utils/queryHooks/relationship.ts
@@ -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({
+ method: 'post',
+ url: `accounts/${params.id}/note`,
+ body: { comment: params.payload }
+ }).then(res => res.body)
}
}
const useRelationshipMutation = (
- options: UseMutationOptions
+ options?: UseMutationOptions
) => {
return useMutation(mutationFunction, options)
}
diff --git a/src/utils/queryHooks/timeline.ts b/src/utils/queryHooks/timeline.ts
index e36f3e91..fde8cb49 100644
--- a/src/utils/queryHooks/timeline.ts
+++ b/src/utils/queryHooks/timeline.ts
@@ -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
+ page: Exclude
}
| {
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':
- return apiInstance({
- method: 'get',
- url: 'trends/statuses',
- params
- })
+ case 'Explore':
+ if (page.domain) {
+ return apiGeneral({
+ 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({
+ method: 'get',
+ url: 'trends/statuses',
+ params
+ })
+ }
case 'Notifications':
const notificationsFilter = getAccountStorage.object('notifications')
diff --git a/src/utils/storage/global/v0.ts b/src/utils/storage/global/v0.ts
index 4acdfc39..3dd6f60e 100644
--- a/src/utils/storage/global/v0.ts
+++ b/src/utils/storage/global/v0.ts
@@ -5,7 +5,7 @@ export type GlobalV0 = {
// string
'app.expo_token'?: string
'app.prev_tab'?: keyof ScreenTabsStackParamList
- 'app.prev_public_segment'?: Extract
+ 'app.prev_public_segment'?: Extract
'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 }[]
}
diff --git a/src/utils/styles/constants.ts b/src/utils/styles/constants.ts
index a8ad52f5..02fc2d73 100644
--- a/src/utils/styles/constants.ts
+++ b/src/utils/styles/constants.ts
@@ -21,5 +21,7 @@ export const StyleConstants = {
Global: { PagePadding: Base * 4 }
},
+ BorderRadius: Base * 2,
+
Avatar: { XS: 32, S: 40, M: 48, L: 96 }
}
diff --git a/src/utils/styles/scaling.ts b/src/utils/styles/scaling.ts
index b17db475..6ace637e 100644
--- a/src/utils/styles/scaling.ts
+++ b/src/utils/styles/scaling.ts
@@ -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'
diff --git a/src/utils/styles/themes.ts b/src/utils/styles/themes.ts
index 4f6bc268..daced6f5 100644
--- a/src/utils/styles/themes.ts
+++ b/src/utils/styles/themes.ts
@@ -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: {