mirror of
https://github.com/tooot-app/app
synced 2025-02-18 04:40:57 +01:00
parent
700b9ad433
commit
03dc94c7c9
@ -60,6 +60,7 @@
|
||||
"react-native-screens": "~2.17.1",
|
||||
"react-native-shared-element": "^0.7.0",
|
||||
"react-native-svg": "12.1.0",
|
||||
"react-native-swipe-list-view": "^3.2.6",
|
||||
"react-native-tab-view": "^2.15.2",
|
||||
"react-native-tab-view-viewpager-adapter": "^1.1.0",
|
||||
"react-native-toast-message": "^1.4.3",
|
||||
|
8
src/@types/app.d.ts
vendored
8
src/@types/app.d.ts
vendored
@ -14,4 +14,12 @@ declare namespace App {
|
||||
| 'Conversations'
|
||||
| 'Bookmarks'
|
||||
| 'Favourites'
|
||||
|
||||
interface IImageInfo {
|
||||
url: string
|
||||
width?: number
|
||||
height?: number
|
||||
originUrl?: string
|
||||
props?: any
|
||||
}
|
||||
}
|
||||
|
11
src/@types/react-navigation.d.ts
vendored
11
src/@types/react-navigation.d.ts
vendored
@ -1,11 +1,3 @@
|
||||
interface IImageInfo {
|
||||
url: string
|
||||
width?: number
|
||||
height?: number
|
||||
originUrl?: string
|
||||
props?: any
|
||||
}
|
||||
|
||||
declare namespace Nav {
|
||||
type RootStackParamList = {
|
||||
'Screen-Tabs': undefined
|
||||
@ -56,7 +48,7 @@ declare namespace Nav {
|
||||
}
|
||||
| undefined
|
||||
'Screen-ImagesViewer': {
|
||||
imageUrls: (IImageInfo & {
|
||||
imageUrls: (App.IImageInfo & {
|
||||
preview_url: Mastodon.AttachmentImage['preview_url']
|
||||
remote_url?: Mastodon.AttachmentImage['remote_url']
|
||||
imageIndex: number
|
||||
@ -68,6 +60,7 @@ declare namespace Nav {
|
||||
type ScreenComposeStackParamList = {
|
||||
'Screen-Compose-Root': undefined
|
||||
'Screen-Compose-EditAttachment': { index: number }
|
||||
'Screen-Compose-DraftsList': { timestamp: number }
|
||||
}
|
||||
|
||||
type ScreenTabsStackParamList = {
|
||||
|
@ -12,7 +12,7 @@ import ScreenImagesViewer from '@screens/ImagesViewer'
|
||||
import ScreenTabs from '@screens/Tabs'
|
||||
import {
|
||||
getLocalActiveIndex,
|
||||
localUpdateAccountPreferences
|
||||
updateLocalAccountPreferences
|
||||
} from '@utils/slices/instancesSlice'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { themes } from '@utils/styles/themes'
|
||||
@ -103,7 +103,7 @@ const Index: React.FC<Props> = ({ localCorrupt }) => {
|
||||
// Lazily update users's preferences, for e.g. composing default visibility
|
||||
useEffect(() => {
|
||||
if (localActiveIndex !== null) {
|
||||
dispatch(localUpdateAccountPreferences())
|
||||
dispatch(updateLocalAccountPreferences())
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
@ -6,7 +6,7 @@ import TimeAgo from 'react-timeago'
|
||||
import buildFormatter from 'react-timeago/lib/formatters/buildFormatter'
|
||||
|
||||
export interface Props {
|
||||
date: string
|
||||
date: string | number
|
||||
}
|
||||
|
||||
const RelativeTime: React.FC<Props> = ({ date }) => {
|
||||
|
@ -1,8 +1,7 @@
|
||||
import ComponentSeparator from '@components/Separator'
|
||||
import { useNavigation, useScrollToTop } from '@react-navigation/native'
|
||||
import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
|
||||
import { getPublicRemoteNotice } from '@utils/slices/contextsSlice'
|
||||
import { localUpdateNotification } from '@utils/slices/instancesSlice'
|
||||
import { updateLocalNotification } from '@utils/slices/instancesSlice'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { findIndex } from 'lodash'
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
@ -13,7 +12,7 @@ import {
|
||||
StyleSheet
|
||||
} from 'react-native'
|
||||
import { FlatList } from 'react-native-gesture-handler'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import TimelineConversation from './Timeline/Conversation'
|
||||
import TimelineDefault from './Timeline/Default'
|
||||
import TimelineEmpty from './Timeline/Empty'
|
||||
@ -85,7 +84,7 @@ const Timeline: React.FC<Props> = ({
|
||||
if (props.target && props.target.includes('Tab-Notifications-Root')) {
|
||||
if (flattenData.length) {
|
||||
dispatch(
|
||||
localUpdateNotification({
|
||||
updateLocalNotification({
|
||||
latestTime: (flattenData[0] as Mastodon.Notification).created_at
|
||||
})
|
||||
)
|
||||
@ -197,8 +196,6 @@ const Timeline: React.FC<Props> = ({
|
||||
)
|
||||
}, [])
|
||||
|
||||
const publicRemoteNotice = useSelector(getPublicRemoteNotice).hidden
|
||||
|
||||
useScrollToTop(flRef)
|
||||
|
||||
return (
|
||||
|
@ -33,7 +33,7 @@ const TimelineAttachment: React.FC<Props> = ({ status }) => {
|
||||
haptics('Light')
|
||||
}, [])
|
||||
|
||||
let imageUrls: (IImageInfo & {
|
||||
let imageUrls: (App.IImageInfo & {
|
||||
preview_url: Mastodon.AttachmentImage['preview_url']
|
||||
remote_url?: Mastodon.AttachmentImage['remote_url']
|
||||
imageIndex: number
|
||||
|
@ -34,12 +34,7 @@ const TimelineHeaderDefault: React.FC<Props> = ({ queryKey, status }) => {
|
||||
|
||||
{queryKey ? (
|
||||
<Pressable
|
||||
style={{
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
paddingBottom: StyleConstants.Spacing.S
|
||||
}}
|
||||
style={styles.action}
|
||||
onPress={() =>
|
||||
navigation.navigate('Screen-Actions', {
|
||||
queryKey,
|
||||
@ -77,6 +72,12 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
created_at: {
|
||||
...StyleConstants.FontStyle.S
|
||||
},
|
||||
action: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
paddingBottom: StyleConstants.Spacing.S
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -5,18 +5,21 @@ import React from 'react'
|
||||
import { StyleSheet, Text } from 'react-native'
|
||||
|
||||
export interface Props {
|
||||
created_at: Mastodon.Status['created_at']
|
||||
created_at: Mastodon.Status['created_at'] | number
|
||||
}
|
||||
|
||||
const HeaderSharedCreated: React.FC<Props> = ({ created_at }) => {
|
||||
const { theme } = useTheme()
|
||||
const HeaderSharedCreated = React.memo(
|
||||
({ created_at }: Props) => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
return (
|
||||
<Text style={[styles.created_at, { color: theme.secondary }]}>
|
||||
<RelativeTime date={created_at} />
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Text style={[styles.created_at, { color: theme.secondary }]}>
|
||||
<RelativeTime date={created_at} />
|
||||
</Text>
|
||||
)
|
||||
},
|
||||
() => true
|
||||
)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
created_at: {
|
||||
@ -24,4 +27,4 @@ const styles = StyleSheet.create({
|
||||
}
|
||||
})
|
||||
|
||||
export default React.memo(HeaderSharedCreated, () => true)
|
||||
export default HeaderSharedCreated
|
||||
|
@ -5,8 +5,9 @@ export default {
|
||||
alert: {
|
||||
title: 'Cancel editing?',
|
||||
buttons: {
|
||||
exit: 'Confirm',
|
||||
continue: 'Continue'
|
||||
save: 'Save draft',
|
||||
delete: 'Delete draft',
|
||||
cancel: 'Cancel'
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -114,19 +115,16 @@ export default {
|
||||
cancel: '$t(common:buttons.cancel)'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
drafts: 'Draft ({{count}})',
|
||||
drafts_plural: 'Drafts ({{count}})'
|
||||
},
|
||||
editAttachment: {
|
||||
header: {
|
||||
left: 'Cancel',
|
||||
title: 'Edit attachment',
|
||||
right: {
|
||||
button: 'Apply',
|
||||
succeed: {
|
||||
title: 'Apply update succeed',
|
||||
button: 'Ok'
|
||||
},
|
||||
failed: {
|
||||
title: 'Apply update failed',
|
||||
title: 'Editing failed',
|
||||
button: 'Try again'
|
||||
}
|
||||
}
|
||||
@ -139,6 +137,14 @@ export default {
|
||||
},
|
||||
imageFocus: 'Drag the focus circle to update focus point'
|
||||
}
|
||||
},
|
||||
draftsList: {
|
||||
header: {
|
||||
title: 'Draft'
|
||||
},
|
||||
content: {
|
||||
textEmpty: 'Content empty'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,8 +5,9 @@ export default {
|
||||
alert: {
|
||||
title: '确认退出编辑?',
|
||||
buttons: {
|
||||
exit: '退出编辑',
|
||||
continue: '继续编辑'
|
||||
save: '保存草稿',
|
||||
delete: '删除草稿',
|
||||
cancel: '继续编辑'
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -114,19 +115,15 @@ export default {
|
||||
cancel: '$t(common:buttons.cancel)'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
drafts: '草稿 ({{count}})'
|
||||
},
|
||||
editAttachment: {
|
||||
header: {
|
||||
left: '取消修改',
|
||||
title: '编辑附件',
|
||||
right: {
|
||||
button: '应用修改',
|
||||
succeed: {
|
||||
title: '修改成功',
|
||||
button: '好的'
|
||||
},
|
||||
failed: {
|
||||
title: '修改失败',
|
||||
title: '编辑失败',
|
||||
button: '返回重试'
|
||||
}
|
||||
}
|
||||
@ -139,6 +136,14 @@ export default {
|
||||
},
|
||||
imageFocus: '在预览图上拖动圆圈,以选择缩略图的焦点'
|
||||
}
|
||||
},
|
||||
draftsList: {
|
||||
header: {
|
||||
title: '草稿'
|
||||
},
|
||||
content: {
|
||||
textEmpty: '无正文内容'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,32 +2,40 @@ import analytics from '@components/analytics'
|
||||
import { HeaderLeft, HeaderRight } from '@components/Header'
|
||||
import { StackScreenProps } from '@react-navigation/stack'
|
||||
import haptics from '@root/components/haptics'
|
||||
import { store } from '@root/store'
|
||||
import formatText from '@screens/Compose/formatText'
|
||||
import ComposeRoot from '@screens/Compose/Root'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import { updateStoreReview } from '@utils/slices/contextsSlice'
|
||||
import {
|
||||
getLocalAccount,
|
||||
getLocalMaxTootChar
|
||||
getLocalMaxTootChar,
|
||||
removeLocalDraft,
|
||||
updateLocalDraft
|
||||
} from '@utils/slices/instancesSlice'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useCallback, useEffect, useReducer, useState } from 'react'
|
||||
import { filter } from 'lodash'
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useReducer,
|
||||
useState
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
Alert,
|
||||
Keyboard,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
Text
|
||||
StyleSheet
|
||||
} from 'react-native'
|
||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||
import { useQueryClient } from 'react-query'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import * as Sentry from 'sentry-expo'
|
||||
import ComposeDraftsList from './Compose/DraftsList'
|
||||
import ComposeEditAttachment from './Compose/EditAttachment'
|
||||
import ComposeContext from './Compose/utils/createContext'
|
||||
import composeInitialState from './Compose/utils/initialState'
|
||||
@ -55,7 +63,6 @@ const ScreenCompose: React.FC<ScreenComposeProp> = ({
|
||||
Keyboard.addListener('keyboardWillShow', _keyboardDidShow)
|
||||
Keyboard.addListener('keyboardWillHide', _keyboardDidHide)
|
||||
|
||||
// cleanup function
|
||||
return () => {
|
||||
Keyboard.removeListener('keyboardWillShow', _keyboardDidShow)
|
||||
Keyboard.removeListener('keyboardWillHide', _keyboardDidHide)
|
||||
@ -68,21 +75,53 @@ const ScreenCompose: React.FC<ScreenComposeProp> = ({
|
||||
setHasKeyboard(false)
|
||||
}
|
||||
|
||||
const localAccount = getLocalAccount(store.getState())
|
||||
// const draft = useSelector(getLocalDraft, () => true)
|
||||
const initialReducerState = useMemo(() => {
|
||||
if (params) {
|
||||
return composeParseState(params)
|
||||
} else {
|
||||
return {
|
||||
...composeInitialState,
|
||||
timestamp: Date.now(),
|
||||
visibility:
|
||||
localAccount?.preferences &&
|
||||
localAccount.preferences['posting:default:visibility']
|
||||
? localAccount.preferences['posting:default:visibility']
|
||||
: 'public'
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const localAccount = useSelector(getLocalAccount)
|
||||
const [composeState, composeDispatch] = useReducer(
|
||||
composeReducer,
|
||||
params
|
||||
? composeParseState(params)
|
||||
: {
|
||||
...composeInitialState,
|
||||
visibility:
|
||||
localAccount?.preferences &&
|
||||
localAccount.preferences['posting:default:visibility']
|
||||
? localAccount.preferences['posting:default:visibility']
|
||||
: 'public'
|
||||
}
|
||||
initialReducerState
|
||||
)
|
||||
|
||||
const maxTootChars = useSelector(getLocalMaxTootChar)
|
||||
const totalTextCount =
|
||||
(composeState.spoiler.active ? composeState.spoiler.count : 0) +
|
||||
composeState.text.count
|
||||
|
||||
// If compose state is dirty, then disallow add back drafts
|
||||
useEffect(() => {
|
||||
composeDispatch({
|
||||
type: 'dirty',
|
||||
payload:
|
||||
totalTextCount !== 0 ||
|
||||
composeState.attachments.uploads.length !== 0 ||
|
||||
(composeState.poll.active === true &&
|
||||
filter(composeState.poll.options, o => {
|
||||
return o !== undefined && o.length > 0
|
||||
}).length > 0)
|
||||
})
|
||||
}, [
|
||||
totalTextCount,
|
||||
composeState.attachments.uploads.length,
|
||||
composeState.poll.active,
|
||||
composeState.poll.options
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
switch (params?.type) {
|
||||
case 'edit':
|
||||
@ -113,10 +152,31 @@ const ScreenCompose: React.FC<ScreenComposeProp> = ({
|
||||
}
|
||||
}, [params?.type])
|
||||
|
||||
const maxTootChars = useSelector(getLocalMaxTootChar)
|
||||
const totalTextCount =
|
||||
(composeState.spoiler.active ? composeState.spoiler.count : 0) +
|
||||
composeState.text.count
|
||||
const saveDraft = () => {
|
||||
dispatch(
|
||||
updateLocalDraft({
|
||||
timestamp: composeState.timestamp,
|
||||
spoiler: composeState.spoiler.raw,
|
||||
text: composeState.text.raw,
|
||||
poll: composeState.poll,
|
||||
attachments: composeState.attachments,
|
||||
visibility: composeState.visibility,
|
||||
visibilityLock: composeState.visibilityLock,
|
||||
replyToStatus: composeState.replyToStatus
|
||||
})
|
||||
)
|
||||
}
|
||||
const removeDraft = useCallback(() => {
|
||||
dispatch(removeLocalDraft(composeState.timestamp))
|
||||
}, [composeState.timestamp])
|
||||
useEffect(() => {
|
||||
const autoSave = composeState.dirty
|
||||
? setInterval(() => {
|
||||
saveDraft()
|
||||
}, 2000)
|
||||
: removeDraft()
|
||||
return () => autoSave && clearInterval(autoSave)
|
||||
}, [composeState])
|
||||
|
||||
const headerLeft = useCallback(
|
||||
() => (
|
||||
@ -125,11 +185,7 @@ const ScreenCompose: React.FC<ScreenComposeProp> = ({
|
||||
content={t('heading.left.button')}
|
||||
onPress={() => {
|
||||
analytics('compose_header_back_press')
|
||||
if (
|
||||
totalTextCount === 0 &&
|
||||
composeState.attachments.uploads.length === 0 &&
|
||||
composeState.poll.active === false
|
||||
) {
|
||||
if (!composeState.dirty) {
|
||||
analytics('compose_header_back_empty')
|
||||
navigation.goBack()
|
||||
return
|
||||
@ -137,15 +193,24 @@ const ScreenCompose: React.FC<ScreenComposeProp> = ({
|
||||
analytics('compose_header_back_state_occupied')
|
||||
Alert.alert(t('heading.left.alert.title'), undefined, [
|
||||
{
|
||||
text: t('heading.left.alert.buttons.exit'),
|
||||
text: t('heading.left.alert.buttons.delete'),
|
||||
style: 'destructive',
|
||||
onPress: () => {
|
||||
analytics('compose_header_back_occupied_confirm')
|
||||
analytics('compose_header_back_occupied_save')
|
||||
removeDraft()
|
||||
navigation.goBack()
|
||||
}
|
||||
},
|
||||
{
|
||||
text: t('heading.left.alert.buttons.continue'),
|
||||
text: t('heading.left.alert.buttons.save'),
|
||||
onPress: () => {
|
||||
analytics('compose_header_back_occupied_delete')
|
||||
saveDraft()
|
||||
navigation.goBack()
|
||||
}
|
||||
},
|
||||
{
|
||||
text: t('heading.left.alert.buttons.cancel'),
|
||||
style: 'cancel',
|
||||
onPress: () => {
|
||||
analytics('compose_header_back_occupied_cancel')
|
||||
@ -156,22 +221,7 @@ const ScreenCompose: React.FC<ScreenComposeProp> = ({
|
||||
}}
|
||||
/>
|
||||
),
|
||||
[totalTextCount, composeState]
|
||||
)
|
||||
const headerCenter = useCallback(
|
||||
() => (
|
||||
<Text
|
||||
style={[
|
||||
styles.count,
|
||||
{
|
||||
color: totalTextCount > maxTootChars ? theme.red : theme.secondary
|
||||
}
|
||||
]}
|
||||
>
|
||||
{totalTextCount} / {maxTootChars}
|
||||
</Text>
|
||||
),
|
||||
[totalTextCount, maxTootChars]
|
||||
[composeState]
|
||||
)
|
||||
const dispatch = useDispatch()
|
||||
const headerRight = useCallback(
|
||||
@ -228,7 +278,7 @@ const ScreenCompose: React.FC<ScreenComposeProp> = ({
|
||||
}
|
||||
/>
|
||||
),
|
||||
[totalTextCount, maxTootChars, composeState]
|
||||
[totalTextCount, composeState]
|
||||
)
|
||||
|
||||
return (
|
||||
@ -249,7 +299,26 @@ const ScreenCompose: React.FC<ScreenComposeProp> = ({
|
||||
<Stack.Screen
|
||||
name='Screen-Compose-Root'
|
||||
component={ComposeRoot}
|
||||
options={{ headerLeft, headerCenter, headerRight }}
|
||||
options={{
|
||||
headerLeft,
|
||||
headerTitle: `${totalTextCount} / ${maxTootChars}${__DEV__ &&
|
||||
` Dirty: ${composeState.dirty.toString()}`}`,
|
||||
headerTitleStyle: {
|
||||
fontWeight:
|
||||
totalTextCount > maxTootChars
|
||||
? StyleConstants.Font.Weight.Bold
|
||||
: StyleConstants.Font.Weight.Normal,
|
||||
fontSize: StyleConstants.Font.Size.M
|
||||
},
|
||||
headerTintColor:
|
||||
totalTextCount > maxTootChars ? theme.red : theme.secondary,
|
||||
headerRight
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='Screen-Compose-DraftsList'
|
||||
component={ComposeDraftsList}
|
||||
options={{ stackPresentation: 'modal' }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='Screen-Compose-EditAttachment'
|
||||
@ -264,11 +333,7 @@ const ScreenCompose: React.FC<ScreenComposeProp> = ({
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: { flex: 1 },
|
||||
count: {
|
||||
textAlign: 'center',
|
||||
...StyleConstants.FontStyle.M
|
||||
}
|
||||
base: { flex: 1 }
|
||||
})
|
||||
|
||||
export default ScreenCompose
|
||||
|
59
src/screens/Compose/DraftsList.tsx
Normal file
59
src/screens/Compose/DraftsList.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import { HeaderCenter, HeaderLeft } from '@components/Header'
|
||||
import { StackScreenProps } from '@react-navigation/stack'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Platform } from 'react-native'
|
||||
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||
import ComposeDraftsListRoot from './DraftsList/Root'
|
||||
|
||||
const Stack = createNativeStackNavigator()
|
||||
|
||||
export type ScreenComposeEditAttachmentProp = StackScreenProps<
|
||||
Nav.ScreenComposeStackParamList,
|
||||
'Screen-Compose-DraftsList'
|
||||
>
|
||||
|
||||
const ComposeDraftsList: React.FC<ScreenComposeEditAttachmentProp> = ({
|
||||
route: {
|
||||
params: { timestamp }
|
||||
},
|
||||
navigation
|
||||
}) => {
|
||||
const { t } = useTranslation('sharedCompose')
|
||||
|
||||
const children = useCallback(
|
||||
() => <ComposeDraftsListRoot timestamp={timestamp} />,
|
||||
[]
|
||||
)
|
||||
const headerLeft = useCallback(
|
||||
() => (
|
||||
<HeaderLeft
|
||||
type='icon'
|
||||
content='ChevronDown'
|
||||
onPress={() => navigation.goBack()}
|
||||
/>
|
||||
),
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<Stack.Navigator screenOptions={{ headerTopInsetEnabled: false }}>
|
||||
<Stack.Screen
|
||||
name='Screen-Compose-EditAttachment-Root'
|
||||
children={children}
|
||||
options={{
|
||||
headerLeft,
|
||||
headerTitle: t('content.draftsList.header.title'),
|
||||
...(Platform.OS === 'android' && {
|
||||
headerCenter: () => (
|
||||
<HeaderCenter content={t('content.draftsList.header.title')} />
|
||||
)
|
||||
}),
|
||||
headerHideShadow: true
|
||||
}}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
)
|
||||
}
|
||||
|
||||
export default ComposeDraftsList
|
238
src/screens/Compose/DraftsList/Root.tsx
Normal file
238
src/screens/Compose/DraftsList/Root.tsx
Normal file
@ -0,0 +1,238 @@
|
||||
import client from '@api/client'
|
||||
import Icon from '@components/Icon'
|
||||
import ComponentSeparator from '@components/Separator'
|
||||
import HeaderSharedCreated from '@components/Timelines/Timeline/Shared/HeaderShared/Created'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { getLocalDrafts, removeLocalDraft } from '@utils/slices/instancesSlice'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useCallback, useContext, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
Dimensions,
|
||||
Modal,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View
|
||||
} from 'react-native'
|
||||
import FastImage from 'react-native-fast-image'
|
||||
import { SwipeListView } from 'react-native-swipe-list-view'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import formatText from '../formatText'
|
||||
import ComposeContext from '../utils/createContext'
|
||||
import { ComposeStateDraft, ExtendedAttachment } from '../utils/types'
|
||||
|
||||
export interface Props {
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
const ComposeDraftsListRoot: React.FC<Props> = ({ timestamp }) => {
|
||||
const { composeDispatch } = useContext(ComposeContext)
|
||||
const { t } = useTranslation('sharedCompose')
|
||||
const navigation = useNavigation()
|
||||
const dispatch = useDispatch()
|
||||
const { mode, theme } = useTheme()
|
||||
const localDrafts = useSelector(getLocalDrafts)?.filter(
|
||||
draft => draft.timestamp !== timestamp
|
||||
)
|
||||
|
||||
const actionWidth =
|
||||
StyleConstants.Font.Size.L + StyleConstants.Spacing.Global.PagePadding * 4
|
||||
|
||||
const [checkingAttachments, setCheckingAttachments] = useState(false)
|
||||
|
||||
const removeDraft = useCallback(ts => {
|
||||
dispatch(removeLocalDraft(ts))
|
||||
}, [])
|
||||
|
||||
const renderItem = useCallback(
|
||||
({ item }: { item: ComposeStateDraft }) => {
|
||||
return (
|
||||
<Pressable
|
||||
style={[styles.draft, { backgroundColor: theme.background }]}
|
||||
onPress={async () => {
|
||||
setCheckingAttachments(true)
|
||||
let tempDraft = item
|
||||
let tempUploads: ExtendedAttachment[] = []
|
||||
if (item.attachments && item.attachments.uploads.length) {
|
||||
for (const attachment of item.attachments.uploads) {
|
||||
await client<Mastodon.Attachment>({
|
||||
method: 'get',
|
||||
instance: 'local',
|
||||
url: `media/${attachment.remote?.id}`
|
||||
})
|
||||
.then(res => {
|
||||
if (res.id === attachment.remote?.id) {
|
||||
tempUploads.push(attachment)
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
tempDraft = {
|
||||
...tempDraft,
|
||||
attachments: { ...item.attachments, uploads: tempUploads }
|
||||
}
|
||||
}
|
||||
|
||||
tempDraft.spoiler?.length &&
|
||||
formatText({
|
||||
textInput: 'text',
|
||||
composeDispatch,
|
||||
content: tempDraft.spoiler
|
||||
})
|
||||
tempDraft.text?.length &&
|
||||
formatText({
|
||||
textInput: 'text',
|
||||
composeDispatch,
|
||||
content: tempDraft.text
|
||||
})
|
||||
composeDispatch({
|
||||
type: 'loadDraft',
|
||||
payload: tempDraft
|
||||
})
|
||||
dispatch(removeLocalDraft(item.timestamp))
|
||||
navigation.goBack()
|
||||
}}
|
||||
>
|
||||
<View style={{ flex: 1 }}>
|
||||
<HeaderSharedCreated created_at={item.timestamp} />
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={[styles.text, { color: theme.primary }]}
|
||||
>
|
||||
{item.text ||
|
||||
item.spoiler ||
|
||||
t('content.draftsList.content.textEmpty')}
|
||||
</Text>
|
||||
{item.attachments?.uploads.length ? (
|
||||
<View style={styles.attachments}>
|
||||
{item.attachments.uploads.map((attachment, index) => (
|
||||
<FastImage
|
||||
key={index}
|
||||
style={[
|
||||
styles.attachment,
|
||||
{ marginLeft: index !== 0 ? StyleConstants.Spacing.S : 0 }
|
||||
]}
|
||||
source={{
|
||||
uri:
|
||||
attachment.local?.local_thumbnail ||
|
||||
attachment.remote?.preview_url
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
</Pressable>
|
||||
)
|
||||
},
|
||||
[mode]
|
||||
)
|
||||
const renderHiddenItem = useCallback(
|
||||
({ item }) => (
|
||||
<View
|
||||
style={[styles.hiddenBase, { backgroundColor: theme.red }]}
|
||||
children={
|
||||
<Pressable
|
||||
style={styles.action}
|
||||
onPress={() => removeDraft(item.timestamp)}
|
||||
children={
|
||||
<Icon
|
||||
name='Trash'
|
||||
size={StyleConstants.Font.Size.L}
|
||||
color={theme.primaryOverlay}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
),
|
||||
[mode]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<SwipeListView
|
||||
data={localDrafts}
|
||||
renderItem={renderItem}
|
||||
renderHiddenItem={renderHiddenItem}
|
||||
disableRightSwipe={true}
|
||||
rightOpenValue={-actionWidth}
|
||||
// previewRowKey={
|
||||
// localDrafts?.length ? localDrafts[0].timestamp.toString() : undefined
|
||||
// }
|
||||
// previewDuration={350}
|
||||
// previewOpenValue={-actionWidth / 2}
|
||||
ItemSeparatorComponent={ComponentSeparator}
|
||||
keyExtractor={item => item.timestamp.toString()}
|
||||
/>
|
||||
<Modal
|
||||
transparent
|
||||
animationType='fade'
|
||||
visible={checkingAttachments}
|
||||
children={
|
||||
<View
|
||||
style={[styles.modal, { backgroundColor: theme.backgroundOverlay }]}
|
||||
children={
|
||||
<Text
|
||||
children='检查附件在服务器的状态…'
|
||||
style={{
|
||||
...StyleConstants.FontStyle.M,
|
||||
color: theme.primaryOverlay
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
draft: {
|
||||
flex: 1,
|
||||
padding: StyleConstants.Spacing.Global.PagePadding
|
||||
},
|
||||
text: {
|
||||
marginTop: StyleConstants.Spacing.XS,
|
||||
...StyleConstants.FontStyle.M
|
||||
},
|
||||
attachments: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
marginTop: StyleConstants.Spacing.S
|
||||
},
|
||||
attachment: {
|
||||
width:
|
||||
(Dimensions.get('screen').width -
|
||||
StyleConstants.Spacing.Global.PagePadding * 2 -
|
||||
StyleConstants.Spacing.S * 3) /
|
||||
4,
|
||||
height:
|
||||
(Dimensions.get('screen').width -
|
||||
StyleConstants.Spacing.Global.PagePadding * 2 -
|
||||
StyleConstants.Spacing.S * 3) /
|
||||
4
|
||||
},
|
||||
hiddenBase: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end'
|
||||
},
|
||||
action: {
|
||||
flexBasis:
|
||||
StyleConstants.Font.Size.L +
|
||||
StyleConstants.Spacing.Global.PagePadding * 4,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
},
|
||||
modal: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
}
|
||||
})
|
||||
|
||||
export default ComposeDraftsListRoot
|
@ -1,7 +1,7 @@
|
||||
import client from '@api/client'
|
||||
import analytics from '@components/analytics'
|
||||
import haptics from '@components/haptics'
|
||||
import { HeaderLeft, HeaderRight } from '@components/Header'
|
||||
import { HeaderCenter, HeaderLeft, HeaderRight } from '@components/Header'
|
||||
import { StackScreenProps } from '@react-navigation/stack'
|
||||
import React, {
|
||||
useCallback,
|
||||
@ -71,8 +71,8 @@ const ComposeEditAttachment: React.FC<ScreenComposeEditAttachmentProp> = ({
|
||||
const headerLeft = useCallback(
|
||||
() => (
|
||||
<HeaderLeft
|
||||
type='text'
|
||||
content={t('content.editAttachment.header.left')}
|
||||
type='icon'
|
||||
content='ChevronDown'
|
||||
onPress={() => navigation.goBack()}
|
||||
/>
|
||||
),
|
||||
@ -81,8 +81,8 @@ const ComposeEditAttachment: React.FC<ScreenComposeEditAttachmentProp> = ({
|
||||
const headerRight = useCallback(
|
||||
() => (
|
||||
<HeaderRight
|
||||
type='text'
|
||||
content={t('content.editAttachment.header.right.button')}
|
||||
type='icon'
|
||||
content='Save'
|
||||
loading={isSubmitting}
|
||||
onPress={() => {
|
||||
analytics('editattachment_confirm_press')
|
||||
@ -107,20 +107,7 @@ const ComposeEditAttachment: React.FC<ScreenComposeEditAttachmentProp> = ({
|
||||
})
|
||||
.then(() => {
|
||||
haptics('Success')
|
||||
Alert.alert(
|
||||
t('content.editAttachment.header.right.succeed.title'),
|
||||
undefined,
|
||||
[
|
||||
{
|
||||
text: t(
|
||||
'content.editAttachment.header.right.succeed.button'
|
||||
),
|
||||
onPress: () => {
|
||||
navigation.goBack()
|
||||
}
|
||||
}
|
||||
]
|
||||
)
|
||||
navigation.goBack()
|
||||
})
|
||||
.catch(() => {
|
||||
setIsSubmitting(false)
|
||||
@ -167,7 +154,18 @@ const ComposeEditAttachment: React.FC<ScreenComposeEditAttachmentProp> = ({
|
||||
<Stack.Screen
|
||||
name='Screen-Compose-EditAttachment-Root'
|
||||
children={children}
|
||||
options={{ headerLeft, headerRight, headerCenter: () => null }}
|
||||
options={{
|
||||
headerLeft,
|
||||
headerRight,
|
||||
headerTitle: t('content.editAttachment.header.title'),
|
||||
...(Platform.OS === 'android' && {
|
||||
headerCenter: () => (
|
||||
<HeaderCenter
|
||||
content={t('content.editAttachment.header.title')}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
</SafeAreaView>
|
||||
|
@ -13,6 +13,7 @@ import ComposeRootFooter from './Root/Footer'
|
||||
import ComposeRootHeader from './Root/Header'
|
||||
import ComposeRootSuggestion from './Root/Suggestion'
|
||||
import ComposeContext from './utils/createContext'
|
||||
import ComposeDrafts from './Root/Drafts'
|
||||
|
||||
const ComposeRoot: React.FC = () => {
|
||||
const { theme } = useTheme()
|
||||
@ -68,9 +69,8 @@ const ComposeRoot: React.FC = () => {
|
||||
}, [isFetching])
|
||||
|
||||
const listItem = useCallback(
|
||||
({ item, index }) => (
|
||||
({ item }) => (
|
||||
<ComposeRootSuggestion
|
||||
key={index}
|
||||
item={item}
|
||||
composeState={composeState}
|
||||
composeDispatch={composeDispatch}
|
||||
@ -90,8 +90,10 @@ const ComposeRoot: React.FC = () => {
|
||||
ItemSeparatorComponent={ComponentSeparator}
|
||||
// @ts-ignore
|
||||
data={data ? data[composeState.tag?.type] : undefined}
|
||||
keyExtractor={() => Math.random().toString()}
|
||||
/>
|
||||
<ComposeActions />
|
||||
<ComposeDrafts />
|
||||
<ComposePosting />
|
||||
</View>
|
||||
)
|
||||
|
56
src/screens/Compose/Root/Drafts.tsx
Normal file
56
src/screens/Compose/Root/Drafts.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import Button from '@components/Button'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { getLocalDrafts } from '@utils/slices/instancesSlice'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import layoutAnimation from '@utils/styles/layoutAnimation'
|
||||
import React, { useContext, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { StyleSheet, View } from 'react-native'
|
||||
import { useSelector } from 'react-redux'
|
||||
import ComposeContext from '../utils/createContext'
|
||||
|
||||
const ComposeDrafts: React.FC = () => {
|
||||
const { t } = useTranslation('sharedCompose')
|
||||
const navigation = useNavigation()
|
||||
const { composeState } = useContext(ComposeContext)
|
||||
const localDrafts = useSelector(getLocalDrafts)?.filter(
|
||||
draft => draft.timestamp !== composeState.timestamp
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
layoutAnimation()
|
||||
}, [composeState.dirty])
|
||||
|
||||
if (!composeState.dirty && localDrafts?.length) {
|
||||
return (
|
||||
<View
|
||||
style={styles.base}
|
||||
children={
|
||||
<Button
|
||||
type='text'
|
||||
content={t('content.root.drafts', {
|
||||
count: localDrafts.length
|
||||
})}
|
||||
onPress={() =>
|
||||
navigation.navigate('Screen-Compose-DraftsList', {
|
||||
timestamp: composeState.timestamp
|
||||
})
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
position: 'absolute',
|
||||
bottom: 45 + StyleConstants.Spacing.Global.PagePadding,
|
||||
right: StyleConstants.Spacing.Global.PagePadding
|
||||
}
|
||||
})
|
||||
|
||||
export default ComposeDrafts
|
@ -1,9 +1,9 @@
|
||||
import React, { useContext } from 'react'
|
||||
import ComposeAttachments from '@screens/Compose/Root/Footer/Attachments'
|
||||
import ComposeEmojis from '@screens/Compose/Root/Footer/Emojis'
|
||||
import ComposePoll from '@screens/Compose/Root/Footer/Poll'
|
||||
import ComposeReply from '@screens/Compose/Root/Footer/Reply'
|
||||
import ComposeContext from '@screens/Compose/utils/createContext'
|
||||
import React, { useContext } from 'react'
|
||||
|
||||
const ComposeRootFooter: React.FC = () => {
|
||||
const { composeState } = useContext(ComposeContext)
|
||||
|
@ -250,7 +250,9 @@ const ComposeAttachments: React.FC = () => {
|
||||
keyboardShouldPersistTaps='handled'
|
||||
showsHorizontalScrollIndicator={false}
|
||||
data={composeState.attachments.uploads}
|
||||
keyExtractor={item => item.local?.uri || item.remote?.url}
|
||||
keyExtractor={item =>
|
||||
item.local?.url || item.remote?.url || Math.random().toString()
|
||||
}
|
||||
ListFooterComponent={
|
||||
composeState.attachments.uploads.length < 4 ? listFooter : null
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { createRef } from 'react'
|
||||
import { ComposeState } from './types'
|
||||
|
||||
const composeInitialState: ComposeState = {
|
||||
const composeInitialState: Omit<ComposeState, 'timestamp'> = {
|
||||
dirty: false,
|
||||
posting: false,
|
||||
spoiler: {
|
||||
active: false,
|
||||
|
@ -4,12 +4,14 @@ import composeInitialState from './initialState'
|
||||
import { ComposeState } from './types'
|
||||
|
||||
const composeParseState = (
|
||||
params: NonNullable<Nav.SharedStackParamList['Screen-Compose']>
|
||||
params: NonNullable<Nav.RootStackParamList['Screen-Compose']>
|
||||
): ComposeState => {
|
||||
switch (params.type) {
|
||||
case 'edit':
|
||||
return {
|
||||
...composeInitialState,
|
||||
dirty: true,
|
||||
timestamp: Date.now(),
|
||||
...(params.incomingStatus.spoiler_text && {
|
||||
spoiler: { ...composeInitialState.spoiler, active: true }
|
||||
}),
|
||||
@ -49,6 +51,8 @@ const composeParseState = (
|
||||
const actualStatus = params.incomingStatus.reblog || params.incomingStatus
|
||||
return {
|
||||
...composeInitialState,
|
||||
dirty: true,
|
||||
timestamp: Date.now(),
|
||||
visibility: actualStatus.visibility,
|
||||
visibilityLock: actualStatus.visibility === 'direct',
|
||||
replyToStatus: actualStatus
|
||||
@ -56,6 +60,8 @@ const composeParseState = (
|
||||
case 'conversation':
|
||||
return {
|
||||
...composeInitialState,
|
||||
dirty: true,
|
||||
timestamp: Date.now(),
|
||||
visibility: 'direct',
|
||||
visibilityLock: true
|
||||
}
|
||||
|
@ -1,15 +1,14 @@
|
||||
import client from '@root/api/client'
|
||||
import { ComposeState } from '@screens/Compose/utils/types'
|
||||
import { SharedComposeProp } from '@screens/Tabs/Shared/sharedScreens'
|
||||
import * as Crypto from 'expo-crypto'
|
||||
|
||||
const composePost = async (
|
||||
params: SharedComposeProp['route']['params'],
|
||||
params: Nav.RootStackParamList['Screen-Compose'],
|
||||
composeState: ComposeState
|
||||
) => {
|
||||
const formData = new FormData()
|
||||
|
||||
if (params?.type === 'reply') {
|
||||
if (composeState.replyToStatus) {
|
||||
formData.append('in_reply_to_id', composeState.replyToStatus!.id)
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,29 @@ const composeReducer = (
|
||||
action: ComposeAction
|
||||
): ComposeState => {
|
||||
switch (action.type) {
|
||||
case 'loadDraft':
|
||||
const draft = action.payload
|
||||
console.log(draft.text)
|
||||
return {
|
||||
...state,
|
||||
...(draft.spoiler?.length && {
|
||||
spoiler: {
|
||||
...state.spoiler,
|
||||
active: true,
|
||||
raw: draft.spoiler
|
||||
}
|
||||
}),
|
||||
...(draft.text?.length && {
|
||||
text: { ...state.text, raw: draft.text }
|
||||
}),
|
||||
...(draft.poll && { poll: draft.poll }),
|
||||
...(draft.attachments && { attachments: draft.attachments }),
|
||||
visibility: draft.visibility,
|
||||
visibilityLock: draft.visibilityLock,
|
||||
replyToStatus: draft.replyToStatus
|
||||
}
|
||||
case 'dirty':
|
||||
return { ...state, dirty: action.payload }
|
||||
case 'posting':
|
||||
return { ...state, posting: action.payload }
|
||||
case 'spoiler':
|
||||
|
23
src/screens/Compose/utils/types.d.ts
vendored
23
src/screens/Compose/utils/types.d.ts
vendored
@ -1,10 +1,23 @@
|
||||
export type ExtendedAttachment = {
|
||||
remote?: Mastodon.Attachment
|
||||
local?: ImageInfo & { local_thumbnail?: string; hash?: string }
|
||||
local?: App.IImageInfo & { local_thumbnail?: string; hash?: string }
|
||||
uploading?: boolean
|
||||
}
|
||||
|
||||
export type ComposeStateDraft = {
|
||||
timestamp: number
|
||||
spoiler?: string
|
||||
text?: string
|
||||
poll?: ComposeState['poll']
|
||||
attachments?: ComposeState['attachments']
|
||||
visibility: ComposeState['visibility']
|
||||
visibilityLock: ComposeState['visibilityLock']
|
||||
replyToStatus?: ComposeState['replyToStatus']
|
||||
}
|
||||
|
||||
export type ComposeState = {
|
||||
dirty: boolean
|
||||
timestamp: number
|
||||
posting: boolean
|
||||
spoiler: {
|
||||
active: boolean
|
||||
@ -55,6 +68,14 @@ export type ComposeState = {
|
||||
}
|
||||
|
||||
export type ComposeAction =
|
||||
| {
|
||||
type: 'loadDraft'
|
||||
payload: ComposeStateDraft
|
||||
}
|
||||
| {
|
||||
type: 'dirty'
|
||||
payload: ComposeState['dirty']
|
||||
}
|
||||
| {
|
||||
type: 'posting'
|
||||
payload: ComposeState['posting']
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
getLocalActiveIndex,
|
||||
getLocalInstances,
|
||||
InstanceLocal,
|
||||
localUpdateActiveIndex
|
||||
updateLocalActiveIndex
|
||||
} from '@utils/slices/instancesSlice'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
@ -46,7 +46,7 @@ const AccountButton: React.FC<Props> = ({ instance, disabled = false }) => {
|
||||
haptics('Light')
|
||||
analytics('switch_existing_press')
|
||||
queryClient.clear()
|
||||
dispatch(localUpdateActiveIndex(instance))
|
||||
dispatch(updateLocalActiveIndex(instance))
|
||||
navigation.goBack()
|
||||
}}
|
||||
/>
|
||||
|
@ -3,7 +3,7 @@ import NetInfo from '@react-native-community/netinfo'
|
||||
import { store } from '@root/store'
|
||||
import {
|
||||
localRemoveInstance,
|
||||
localUpdateAccount
|
||||
updateLocalAccount
|
||||
} from '@utils/slices/instancesSlice'
|
||||
import log from './log'
|
||||
|
||||
@ -35,7 +35,7 @@ const netInfo = async (): Promise<{
|
||||
return Promise.resolve({ connected: true, corruputed: '' })
|
||||
} else {
|
||||
store.dispatch(
|
||||
localUpdateAccount({
|
||||
updateLocalAccount({
|
||||
acct: res.acct,
|
||||
avatarStatic: res.avatar_static
|
||||
})
|
||||
|
@ -1,9 +0,0 @@
|
||||
import { preventScreenCaptureAsync } from 'expo-screen-capture'
|
||||
import log from './log'
|
||||
|
||||
const preventScreenshot = () => {
|
||||
log('log', 'Screenshot', 'preventing')
|
||||
preventScreenCaptureAsync()
|
||||
}
|
||||
|
||||
export default preventScreenshot
|
@ -28,6 +28,7 @@ const instancesMigration = {
|
||||
...state.local,
|
||||
instances: state.local.instances.map(instance => {
|
||||
instance.max_toot_chars = 500
|
||||
instance.drafts = []
|
||||
return instance
|
||||
})
|
||||
}
|
||||
|
@ -2,8 +2,10 @@ import client from '@api/client'
|
||||
import analytics from '@components/analytics'
|
||||
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
import { RootState } from '@root/store'
|
||||
import { ComposeStateDraft } from '@screens/Compose/utils/types'
|
||||
import * as AuthSession from 'expo-auth-session'
|
||||
import * as Localization from 'expo-localization'
|
||||
import { findIndex } from 'lodash'
|
||||
|
||||
export type InstanceLocal = {
|
||||
appData: {
|
||||
@ -23,6 +25,7 @@ export type InstanceLocal = {
|
||||
notification: {
|
||||
latestTime?: Mastodon.Notification['created_at']
|
||||
}
|
||||
drafts: ComposeStateDraft[]
|
||||
}
|
||||
|
||||
export type InstancesState = {
|
||||
@ -36,8 +39,8 @@ export type InstancesState = {
|
||||
}
|
||||
}
|
||||
|
||||
export const localUpdateAccountPreferences = createAsyncThunk(
|
||||
'instances/localUpdateAccountPreferences',
|
||||
export const updateLocalAccountPreferences = createAsyncThunk(
|
||||
'instances/updateLocalAccountPreferences',
|
||||
async (): Promise<Mastodon.Preferences> => {
|
||||
const preferences = await client<Mastodon.Preferences>({
|
||||
method: 'get',
|
||||
@ -119,7 +122,8 @@ export const localAddInstance = createAsyncThunk(
|
||||
},
|
||||
notification: {
|
||||
latestTime: undefined
|
||||
}
|
||||
},
|
||||
drafts: []
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -179,7 +183,7 @@ const instancesSlice = createSlice({
|
||||
name: 'instances',
|
||||
initialState: instancesInitialState,
|
||||
reducers: {
|
||||
localUpdateActiveIndex: (state, action: PayloadAction<InstanceLocal>) => {
|
||||
updateLocalActiveIndex: (state, action: PayloadAction<InstanceLocal>) => {
|
||||
state.local.activeIndex = state.local.instances.findIndex(
|
||||
instance =>
|
||||
instance.url === action.payload.url &&
|
||||
@ -187,7 +191,7 @@ const instancesSlice = createSlice({
|
||||
instance.account.id === action.payload.account.id
|
||||
)
|
||||
},
|
||||
localUpdateAccount: (
|
||||
updateLocalAccount: (
|
||||
state,
|
||||
action: PayloadAction<
|
||||
Pick<InstanceLocal['account'], 'acct' & 'avatarStatic'>
|
||||
@ -200,18 +204,42 @@ const instancesSlice = createSlice({
|
||||
}
|
||||
}
|
||||
},
|
||||
localUpdateNotification: (
|
||||
updateLocalNotification: (
|
||||
state,
|
||||
action: PayloadAction<Partial<InstanceLocal['notification']>>
|
||||
) => {
|
||||
state.local.instances[state.local.activeIndex!].notification =
|
||||
action.payload
|
||||
if (state.local.activeIndex !== null) {
|
||||
state.local.instances[state.local.activeIndex].notification =
|
||||
action.payload
|
||||
}
|
||||
},
|
||||
remoteUpdate: (
|
||||
updateLocalDraft: (state, action: PayloadAction<ComposeStateDraft>) => {
|
||||
if (state.local.activeIndex !== null) {
|
||||
const draftIndex = findIndex(
|
||||
state.local.instances[state.local.activeIndex].drafts,
|
||||
['timestamp', action.payload.timestamp]
|
||||
)
|
||||
if (draftIndex === -1) {
|
||||
state.local.instances[state.local.activeIndex].drafts.unshift(
|
||||
action.payload
|
||||
)
|
||||
} else {
|
||||
state.local.instances[state.local.activeIndex].drafts[draftIndex] =
|
||||
action.payload
|
||||
}
|
||||
}
|
||||
},
|
||||
removeLocalDraft: (
|
||||
state,
|
||||
action: PayloadAction<InstancesState['remote']['url']>
|
||||
action: PayloadAction<ComposeStateDraft['timestamp']>
|
||||
) => {
|
||||
state.remote.url = action.payload
|
||||
if (state.local.activeIndex !== null) {
|
||||
state.local.instances[
|
||||
state.local.activeIndex
|
||||
].drafts = state.local.instances[
|
||||
state.local.activeIndex
|
||||
].drafts?.filter(draft => draft.timestamp !== action.payload)
|
||||
}
|
||||
}
|
||||
},
|
||||
extraReducers: builder => {
|
||||
@ -255,11 +283,11 @@ const instancesSlice = createSlice({
|
||||
console.error(action.error)
|
||||
})
|
||||
|
||||
.addCase(localUpdateAccountPreferences.fulfilled, (state, action) => {
|
||||
.addCase(updateLocalAccountPreferences.fulfilled, (state, action) => {
|
||||
state.local.instances[state.local.activeIndex!].account.preferences =
|
||||
action.payload
|
||||
})
|
||||
.addCase(localUpdateAccountPreferences.rejected, (_, action) => {
|
||||
.addCase(updateLocalAccountPreferences.rejected, (_, action) => {
|
||||
console.error(action.error)
|
||||
})
|
||||
}
|
||||
@ -289,13 +317,18 @@ export const getLocalNotification = ({ instances: { local } }: RootState) =>
|
||||
local.activeIndex !== null
|
||||
? local.instances[local.activeIndex].notification
|
||||
: undefined
|
||||
export const getLocalDrafts = ({ instances: { local } }: RootState) =>
|
||||
local.activeIndex !== null
|
||||
? local.instances[local.activeIndex].drafts
|
||||
: undefined
|
||||
export const getRemoteUrl = ({ instances: { remote } }: RootState) => remote.url
|
||||
|
||||
export const {
|
||||
localUpdateActiveIndex,
|
||||
localUpdateAccount,
|
||||
localUpdateNotification,
|
||||
remoteUpdate
|
||||
updateLocalActiveIndex,
|
||||
updateLocalAccount,
|
||||
updateLocalNotification,
|
||||
updateLocalDraft,
|
||||
removeLocalDraft
|
||||
} = instancesSlice.actions
|
||||
|
||||
export default instancesSlice.reducer
|
||||
|
@ -4,7 +4,7 @@ export const StyleConstants = {
|
||||
Font: {
|
||||
Size: { S: 14, M: 16, L: 18 },
|
||||
LineHeight: { S: 20, M: 22, L: 30 },
|
||||
Weight: { Bold: '600' as '600' }
|
||||
Weight: { Normal: '400' as '400', Bold: '600' as '600' }
|
||||
},
|
||||
FontStyle: {
|
||||
S: { fontSize: 14, lineHeight: 20 },
|
||||
|
@ -27,7 +27,7 @@ const themeColors: {
|
||||
},
|
||||
primaryOverlay: {
|
||||
light: 'rgb(250, 250, 250)',
|
||||
dark: 'rgb(180, 180, 180)'
|
||||
dark: 'rgb(200, 200, 200)'
|
||||
},
|
||||
secondary: {
|
||||
light: 'rgb(135, 135, 135)',
|
||||
@ -43,7 +43,7 @@ const themeColors: {
|
||||
},
|
||||
red: {
|
||||
light: 'rgb(225, 45, 35)',
|
||||
dark: 'rgb(225, 98, 89)'
|
||||
dark: 'rgb(225, 78, 79)'
|
||||
},
|
||||
|
||||
background: {
|
||||
|
@ -8651,6 +8651,11 @@ react-native-svg@12.1.0:
|
||||
css-select "^2.1.0"
|
||||
css-tree "^1.0.0-alpha.39"
|
||||
|
||||
react-native-swipe-list-view@^3.2.6:
|
||||
version "3.2.6"
|
||||
resolved "https://registry.yarnpkg.com/react-native-swipe-list-view/-/react-native-swipe-list-view-3.2.6.tgz#dde8f65dcdc24f50fafc05da9e90b139b1c3b185"
|
||||
integrity sha512-FIrLzYjdxpMmD1hinP6bLSVS2eR2bhFdFuaqOJ+6/EuWDqZMBQcL4FLbkIwulMOc39JGdGZAxGExJYs7VKiQeQ==
|
||||
|
||||
react-native-tab-view-viewpager-adapter@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native-tab-view-viewpager-adapter/-/react-native-tab-view-viewpager-adapter-1.1.0.tgz#d6e085ed1c91a13e714d87395d428f8afc2b3377"
|
||||
|
Loading…
x
Reference in New Issue
Block a user