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