mirror of https://github.com/tooot-app/app
Added instance configuration support
This commit is contained in:
parent
98dd7b2b46
commit
7ac789c18c
|
@ -299,8 +299,28 @@ declare namespace Mastodon {
|
||||||
// Others
|
// Others
|
||||||
thumbnail?: string
|
thumbnail?: string
|
||||||
contact_account?: Account
|
contact_account?: Account
|
||||||
|
configuration?: {
|
||||||
// Custom
|
statuses: {
|
||||||
|
max_characters: number
|
||||||
|
max_media_attachments: number
|
||||||
|
characters_reserved_per_url: number
|
||||||
|
}
|
||||||
|
media_attachments: {
|
||||||
|
supported_mime_types: string[]
|
||||||
|
image_size_limit: number
|
||||||
|
image_matrix_limit: number
|
||||||
|
video_size_limit: number
|
||||||
|
video_frame_rate_limit: number
|
||||||
|
video_matrix_limit: number
|
||||||
|
}
|
||||||
|
polls: {
|
||||||
|
max_options: number
|
||||||
|
max_characters_per_option: number
|
||||||
|
min_expiration: number
|
||||||
|
max_expiration: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Custom - to be deprecated in v4
|
||||||
max_toot_chars?: number
|
max_toot_chars?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ import pushUseReceive from '@utils/push/useReceive'
|
||||||
import pushUseRespond from '@utils/push/useRespond'
|
import pushUseRespond from '@utils/push/useRespond'
|
||||||
import { updatePreviousTab } from '@utils/slices/contextsSlice'
|
import { updatePreviousTab } from '@utils/slices/contextsSlice'
|
||||||
import { updateAccountPreferences } from '@utils/slices/instances/updateAccountPreferences'
|
import { updateAccountPreferences } from '@utils/slices/instances/updateAccountPreferences'
|
||||||
|
import { updateConfiguration } from '@utils/slices/instances/updateConfiguration'
|
||||||
import { updateFilters } from '@utils/slices/instances/updateFilters'
|
import { updateFilters } from '@utils/slices/instances/updateFilters'
|
||||||
import { getInstanceActive, getInstances } from '@utils/slices/instancesSlice'
|
import { getInstanceActive, getInstances } from '@utils/slices/instancesSlice'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
|
@ -108,6 +109,7 @@ const Screens: 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 (instanceActive !== -1) {
|
if (instanceActive !== -1) {
|
||||||
|
dispatch(updateConfiguration())
|
||||||
dispatch(updateFilters())
|
dispatch(updateFilters())
|
||||||
dispatch(updateAccountPreferences())
|
dispatch(updateAccountPreferences())
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,9 +22,8 @@ const InstanceAuth = React.memo(
|
||||||
useProxy: false
|
useProxy: false
|
||||||
})
|
})
|
||||||
|
|
||||||
const navigation = useNavigation<
|
const navigation =
|
||||||
TabMeStackNavigationProp<'Tab-Me-Root' | 'Tab-Me-Switch'>
|
useNavigation<TabMeStackNavigationProp<'Tab-Me-Root' | 'Tab-Me-Switch'>>()
|
||||||
>()
|
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
|
|
||||||
|
@ -70,7 +69,6 @@ const InstanceAuth = React.memo(
|
||||||
domain: instanceDomain,
|
domain: instanceDomain,
|
||||||
token: accessToken,
|
token: accessToken,
|
||||||
instance,
|
instance,
|
||||||
max_toot_chars: instance.max_toot_chars,
|
|
||||||
appData
|
appData
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
|
@ -11,7 +11,7 @@ export interface Props {
|
||||||
resize?: { width?: number; height?: number } // Resize mode contain
|
resize?: { width?: number; height?: number } // Resize mode contain
|
||||||
showActionSheetWithOptions: (
|
showActionSheetWithOptions: (
|
||||||
options: ActionSheetOptions,
|
options: ActionSheetOptions,
|
||||||
callback: (i: number) => void
|
callback: (i?: number | undefined) => void | Promise<void>
|
||||||
) => void
|
) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,9 +57,8 @@ const mediaSelector = async ({
|
||||||
},
|
},
|
||||||
async buttonIndex => {
|
async buttonIndex => {
|
||||||
if (buttonIndex === 0) {
|
if (buttonIndex === 0) {
|
||||||
const {
|
const { status } =
|
||||||
status
|
await ImagePicker.requestMediaLibraryPermissionsAsync()
|
||||||
} = await ImagePicker.requestMediaLibraryPermissionsAsync()
|
|
||||||
if (status !== 'granted') {
|
if (status !== 'granted') {
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
i18next.t('componentMediaSelector:library.alert.title'),
|
i18next.t('componentMediaSelector:library.alert.title'),
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||||
import { updateStoreReview } from '@utils/slices/contextsSlice'
|
import { updateStoreReview } from '@utils/slices/contextsSlice'
|
||||||
import {
|
import {
|
||||||
getInstanceAccount,
|
getInstanceAccount,
|
||||||
getInstanceMaxTootChar,
|
getInstanceConfigurationStatusMaxChars,
|
||||||
removeInstanceDraft,
|
removeInstanceDraft,
|
||||||
updateInstanceDraft
|
updateInstanceDraft
|
||||||
} from '@utils/slices/instancesSlice'
|
} from '@utils/slices/instancesSlice'
|
||||||
|
@ -103,7 +103,10 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
|
||||||
initialReducerState
|
initialReducerState
|
||||||
)
|
)
|
||||||
|
|
||||||
const maxTootChars = useSelector(getInstanceMaxTootChar, () => true)
|
const maxTootChars = useSelector(
|
||||||
|
getInstanceConfigurationStatusMaxChars,
|
||||||
|
() => true
|
||||||
|
)
|
||||||
const totalTextCount =
|
const totalTextCount =
|
||||||
(composeState.spoiler.active ? composeState.spoiler.count : 0) +
|
(composeState.spoiler.active ? composeState.spoiler.count : 0) +
|
||||||
composeState.text.count
|
composeState.text.count
|
||||||
|
|
|
@ -29,6 +29,8 @@ import ComposeDrafts from './Root/Drafts'
|
||||||
import FastImage from 'react-native-fast-image'
|
import FastImage from 'react-native-fast-image'
|
||||||
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
||||||
import { ComposeState } from './utils/types'
|
import { ComposeState } from './utils/types'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { getInstanceConfigurationStatusCharsURL } from '@utils/slices/instancesSlice'
|
||||||
|
|
||||||
const prefetchEmojis = (
|
const prefetchEmojis = (
|
||||||
sortedEmojis: NonNullable<ComposeState['emoji']['emojis']>,
|
sortedEmojis: NonNullable<ComposeState['emoji']['emojis']>,
|
||||||
|
@ -54,11 +56,18 @@ const prefetchEmojis = (
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export let instanceConfigurationStatusCharsURL = 23
|
||||||
|
|
||||||
const ComposeRoot = React.memo(
|
const ComposeRoot = React.memo(
|
||||||
() => {
|
() => {
|
||||||
const { reduceMotionEnabled } = useAccessibility()
|
const { reduceMotionEnabled } = useAccessibility()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
|
||||||
|
instanceConfigurationStatusCharsURL = useSelector(
|
||||||
|
getInstanceConfigurationStatusCharsURL,
|
||||||
|
() => true
|
||||||
|
)
|
||||||
|
|
||||||
const accessibleRefDrafts = useRef(null)
|
const accessibleRefDrafts = useRef(null)
|
||||||
const accessibleRefAttachments = useRef(null)
|
const accessibleRefAttachments = useRef(null)
|
||||||
const accessibleRefEmojis = useRef(null)
|
const accessibleRefEmojis = useRef(null)
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import analytics from '@components/analytics'
|
import analytics from '@components/analytics'
|
||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
import { useActionSheet } from '@expo/react-native-action-sheet'
|
import { useActionSheet } from '@expo/react-native-action-sheet'
|
||||||
|
import { getInstanceConfigurationStatusMaxAttachments } from '@utils/slices/instancesSlice'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import layoutAnimation from '@utils/styles/layoutAnimation'
|
import layoutAnimation from '@utils/styles/layoutAnimation'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useCallback, useContext, useMemo } from 'react'
|
import React, { useCallback, useContext, useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Pressable, StyleSheet, View } from 'react-native'
|
import { Pressable, StyleSheet, View } from 'react-native'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
import ComposeContext from '../utils/createContext'
|
import ComposeContext from '../utils/createContext'
|
||||||
import addAttachment from './Footer/addAttachment'
|
import addAttachment from './Footer/addAttachment'
|
||||||
|
|
||||||
|
@ -15,6 +17,10 @@ const ComposeActions: React.FC = () => {
|
||||||
const { composeState, composeDispatch } = useContext(ComposeContext)
|
const { composeState, composeDispatch } = useContext(ComposeContext)
|
||||||
const { t } = useTranslation('screenCompose')
|
const { t } = useTranslation('screenCompose')
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
const instanceConfigurationStatusMaxAttachments = useSelector(
|
||||||
|
getInstanceConfigurationStatusMaxAttachments,
|
||||||
|
() => true
|
||||||
|
)
|
||||||
|
|
||||||
const attachmentColor = useMemo(() => {
|
const attachmentColor = useMemo(() => {
|
||||||
if (composeState.poll.active) return theme.disabled
|
if (composeState.poll.active) return theme.disabled
|
||||||
|
@ -28,7 +34,10 @@ const ComposeActions: React.FC = () => {
|
||||||
const attachmentOnPress = useCallback(async () => {
|
const attachmentOnPress = useCallback(async () => {
|
||||||
if (composeState.poll.active) return
|
if (composeState.poll.active) return
|
||||||
|
|
||||||
if (composeState.attachments.uploads.length < 4) {
|
if (
|
||||||
|
composeState.attachments.uploads.length <
|
||||||
|
instanceConfigurationStatusMaxAttachments
|
||||||
|
) {
|
||||||
analytics('compose_actions_attachment_press', {
|
analytics('compose_actions_attachment_press', {
|
||||||
count: composeState.attachments.uploads.length
|
count: composeState.attachments.uploads.length
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,11 +3,13 @@ import Button from '@components/Button'
|
||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
import { MenuRow } from '@components/Menu'
|
import { MenuRow } from '@components/Menu'
|
||||||
import { useActionSheet } from '@expo/react-native-action-sheet'
|
import { useActionSheet } from '@expo/react-native-action-sheet'
|
||||||
|
import { getInstanceConfigurationPoll } 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, { useContext, useEffect, useState } from 'react'
|
import React, { useContext, useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { StyleSheet, TextInput, View } from 'react-native'
|
import { StyleSheet, Text, TextInput, View } from 'react-native'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
import ComposeContext from '../../utils/createContext'
|
import ComposeContext from '../../utils/createContext'
|
||||||
|
|
||||||
const ComposePoll: React.FC = () => {
|
const ComposePoll: React.FC = () => {
|
||||||
|
@ -21,6 +23,16 @@ const ComposePoll: React.FC = () => {
|
||||||
const { t } = useTranslation('screenCompose')
|
const { t } = useTranslation('screenCompose')
|
||||||
const { mode, theme } = useTheme()
|
const { mode, theme } = useTheme()
|
||||||
|
|
||||||
|
const instanceConfigurationPoll = useSelector(
|
||||||
|
getInstanceConfigurationPoll,
|
||||||
|
() => true
|
||||||
|
)
|
||||||
|
const MAX_OPTIONS = instanceConfigurationPoll.max_options
|
||||||
|
const MAX_CHARS_PER_OPTION =
|
||||||
|
instanceConfigurationPoll.max_characters_per_option
|
||||||
|
const MIN_EXPIRATION = instanceConfigurationPoll.min_expiration
|
||||||
|
const MAX_EXPIRATION = instanceConfigurationPoll.max_expiration
|
||||||
|
|
||||||
const [firstRender, setFirstRender] = useState(true)
|
const [firstRender, setFirstRender] = useState(true)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFirstRender(false)
|
setFirstRender(false)
|
||||||
|
@ -67,7 +79,7 @@ const ComposePoll: React.FC = () => {
|
||||||
: t('content.root.footer.poll.option.placeholder.single')
|
: t('content.root.footer.poll.option.placeholder.single')
|
||||||
}
|
}
|
||||||
placeholderTextColor={theme.disabled}
|
placeholderTextColor={theme.disabled}
|
||||||
maxLength={50}
|
maxLength={MAX_CHARS_PER_OPTION}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
value={options[i]}
|
value={options[i]}
|
||||||
onChangeText={e =>
|
onChangeText={e =>
|
||||||
|
@ -82,7 +94,6 @@ const ComposePoll: React.FC = () => {
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.controlAmount}>
|
<View style={styles.controlAmount}>
|
||||||
<View style={styles.firstButton}>
|
|
||||||
<Button
|
<Button
|
||||||
{...(total > 2
|
{...(total > 2
|
||||||
? {
|
? {
|
||||||
|
@ -110,9 +121,11 @@ const ComposePoll: React.FC = () => {
|
||||||
round
|
round
|
||||||
disabled={!(total > 2)}
|
disabled={!(total > 2)}
|
||||||
/>
|
/>
|
||||||
</View>
|
<Text style={styles.controlCount}>
|
||||||
|
{total} / {MAX_OPTIONS}
|
||||||
|
</Text>
|
||||||
<Button
|
<Button
|
||||||
{...(total < 4
|
{...(total < MAX_OPTIONS
|
||||||
? {
|
? {
|
||||||
accessibilityLabel: t(
|
accessibilityLabel: t(
|
||||||
'content.root.footer.poll.quantity.increase.accessibilityLabel',
|
'content.root.footer.poll.quantity.increase.accessibilityLabel',
|
||||||
|
@ -127,7 +140,7 @@ const ComposePoll: React.FC = () => {
|
||||||
})}
|
})}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
analytics('compose_poll_increase_press')
|
analytics('compose_poll_increase_press')
|
||||||
total < 4 &&
|
total < MAX_OPTIONS &&
|
||||||
composeDispatch({
|
composeDispatch({
|
||||||
type: 'poll',
|
type: 'poll',
|
||||||
payload: { total: total + 1 }
|
payload: { total: total + 1 }
|
||||||
|
@ -136,7 +149,7 @@ const ComposePoll: React.FC = () => {
|
||||||
type='icon'
|
type='icon'
|
||||||
content='Plus'
|
content='Plus'
|
||||||
round
|
round
|
||||||
disabled={!(total < 4)}
|
disabled={!(total < MAX_OPTIONS)}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.controlOptions}>
|
<View style={styles.controlOptions}>
|
||||||
|
@ -158,7 +171,7 @@ const ComposePoll: React.FC = () => {
|
||||||
cancelButtonIndex: 2
|
cancelButtonIndex: 2
|
||||||
},
|
},
|
||||||
index => {
|
index => {
|
||||||
if (index < 2) {
|
if (index && index < 2) {
|
||||||
analytics('compose_poll_expiration_press', {
|
analytics('compose_poll_expiration_press', {
|
||||||
current: multiple,
|
current: multiple,
|
||||||
new: index === 1
|
new: index === 1
|
||||||
|
@ -177,6 +190,7 @@ const ComposePoll: React.FC = () => {
|
||||||
title={t('content.root.footer.poll.expiration.heading')}
|
title={t('content.root.footer.poll.expiration.heading')}
|
||||||
content={t(`content.root.footer.poll.expiration.options.${expire}`)}
|
content={t(`content.root.footer.poll.expiration.options.${expire}`)}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
|
// @ts-ignore
|
||||||
const expirations: [
|
const expirations: [
|
||||||
'300',
|
'300',
|
||||||
'1800',
|
'1800',
|
||||||
|
@ -185,7 +199,19 @@ const ComposePoll: React.FC = () => {
|
||||||
'86400',
|
'86400',
|
||||||
'259200',
|
'259200',
|
||||||
'604800'
|
'604800'
|
||||||
] = ['300', '1800', '3600', '21600', '86400', '259200', '604800']
|
] = [
|
||||||
|
'300',
|
||||||
|
'1800',
|
||||||
|
'3600',
|
||||||
|
'21600',
|
||||||
|
'86400',
|
||||||
|
'259200',
|
||||||
|
'604800'
|
||||||
|
].filter(
|
||||||
|
expiration =>
|
||||||
|
parseInt(expiration) >= MIN_EXPIRATION &&
|
||||||
|
parseInt(expiration) <= MAX_EXPIRATION
|
||||||
|
)
|
||||||
showActionSheetWithOptions(
|
showActionSheetWithOptions(
|
||||||
{
|
{
|
||||||
options: [
|
options: [
|
||||||
|
@ -197,7 +223,7 @@ const ComposePoll: React.FC = () => {
|
||||||
cancelButtonIndex: 7
|
cancelButtonIndex: 7
|
||||||
},
|
},
|
||||||
index => {
|
index => {
|
||||||
if (index < 7) {
|
if (index && expirations.length < 7) {
|
||||||
analytics('compose_poll_expiration_press', {
|
analytics('compose_poll_expiration_press', {
|
||||||
current: expire,
|
current: expire,
|
||||||
new: expirations[index]
|
new: expirations[index]
|
||||||
|
@ -246,14 +272,15 @@ const styles = StyleSheet.create({
|
||||||
controlAmount: {
|
controlAmount: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
justifyContent: 'flex-end',
|
justifyContent: 'flex-end',
|
||||||
marginRight: StyleConstants.Spacing.M
|
marginRight: StyleConstants.Spacing.M
|
||||||
},
|
},
|
||||||
controlOptions: {
|
controlOptions: {
|
||||||
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding
|
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding
|
||||||
},
|
},
|
||||||
firstButton: {
|
controlCount: {
|
||||||
marginRight: StyleConstants.Spacing.S
|
marginHorizontal: StyleConstants.Spacing.S
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ export interface Props {
|
||||||
composeDispatch: Dispatch<ComposeAction>
|
composeDispatch: Dispatch<ComposeAction>
|
||||||
showActionSheetWithOptions: (
|
showActionSheetWithOptions: (
|
||||||
options: ActionSheetOptions,
|
options: ActionSheetOptions,
|
||||||
callback: (i: number) => void
|
callback: (i?: number | undefined) => void | Promise<void>
|
||||||
) => void
|
) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { FetchOptions } from 'react-query/types/core/query'
|
||||||
import Autolinker from '@root/modules/autolinker'
|
import Autolinker from '@root/modules/autolinker'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { ComposeAction, ComposeState } from './utils/types'
|
import { ComposeAction, ComposeState } from './utils/types'
|
||||||
|
import { instanceConfigurationStatusCharsURL } from './Root'
|
||||||
|
|
||||||
export interface Params {
|
export interface Params {
|
||||||
textInput: ComposeState['textInputFocus']['current']
|
textInput: ComposeState['textInputFocus']['current']
|
||||||
|
@ -92,7 +93,7 @@ const formatText = ({
|
||||||
children.push(<TagText key={index} text={main} />)
|
children.push(<TagText key={index} text={main} />)
|
||||||
switch (tag.type) {
|
switch (tag.type) {
|
||||||
case 'url':
|
case 'url':
|
||||||
contentLength = contentLength + 23
|
contentLength = contentLength + instanceConfigurationStatusCharsURL
|
||||||
break
|
break
|
||||||
case 'accounts':
|
case 'accounts':
|
||||||
const theMatch = main.match(/@/g)
|
const theMatch = main.match(/@/g)
|
||||||
|
|
|
@ -22,7 +22,7 @@ const instancesPersistConfig = {
|
||||||
key: 'instances',
|
key: 'instances',
|
||||||
prefix,
|
prefix,
|
||||||
storage: secureStorage,
|
storage: secureStorage,
|
||||||
version: 5,
|
version: 6,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
migrate: createMigrate(instancesMigration)
|
migrate: createMigrate(instancesMigration)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { InstanceV3 } from './v3'
|
import { InstanceV3 } from './v3'
|
||||||
import { InstanceV4 } from './v4'
|
import { InstanceV4 } from './v4'
|
||||||
|
import { InstanceV5 } from './v5'
|
||||||
|
|
||||||
const instancesMigration = {
|
const instancesMigration = {
|
||||||
4: (state: InstanceV3) => {
|
4: (state: InstanceV3) => {
|
||||||
|
@ -27,7 +28,6 @@ const instancesMigration = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
5: (state: InstanceV4) => {
|
5: (state: InstanceV4) => {
|
||||||
// Migration is run on each start, don't know why
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (state.instances.length && !state.instances[0].notifications_filter) {
|
if (state.instances.length && !state.instances[0].notifications_filter) {
|
||||||
return {
|
return {
|
||||||
|
@ -47,6 +47,11 @@ const instancesMigration = {
|
||||||
} else {
|
} else {
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
6: (state: InstanceV5) => {
|
||||||
|
return state.instances.map(instance => {
|
||||||
|
return { ...instance, configuration: undefined }
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
import { ComposeStateDraft } from '@screens/Compose/utils/types'
|
||||||
|
|
||||||
|
type Instance = {
|
||||||
|
active: boolean
|
||||||
|
appData: {
|
||||||
|
clientId: string
|
||||||
|
clientSecret: string
|
||||||
|
}
|
||||||
|
url: string
|
||||||
|
token: string
|
||||||
|
uri: Mastodon.Instance['uri']
|
||||||
|
urls: Mastodon.Instance['urls']
|
||||||
|
max_toot_chars: number
|
||||||
|
account: {
|
||||||
|
id: Mastodon.Account['id']
|
||||||
|
acct: Mastodon.Account['acct']
|
||||||
|
avatarStatic: Mastodon.Account['avatar_static']
|
||||||
|
preferences: Mastodon.Preferences
|
||||||
|
}
|
||||||
|
filters: Mastodon.Filter[]
|
||||||
|
notifications_filter: {
|
||||||
|
follow: boolean
|
||||||
|
favourite: boolean
|
||||||
|
reblog: boolean
|
||||||
|
mention: boolean
|
||||||
|
poll: boolean
|
||||||
|
follow_request: boolean
|
||||||
|
}
|
||||||
|
push:
|
||||||
|
| {
|
||||||
|
global: { loading: boolean; value: boolean }
|
||||||
|
decode: { loading: boolean; value: true }
|
||||||
|
alerts: {
|
||||||
|
follow: {
|
||||||
|
loading: boolean
|
||||||
|
value: Mastodon.PushSubscription['alerts']['follow']
|
||||||
|
}
|
||||||
|
favourite: {
|
||||||
|
loading: boolean
|
||||||
|
value: Mastodon.PushSubscription['alerts']['favourite']
|
||||||
|
}
|
||||||
|
reblog: {
|
||||||
|
loading: boolean
|
||||||
|
value: Mastodon.PushSubscription['alerts']['reblog']
|
||||||
|
}
|
||||||
|
mention: {
|
||||||
|
loading: boolean
|
||||||
|
value: Mastodon.PushSubscription['alerts']['mention']
|
||||||
|
}
|
||||||
|
poll: {
|
||||||
|
loading: boolean
|
||||||
|
value: Mastodon.PushSubscription['alerts']['poll']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keys: {
|
||||||
|
auth: string
|
||||||
|
public: string
|
||||||
|
private: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
global: { loading: boolean; value: boolean }
|
||||||
|
decode: { loading: boolean; value: false }
|
||||||
|
alerts: {
|
||||||
|
follow: {
|
||||||
|
loading: boolean
|
||||||
|
value: Mastodon.PushSubscription['alerts']['follow']
|
||||||
|
}
|
||||||
|
favourite: {
|
||||||
|
loading: boolean
|
||||||
|
value: Mastodon.PushSubscription['alerts']['favourite']
|
||||||
|
}
|
||||||
|
reblog: {
|
||||||
|
loading: boolean
|
||||||
|
value: Mastodon.PushSubscription['alerts']['reblog']
|
||||||
|
}
|
||||||
|
mention: {
|
||||||
|
loading: boolean
|
||||||
|
value: Mastodon.PushSubscription['alerts']['mention']
|
||||||
|
}
|
||||||
|
poll: {
|
||||||
|
loading: boolean
|
||||||
|
value: Mastodon.PushSubscription['alerts']['poll']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keys: undefined
|
||||||
|
}
|
||||||
|
drafts: ComposeStateDraft[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InstanceV5 = {
|
||||||
|
instances: Instance[]
|
||||||
|
}
|
|
@ -9,13 +9,11 @@ const addInstance = createAsyncThunk(
|
||||||
domain,
|
domain,
|
||||||
token,
|
token,
|
||||||
instance,
|
instance,
|
||||||
max_toot_chars = 500,
|
|
||||||
appData
|
appData
|
||||||
}: {
|
}: {
|
||||||
domain: Instance['url']
|
domain: Instance['url']
|
||||||
token: Instance['token']
|
token: Instance['token']
|
||||||
instance: Mastodon.Instance
|
instance: Mastodon.Instance
|
||||||
max_toot_chars?: number
|
|
||||||
appData: Instance['appData']
|
appData: Instance['appData']
|
||||||
}): Promise<{ type: 'add' | 'overwrite'; data: Instance }> => {
|
}): Promise<{ type: 'add' | 'overwrite'; data: Instance }> => {
|
||||||
const { store } = require('@root/store')
|
const { store } = require('@root/store')
|
||||||
|
@ -70,13 +68,18 @@ const addInstance = createAsyncThunk(
|
||||||
token,
|
token,
|
||||||
uri: instance.uri,
|
uri: instance.uri,
|
||||||
urls: instance.urls,
|
urls: instance.urls,
|
||||||
max_toot_chars,
|
|
||||||
account: {
|
account: {
|
||||||
id,
|
id,
|
||||||
acct,
|
acct,
|
||||||
avatarStatic: avatar_static,
|
avatarStatic: avatar_static,
|
||||||
preferences
|
preferences
|
||||||
},
|
},
|
||||||
|
...(instance.max_toot_chars && {
|
||||||
|
max_toot_chars: instance.max_toot_chars
|
||||||
|
}),
|
||||||
|
...(instance.configuration && {
|
||||||
|
configuration: instance.configuration
|
||||||
|
}),
|
||||||
filters,
|
filters,
|
||||||
notifications_filter: {
|
notifications_filter: {
|
||||||
follow: true,
|
follow: true,
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
import apiInstance from '@api/instance'
|
||||||
|
import { createAsyncThunk } from '@reduxjs/toolkit'
|
||||||
|
|
||||||
|
export const updateConfiguration = createAsyncThunk(
|
||||||
|
'instances/updateConfiguration',
|
||||||
|
async (): Promise<Mastodon.Instance> => {
|
||||||
|
return apiInstance<Mastodon.Instance>({
|
||||||
|
method: 'get',
|
||||||
|
url: `instance`
|
||||||
|
}).then(res => res.body)
|
||||||
|
}
|
||||||
|
)
|
|
@ -6,6 +6,7 @@ import { findIndex } from 'lodash'
|
||||||
import addInstance from './instances/add'
|
import addInstance from './instances/add'
|
||||||
import removeInstance from './instances/remove'
|
import removeInstance from './instances/remove'
|
||||||
import { updateAccountPreferences } from './instances/updateAccountPreferences'
|
import { updateAccountPreferences } from './instances/updateAccountPreferences'
|
||||||
|
import { updateConfiguration } from './instances/updateConfiguration'
|
||||||
import { updateFilters } from './instances/updateFilters'
|
import { updateFilters } from './instances/updateFilters'
|
||||||
import { updateInstancePush } from './instances/updatePush'
|
import { updateInstancePush } from './instances/updatePush'
|
||||||
import { updateInstancePushAlert } from './instances/updatePushAlert'
|
import { updateInstancePushAlert } from './instances/updatePushAlert'
|
||||||
|
@ -21,13 +22,14 @@ export type Instance = {
|
||||||
token: string
|
token: string
|
||||||
uri: Mastodon.Instance['uri']
|
uri: Mastodon.Instance['uri']
|
||||||
urls: Mastodon.Instance['urls']
|
urls: Mastodon.Instance['urls']
|
||||||
max_toot_chars: number
|
|
||||||
account: {
|
account: {
|
||||||
id: Mastodon.Account['id']
|
id: Mastodon.Account['id']
|
||||||
acct: Mastodon.Account['acct']
|
acct: Mastodon.Account['acct']
|
||||||
avatarStatic: Mastodon.Account['avatar_static']
|
avatarStatic: Mastodon.Account['avatar_static']
|
||||||
preferences: Mastodon.Preferences
|
preferences: Mastodon.Preferences
|
||||||
}
|
}
|
||||||
|
max_toot_chars?: number // To be deprecated in v4
|
||||||
|
configuration?: Mastodon.Instance['configuration']
|
||||||
filters: Mastodon.Filter[]
|
filters: Mastodon.Filter[]
|
||||||
notifications_filter: {
|
notifications_filter: {
|
||||||
follow: boolean
|
follow: boolean
|
||||||
|
@ -107,8 +109,8 @@ export const instancesInitialState: InstancesState = {
|
||||||
instances: []
|
instances: []
|
||||||
}
|
}
|
||||||
|
|
||||||
const findInstanceActive = (state: Instance[]) =>
|
const findInstanceActive = (instances: Instance[]) =>
|
||||||
state.findIndex(instance => instance.active)
|
instances.findIndex(instance => instance.active)
|
||||||
|
|
||||||
const instancesSlice = createSlice({
|
const instancesSlice = createSlice({
|
||||||
name: 'instances',
|
name: 'instances',
|
||||||
|
@ -254,6 +256,18 @@ const instancesSlice = createSlice({
|
||||||
console.error(action.error)
|
console.error(action.error)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Update Instance Configuration
|
||||||
|
.addCase(updateConfiguration.fulfilled, (state, action) => {
|
||||||
|
const activeIndex = findInstanceActive(state.instances)
|
||||||
|
state.instances[activeIndex].max_toot_chars =
|
||||||
|
action.payload.max_toot_chars
|
||||||
|
state.instances[activeIndex].configuration =
|
||||||
|
action.payload.configuration
|
||||||
|
})
|
||||||
|
.addCase(updateConfiguration.rejected, (_, action) => {
|
||||||
|
console.error(action.error)
|
||||||
|
})
|
||||||
|
|
||||||
// Update Instance Push Global
|
// Update Instance Push Global
|
||||||
.addCase(updateInstancePush.fulfilled, (state, action) => {
|
.addCase(updateInstancePush.fulfilled, (state, action) => {
|
||||||
const activeIndex = findInstanceActive(state.instances)
|
const activeIndex = findInstanceActive(state.instances)
|
||||||
|
@ -314,56 +328,62 @@ export const getInstanceActive = ({ instances: { instances } }: RootState) =>
|
||||||
export const getInstances = ({ instances: { instances } }: RootState) =>
|
export const getInstances = ({ instances: { instances } }: RootState) =>
|
||||||
instances
|
instances
|
||||||
|
|
||||||
export const getInstance = ({ instances: { instances } }: RootState) => {
|
export const getInstance = ({ instances: { instances } }: RootState) =>
|
||||||
const instanceActive = findInstanceActive(instances)
|
instances[findInstanceActive(instances)]
|
||||||
return instanceActive !== -1 ? instances[instanceActive] : null
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getInstanceUrl = ({ instances: { instances } }: RootState) => {
|
export const getInstanceUrl = ({ instances: { instances } }: RootState) =>
|
||||||
const instanceActive = findInstanceActive(instances)
|
instances[findInstanceActive(instances)]?.url
|
||||||
return instanceActive !== -1 ? instances[instanceActive].url : null
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getInstanceUri = ({ instances: { instances } }: RootState) => {
|
export const getInstanceUri = ({ instances: { instances } }: RootState) =>
|
||||||
const instanceActive = findInstanceActive(instances)
|
instances[findInstanceActive(instances)]?.uri
|
||||||
return instanceActive !== -1 ? instances[instanceActive].uri : null
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getInstanceUrls = ({ instances: { instances } }: RootState) => {
|
export const getInstanceUrls = ({ instances: { instances } }: RootState) =>
|
||||||
const instanceActive = findInstanceActive(instances)
|
instances[findInstanceActive(instances)]?.urls
|
||||||
return instanceActive !== -1 ? instances[instanceActive].urls : null
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getInstanceMaxTootChar = ({
|
/* Get Instance Configuration */
|
||||||
|
export const getInstanceConfigurationStatusMaxChars = ({
|
||||||
instances: { instances }
|
instances: { instances }
|
||||||
}: RootState) => {
|
}: RootState) =>
|
||||||
const instanceActive = findInstanceActive(instances)
|
instances[findInstanceActive(instances)]?.configuration?.statuses
|
||||||
return instanceActive !== -1 ? instances[instanceActive].max_toot_chars : 500
|
.max_characters ||
|
||||||
}
|
instances[findInstanceActive(instances)]?.max_toot_chars ||
|
||||||
|
500
|
||||||
|
|
||||||
export const getInstanceAccount = ({ instances: { instances } }: RootState) => {
|
export const getInstanceConfigurationStatusMaxAttachments = ({
|
||||||
const instanceActive = findInstanceActive(instances)
|
instances: { instances }
|
||||||
return instanceActive !== -1 ? instances[instanceActive].account : null
|
}: RootState) =>
|
||||||
}
|
instances[findInstanceActive(instances)]?.configuration?.statuses
|
||||||
|
.max_media_attachments || 4
|
||||||
|
|
||||||
|
export const getInstanceConfigurationStatusCharsURL = ({
|
||||||
|
instances: { instances }
|
||||||
|
}: RootState) =>
|
||||||
|
instances[findInstanceActive(instances)]?.configuration?.statuses
|
||||||
|
.characters_reserved_per_url || 23
|
||||||
|
|
||||||
|
export const getInstanceConfigurationPoll = ({
|
||||||
|
instances: { instances }
|
||||||
|
}: RootState) =>
|
||||||
|
instances[findInstanceActive(instances)]?.configuration?.polls || {
|
||||||
|
max_options: 4,
|
||||||
|
max_characters_per_option: 50,
|
||||||
|
min_expiration: 300,
|
||||||
|
max_expiration: 2629746
|
||||||
|
}
|
||||||
|
/* END */
|
||||||
|
|
||||||
|
export const getInstanceAccount = ({ instances: { instances } }: RootState) =>
|
||||||
|
instances[findInstanceActive(instances)]?.account
|
||||||
|
|
||||||
export const getInstanceNotificationsFilter = ({
|
export const getInstanceNotificationsFilter = ({
|
||||||
instances: { instances }
|
instances: { instances }
|
||||||
}: RootState) => {
|
}: RootState) => instances[findInstanceActive(instances)].notifications_filter
|
||||||
const instanceActive = findInstanceActive(instances)
|
|
||||||
return instanceActive !== -1
|
|
||||||
? instances[instanceActive].notifications_filter
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getInstancePush = ({ instances: { instances } }: RootState) => {
|
export const getInstancePush = ({ instances: { instances } }: RootState) =>
|
||||||
const instanceActive = findInstanceActive(instances)
|
instances[findInstanceActive(instances)]?.push
|
||||||
return instanceActive !== -1 ? instances[instanceActive].push : null
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getInstanceDrafts = ({ instances: { instances } }: RootState) => {
|
export const getInstanceDrafts = ({ instances: { instances } }: RootState) =>
|
||||||
const instanceActive = findInstanceActive(instances)
|
instances[findInstanceActive(instances)]?.drafts
|
||||||
return instanceActive !== -1 ? instances[instanceActive].drafts : null
|
|
||||||
}
|
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
updateInstanceActive,
|
updateInstanceActive,
|
||||||
|
|
Loading…
Reference in New Issue