mirror of
				https://github.com/tooot-app/app
				synced 2025-06-05 22:19:13 +02:00 
			
		
		
		
	Merge branch 'main' into l10n_main
This commit is contained in:
		@@ -4,7 +4,7 @@
 | 
			
		||||
    "native": "220206",
 | 
			
		||||
    "major": 3,
 | 
			
		||||
    "minor": 4,
 | 
			
		||||
    "patch": 4,
 | 
			
		||||
    "patch": 5,
 | 
			
		||||
    "expo": "44.0.0"
 | 
			
		||||
  },
 | 
			
		||||
  "description": "tooot app for Mastodon",
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@ import pushUseConnect from '@utils/push/useConnect'
 | 
			
		||||
import pushUseReceive from '@utils/push/useReceive'
 | 
			
		||||
import pushUseRespond from '@utils/push/useRespond'
 | 
			
		||||
import { updatePreviousTab } from '@utils/slices/contextsSlice'
 | 
			
		||||
import { checkEmojis } from '@utils/slices/instances/checkEmojis'
 | 
			
		||||
import { updateAccountPreferences } from '@utils/slices/instances/updateAccountPreferences'
 | 
			
		||||
import { updateConfiguration } from '@utils/slices/instances/updateConfiguration'
 | 
			
		||||
import { updateFilters } from '@utils/slices/instances/updateFilters'
 | 
			
		||||
@@ -92,6 +93,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
 | 
			
		||||
      dispatch(updateConfiguration())
 | 
			
		||||
      dispatch(updateFilters())
 | 
			
		||||
      dispatch(updateAccountPreferences())
 | 
			
		||||
      dispatch(checkEmojis())
 | 
			
		||||
    }
 | 
			
		||||
  }, [instanceActive])
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ import EmojisButton from '@components/Emojis/Button'
 | 
			
		||||
import EmojisList from '@components/Emojis/List'
 | 
			
		||||
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
 | 
			
		||||
import { useEmojisQuery } from '@utils/queryHooks/emojis'
 | 
			
		||||
import { getInstanceFrequentEmojis } from '@utils/slices/instancesSlice'
 | 
			
		||||
import { chunk, forEach, groupBy, sortBy } from 'lodash'
 | 
			
		||||
import React, {
 | 
			
		||||
  Dispatch,
 | 
			
		||||
@@ -11,11 +12,16 @@ import React, {
 | 
			
		||||
  useEffect,
 | 
			
		||||
  useReducer
 | 
			
		||||
} from 'react'
 | 
			
		||||
import { useTranslation } from 'react-i18next'
 | 
			
		||||
import FastImage from 'react-native-fast-image'
 | 
			
		||||
import { useSelector } from 'react-redux'
 | 
			
		||||
import EmojisContext, { emojisReducer } from './Emojis/helpers/EmojisContext'
 | 
			
		||||
 | 
			
		||||
const prefetchEmojis = (
 | 
			
		||||
  sortedEmojis: { title: string; data: Mastodon.Emoji[][] }[],
 | 
			
		||||
  sortedEmojis: {
 | 
			
		||||
    title: string
 | 
			
		||||
    data: Pick<Mastodon.Emoji, 'shortcode' | 'url' | 'static_url'>[][]
 | 
			
		||||
  }[],
 | 
			
		||||
  reduceMotionEnabled: boolean
 | 
			
		||||
) => {
 | 
			
		||||
  const prefetches: { uri: string }[] = []
 | 
			
		||||
@@ -101,14 +107,28 @@ const ComponentEmojis: React.FC<Props> = ({
 | 
			
		||||
    [value, selectionRange.current?.start, selectionRange.current?.end]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const { t } = useTranslation()
 | 
			
		||||
  const { data } = useEmojisQuery({ options: { enabled } })
 | 
			
		||||
  const frequentEmojis = useSelector(getInstanceFrequentEmojis, () => true)
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (data && data.length) {
 | 
			
		||||
      let sortedEmojis: { title: string; data: Mastodon.Emoji[][] }[] = []
 | 
			
		||||
      let sortedEmojis: {
 | 
			
		||||
        title: string
 | 
			
		||||
        data: Pick<Mastodon.Emoji, 'shortcode' | 'url' | 'static_url'>[][]
 | 
			
		||||
      }[] = []
 | 
			
		||||
      forEach(
 | 
			
		||||
        groupBy(sortBy(data, ['category', 'shortcode']), 'category'),
 | 
			
		||||
        (value, key) => sortedEmojis.push({ title: key, data: chunk(value, 5) })
 | 
			
		||||
      )
 | 
			
		||||
      if (frequentEmojis.length) {
 | 
			
		||||
        sortedEmojis.unshift({
 | 
			
		||||
          title: t('componentEmojis:frequentUsed'),
 | 
			
		||||
          data: chunk(
 | 
			
		||||
            frequentEmojis.map(e => e.emoji),
 | 
			
		||||
            5
 | 
			
		||||
          )
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
      emojisDispatch({
 | 
			
		||||
        type: 'load',
 | 
			
		||||
        payload: sortedEmojis
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
 | 
			
		||||
import { countInstanceEmoji } from '@utils/slices/instancesSlice'
 | 
			
		||||
import { StyleConstants } from '@utils/styles/constants'
 | 
			
		||||
import layoutAnimation from '@utils/styles/layoutAnimation'
 | 
			
		||||
import { useTheme } from '@utils/styles/ThemeManager'
 | 
			
		||||
@@ -14,11 +15,13 @@ import {
 | 
			
		||||
  View
 | 
			
		||||
} from 'react-native'
 | 
			
		||||
import FastImage from 'react-native-fast-image'
 | 
			
		||||
import { useDispatch } from 'react-redux'
 | 
			
		||||
import validUrl from 'valid-url'
 | 
			
		||||
import EmojisContext from './helpers/EmojisContext'
 | 
			
		||||
 | 
			
		||||
const EmojisList = React.memo(
 | 
			
		||||
  () => {
 | 
			
		||||
    const dispatch = useDispatch()
 | 
			
		||||
    const { reduceMotionEnabled } = useAccessibility()
 | 
			
		||||
    const { t } = useTranslation()
 | 
			
		||||
 | 
			
		||||
@@ -42,12 +45,13 @@ const EmojisList = React.memo(
 | 
			
		||||
                return (
 | 
			
		||||
                  <Pressable
 | 
			
		||||
                    key={emoji.shortcode}
 | 
			
		||||
                    onPress={() =>
 | 
			
		||||
                    onPress={() => {
 | 
			
		||||
                      emojisDispatch({
 | 
			
		||||
                        type: 'shortcode',
 | 
			
		||||
                        payload: `:${emoji.shortcode}:`
 | 
			
		||||
                      })
 | 
			
		||||
                    }
 | 
			
		||||
                      dispatch(countInstanceEmoji(emoji))
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    <FastImage
 | 
			
		||||
                      accessibilityLabel={t(
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,10 @@ import { createContext, Dispatch } from 'react'
 | 
			
		||||
export type EmojisState = {
 | 
			
		||||
  enabled: boolean
 | 
			
		||||
  active: boolean
 | 
			
		||||
  emojis: { title: string; data: Mastodon.Emoji[][] }[]
 | 
			
		||||
  emojis: {
 | 
			
		||||
    title: string
 | 
			
		||||
    data: Pick<Mastodon.Emoji, 'shortcode' | 'url' | 'static_url'>[][]
 | 
			
		||||
  }[]
 | 
			
		||||
  shortcode: Mastodon.Emoji['shortcode'] | null
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ export default {
 | 
			
		||||
  screenImageViewer: require('./screens/imageViewer'),
 | 
			
		||||
  screenTabs: require('./screens/tabs'),
 | 
			
		||||
 | 
			
		||||
  componentEmojis: require('./components/emojis'),
 | 
			
		||||
  componentInstance: require('./components/instance'),
 | 
			
		||||
  componentMediaSelector: require('./components/mediaSelector'),
 | 
			
		||||
  componentParse: require('./components/parse'),
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								src/i18n/en/components/emojis.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/i18n/en/components/emojis.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
{
 | 
			
		||||
  "frequentUsed": "Frequent used"
 | 
			
		||||
}
 | 
			
		||||
@@ -8,6 +8,7 @@ export default {
 | 
			
		||||
  screenImageViewer: require('./screens/imageViewer'),
 | 
			
		||||
  screenTabs: require('./screens/tabs'),
 | 
			
		||||
 | 
			
		||||
  componentEmojis: require('./components/emojis'),
 | 
			
		||||
  componentInstance: require('./components/instance'),
 | 
			
		||||
  componentMediaSelector: require('./components/mediaSelector'),
 | 
			
		||||
  componentParse: require('./components/parse'),
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								src/i18n/ko/components/emojis.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/i18n/ko/components/emojis.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
{}
 | 
			
		||||
@@ -8,6 +8,7 @@ export default {
 | 
			
		||||
  screenImageViewer: require('./screens/imageViewer'),
 | 
			
		||||
  screenTabs: require('./screens/tabs'),
 | 
			
		||||
 | 
			
		||||
  componentEmojis: require('./components/emojis'),
 | 
			
		||||
  componentInstance: require('./components/instance'),
 | 
			
		||||
  componentMediaSelector: require('./components/mediaSelector'),
 | 
			
		||||
  componentParse: require('./components/parse'),
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								src/i18n/vi/components/emojis.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/i18n/vi/components/emojis.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
{}
 | 
			
		||||
@@ -8,6 +8,7 @@ export default {
 | 
			
		||||
  screenImageViewer: require('./screens/imageViewer'),
 | 
			
		||||
  screenTabs: require('./screens/tabs'),
 | 
			
		||||
 | 
			
		||||
  componentEmojis: require('./components/emojis'),
 | 
			
		||||
  componentInstance: require('./components/instance'),
 | 
			
		||||
  componentMediaSelector: require('./components/mediaSelector'),
 | 
			
		||||
  componentParse: require('./components/parse'),
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,3 @@
 | 
			
		||||
{
 | 
			
		||||
  "frequentUsed": "常用"
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,11 @@ import FastImage from 'react-native-fast-image'
 | 
			
		||||
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
 | 
			
		||||
import { ComposeState } from './utils/types'
 | 
			
		||||
import { useSelector } from 'react-redux'
 | 
			
		||||
import { getInstanceConfigurationStatusCharsURL } from '@utils/slices/instancesSlice'
 | 
			
		||||
import {
 | 
			
		||||
  getInstanceConfigurationStatusCharsURL,
 | 
			
		||||
  getInstanceFrequentEmojis
 | 
			
		||||
} from '@utils/slices/instancesSlice'
 | 
			
		||||
import { useTranslation } from 'react-i18next'
 | 
			
		||||
 | 
			
		||||
const prefetchEmojis = (
 | 
			
		||||
  sortedEmojis: NonNullable<ComposeState['emoji']['emojis']>,
 | 
			
		||||
@@ -99,15 +103,29 @@ const ComposeRoot = React.memo(
 | 
			
		||||
      }
 | 
			
		||||
    }, [composeState.tag])
 | 
			
		||||
 | 
			
		||||
    const { t } = useTranslation()
 | 
			
		||||
    const { data: emojisData } = useEmojisQuery({})
 | 
			
		||||
    const frequentEmojis = useSelector(getInstanceFrequentEmojis, () => true)
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
      if (emojisData && emojisData.length) {
 | 
			
		||||
        let sortedEmojis: { title: string; data: Mastodon.Emoji[][] }[] = []
 | 
			
		||||
        const sortedEmojis: {
 | 
			
		||||
          title: string
 | 
			
		||||
          data: Pick<Mastodon.Emoji, 'shortcode' | 'url' | 'static_url'>[][]
 | 
			
		||||
        }[] = []
 | 
			
		||||
        forEach(
 | 
			
		||||
          groupBy(sortBy(emojisData, ['category', 'shortcode']), 'category'),
 | 
			
		||||
          (value, key) =>
 | 
			
		||||
            sortedEmojis.push({ title: key, data: chunk(value, 5) })
 | 
			
		||||
        )
 | 
			
		||||
        if (frequentEmojis.length) {
 | 
			
		||||
          sortedEmojis.unshift({
 | 
			
		||||
            title: t('componentEmojis:frequentUsed'),
 | 
			
		||||
            data: chunk(
 | 
			
		||||
              frequentEmojis.map(e => e.emoji),
 | 
			
		||||
              5
 | 
			
		||||
            )
 | 
			
		||||
          })
 | 
			
		||||
        }
 | 
			
		||||
        composeDispatch({
 | 
			
		||||
          type: 'emoji',
 | 
			
		||||
          payload: { ...composeState.emoji, emojis: sortedEmojis }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import haptics from '@components/haptics'
 | 
			
		||||
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
 | 
			
		||||
import { countInstanceEmoji } from '@utils/slices/instancesSlice'
 | 
			
		||||
import { StyleConstants } from '@utils/styles/constants'
 | 
			
		||||
import { useTheme } from '@utils/styles/ThemeManager'
 | 
			
		||||
import React, { RefObject, useCallback, useContext, useEffect } from 'react'
 | 
			
		||||
@@ -14,6 +15,7 @@ import {
 | 
			
		||||
  View
 | 
			
		||||
} from 'react-native'
 | 
			
		||||
import FastImage from 'react-native-fast-image'
 | 
			
		||||
import { useDispatch } from 'react-redux'
 | 
			
		||||
import validUrl from 'valid-url'
 | 
			
		||||
import updateText from '../../updateText'
 | 
			
		||||
import ComposeContext from '../../utils/createContext'
 | 
			
		||||
@@ -27,6 +29,7 @@ const ComposeEmojis: React.FC<Props> = ({ accessibleRefEmojis }) => {
 | 
			
		||||
  const { reduceMotionEnabled } = useAccessibility()
 | 
			
		||||
  const { colors } = useTheme()
 | 
			
		||||
  const { t } = useTranslation()
 | 
			
		||||
  const dispatch = useDispatch()
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const tagEmojis = findNodeHandle(accessibleRefEmojis.current)
 | 
			
		||||
@@ -53,13 +56,14 @@ const ComposeEmojis: React.FC<Props> = ({ accessibleRefEmojis }) => {
 | 
			
		||||
                <Pressable
 | 
			
		||||
                  key={emoji.shortcode}
 | 
			
		||||
                  onPress={() => {
 | 
			
		||||
                    haptics('Light')
 | 
			
		||||
                    updateText({
 | 
			
		||||
                      composeState,
 | 
			
		||||
                      composeDispatch,
 | 
			
		||||
                      newText: `:${emoji.shortcode}:`,
 | 
			
		||||
                      type: 'emoji'
 | 
			
		||||
                    })
 | 
			
		||||
                    haptics('Light')
 | 
			
		||||
                    dispatch(countInstanceEmoji(emoji))
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <FastImage
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										7
									
								
								src/screens/Compose/utils/types.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								src/screens/Compose/utils/types.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -40,7 +40,12 @@ export type ComposeState = {
 | 
			
		||||
  }
 | 
			
		||||
  emoji: {
 | 
			
		||||
    active: boolean
 | 
			
		||||
    emojis: { title: string; data: Mastodon.Emoji[][] }[] | undefined
 | 
			
		||||
    emojis:
 | 
			
		||||
      | {
 | 
			
		||||
          title: string
 | 
			
		||||
          data: Pick<Mastodon.Emoji, 'shortcode' | 'url' | 'static_url'>[][]
 | 
			
		||||
        }[]
 | 
			
		||||
      | undefined
 | 
			
		||||
  }
 | 
			
		||||
  poll: {
 | 
			
		||||
    active: boolean
 | 
			
		||||
 
 | 
			
		||||
@@ -45,6 +45,13 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
 | 
			
		||||
          scrolled.current = true
 | 
			
		||||
          const pointer = flattenData.findIndex(({ id }) => id === toot.id)
 | 
			
		||||
          if (pointer === -1) return
 | 
			
		||||
          Sentry.Native.setContext('Scroll to Index', {
 | 
			
		||||
            type: 'original',
 | 
			
		||||
            index: pointer,
 | 
			
		||||
            itemsLength: flattenData.length,
 | 
			
		||||
            id: toot.id,
 | 
			
		||||
            flattenData: flattenData.map(({ id }) => id)
 | 
			
		||||
          })
 | 
			
		||||
          try {
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
              flRef.current?.scrollToIndex({
 | 
			
		||||
@@ -54,13 +61,6 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
 | 
			
		||||
            }, 500)
 | 
			
		||||
          } catch (err) {
 | 
			
		||||
            if (Math.random() < 0.1) {
 | 
			
		||||
              Sentry.Native.setContext('Scroll to Index', {
 | 
			
		||||
                type: 'original',
 | 
			
		||||
                index: pointer,
 | 
			
		||||
                itemsLength: flattenData.length,
 | 
			
		||||
                id: toot.id,
 | 
			
		||||
                flattenData: flattenData.map(({ id }) => id)
 | 
			
		||||
              })
 | 
			
		||||
              Sentry.Native.captureException(err)
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
@@ -74,6 +74,12 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
 | 
			
		||||
    error => {
 | 
			
		||||
      const offset = error.averageItemLength * error.index
 | 
			
		||||
      flRef.current?.scrollToOffset({ offset })
 | 
			
		||||
      Sentry.Native.setContext('Scroll to Index', {
 | 
			
		||||
        type: 'onScrollToIndexFailed',
 | 
			
		||||
        index: error.index,
 | 
			
		||||
        itemsLength,
 | 
			
		||||
        id: toot.id
 | 
			
		||||
      })
 | 
			
		||||
      try {
 | 
			
		||||
        error.index < itemsLength &&
 | 
			
		||||
          setTimeout(
 | 
			
		||||
@@ -86,12 +92,6 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
 | 
			
		||||
          )
 | 
			
		||||
      } catch (err) {
 | 
			
		||||
        if (Math.random() < 0.1) {
 | 
			
		||||
          Sentry.Native.setContext('Scroll to Index', {
 | 
			
		||||
            type: 'onScrollToIndexFailed',
 | 
			
		||||
            index: error.index,
 | 
			
		||||
            itemsLength,
 | 
			
		||||
            id: toot.id
 | 
			
		||||
          })
 | 
			
		||||
          Sentry.Native.captureException(err)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,7 @@ const instancesPersistConfig = {
 | 
			
		||||
  key: 'instances',
 | 
			
		||||
  prefix,
 | 
			
		||||
  storage: secureStorage,
 | 
			
		||||
  version: 7,
 | 
			
		||||
  version: 8,
 | 
			
		||||
  // @ts-ignore
 | 
			
		||||
  migrate: createMigrate(instancesMigration)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ import { InstanceV4 } from './v4'
 | 
			
		||||
import { InstanceV5 } from './v5'
 | 
			
		||||
import { InstanceV6 } from './v6'
 | 
			
		||||
import { InstanceV7 } from './v7'
 | 
			
		||||
import { InstanceV8 } from './v8'
 | 
			
		||||
 | 
			
		||||
const instancesMigration = {
 | 
			
		||||
  4: (state: InstanceV3): InstanceV4 => {
 | 
			
		||||
@@ -76,6 +77,16 @@ const instancesMigration = {
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  8: (state: InstanceV7): InstanceV8 => {
 | 
			
		||||
    return {
 | 
			
		||||
      instances: state.instances.map(instance => {
 | 
			
		||||
        return {
 | 
			
		||||
          ...instance,
 | 
			
		||||
          frequentEmojis: []
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										83
									
								
								src/utils/migrations/instances/v8.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								src/utils/migrations/instances/v8.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,83 @@
 | 
			
		||||
import { ComposeStateDraft } from '@screens/Compose/utils/types'
 | 
			
		||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
 | 
			
		||||
 | 
			
		||||
type Instance = {
 | 
			
		||||
  active: boolean
 | 
			
		||||
  appData: {
 | 
			
		||||
    clientId: string
 | 
			
		||||
    clientSecret: string
 | 
			
		||||
  }
 | 
			
		||||
  url: string
 | 
			
		||||
  token: string
 | 
			
		||||
  uri: Mastodon.Instance['uri']
 | 
			
		||||
  urls: Mastodon.Instance['urls']
 | 
			
		||||
  account: {
 | 
			
		||||
    id: Mastodon.Account['id']
 | 
			
		||||
    acct: Mastodon.Account['acct']
 | 
			
		||||
    avatarStatic: Mastodon.Account['avatar_static']
 | 
			
		||||
    preferences: Mastodon.Preferences
 | 
			
		||||
  }
 | 
			
		||||
  max_toot_chars?: number // To be deprecated in v4
 | 
			
		||||
  configuration?: Mastodon.Instance['configuration']
 | 
			
		||||
  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: boolean }
 | 
			
		||||
    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 // legacy
 | 
			
		||||
      private?: string // legacy
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  timelinesLookback?: {
 | 
			
		||||
    [key: string]: {
 | 
			
		||||
      queryKey: QueryKeyTimeline
 | 
			
		||||
      ids: Mastodon.Status['id'][]
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  mePage: {
 | 
			
		||||
    lists: { shown: boolean }
 | 
			
		||||
    announcements: { shown: boolean; unread: number }
 | 
			
		||||
  }
 | 
			
		||||
  drafts: ComposeStateDraft[]
 | 
			
		||||
  frequentEmojis: {
 | 
			
		||||
    emoji: Pick<Mastodon.Emoji, 'shortcode' | 'url' | 'static_url'>
 | 
			
		||||
    score: number
 | 
			
		||||
    count: number
 | 
			
		||||
    lastUsed: Date
 | 
			
		||||
  }[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type InstanceV8 = {
 | 
			
		||||
  instances: Instance[]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										15
									
								
								src/utils/slices/instances/checkEmojis.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/utils/slices/instances/checkEmojis.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
import apiInstance from '@api/instance'
 | 
			
		||||
import queryClient from '@helpers/queryClient'
 | 
			
		||||
import { createAsyncThunk } from '@reduxjs/toolkit'
 | 
			
		||||
 | 
			
		||||
export const checkEmojis = createAsyncThunk(
 | 
			
		||||
  'instances/checkEmojis',
 | 
			
		||||
  async (): Promise<Mastodon.Emoji[]> => {
 | 
			
		||||
    const res = await apiInstance<Mastodon.Emoji[]>({
 | 
			
		||||
      method: 'get',
 | 
			
		||||
      url: 'custom_emojis'
 | 
			
		||||
    }).then(res => res.body)
 | 
			
		||||
    queryClient.setQueryData(['Emojis'], res)
 | 
			
		||||
    return res
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
@@ -4,6 +4,7 @@ import { RootState } from '@root/store'
 | 
			
		||||
import { ComposeStateDraft } from '@screens/Compose/utils/types'
 | 
			
		||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
 | 
			
		||||
import addInstance from './instances/add'
 | 
			
		||||
import { checkEmojis } from './instances/checkEmojis'
 | 
			
		||||
import removeInstance from './instances/remove'
 | 
			
		||||
import { updateAccountPreferences } from './instances/updateAccountPreferences'
 | 
			
		||||
import { updateConfiguration } from './instances/updateConfiguration'
 | 
			
		||||
@@ -81,6 +82,12 @@ export type Instance = {
 | 
			
		||||
    announcements: { shown: boolean; unread: number }
 | 
			
		||||
  }
 | 
			
		||||
  drafts: ComposeStateDraft[]
 | 
			
		||||
  frequentEmojis: {
 | 
			
		||||
    emoji: Pick<Mastodon.Emoji, 'shortcode' | 'url' | 'static_url'>
 | 
			
		||||
    score: number
 | 
			
		||||
    count: number
 | 
			
		||||
    lastUsed: number
 | 
			
		||||
  }[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type InstancesState = {
 | 
			
		||||
@@ -184,6 +191,56 @@ const instancesSlice = createSlice({
 | 
			
		||||
        ...instances[activeIndex].mePage,
 | 
			
		||||
        ...action.payload
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    countInstanceEmoji: (
 | 
			
		||||
      { instances },
 | 
			
		||||
      action: PayloadAction<Instance['frequentEmojis'][0]['emoji']>
 | 
			
		||||
    ) => {
 | 
			
		||||
      const HALF_LIFE = 60 * 60 * 24 * 7 // 1 week
 | 
			
		||||
      const calculateScore = (emoji: Instance['frequentEmojis'][0]): number => {
 | 
			
		||||
        var seconds = (new Date().getTime() - emoji.lastUsed) / 1000
 | 
			
		||||
        var score = emoji.count + 1
 | 
			
		||||
        var order = Math.log(Math.max(score, 1)) / Math.LN10
 | 
			
		||||
        var sign = score > 0 ? 1 : score === 0 ? 0 : -1
 | 
			
		||||
        return (sign * order + seconds / HALF_LIFE) * 10
 | 
			
		||||
      }
 | 
			
		||||
      const activeIndex = findInstanceActive(instances)
 | 
			
		||||
      const foundEmojiIndex = instances[activeIndex].frequentEmojis?.findIndex(
 | 
			
		||||
        e =>
 | 
			
		||||
          e.emoji.shortcode === action.payload.shortcode &&
 | 
			
		||||
          e.emoji.url === action.payload.url
 | 
			
		||||
      )
 | 
			
		||||
      let newEmojisSort: Instance['frequentEmojis']
 | 
			
		||||
      if (foundEmojiIndex > -1) {
 | 
			
		||||
        newEmojisSort = instances[activeIndex].frequentEmojis
 | 
			
		||||
          .map((e, i) =>
 | 
			
		||||
            i === foundEmojiIndex
 | 
			
		||||
              ? {
 | 
			
		||||
                  ...e,
 | 
			
		||||
                  score: calculateScore(e),
 | 
			
		||||
                  count: e.count + 1,
 | 
			
		||||
                  lastUsed: new Date().getTime()
 | 
			
		||||
                }
 | 
			
		||||
              : e
 | 
			
		||||
          )
 | 
			
		||||
          .sort((a, b) => b.score - a.score)
 | 
			
		||||
      } else {
 | 
			
		||||
        newEmojisSort = instances[activeIndex].frequentEmojis || []
 | 
			
		||||
        const temp = {
 | 
			
		||||
          emoji: action.payload,
 | 
			
		||||
          score: 0,
 | 
			
		||||
          count: 0,
 | 
			
		||||
          lastUsed: new Date().getTime()
 | 
			
		||||
        }
 | 
			
		||||
        newEmojisSort.push({
 | 
			
		||||
          ...temp,
 | 
			
		||||
          score: calculateScore(temp),
 | 
			
		||||
          count: temp.count + 1
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
      instances[activeIndex].frequentEmojis = newEmojisSort
 | 
			
		||||
        .sort((a, b) => b.score - a.score)
 | 
			
		||||
        .slice(0, 20)
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  extraReducers: builder => {
 | 
			
		||||
@@ -321,6 +378,22 @@ const instancesSlice = createSlice({
 | 
			
		||||
          action.meta.arg.changed
 | 
			
		||||
        ].loading = true
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      // Check if frequently used emojis still exist
 | 
			
		||||
      .addCase(checkEmojis.fulfilled, (state, action) => {
 | 
			
		||||
        const activeIndex = findInstanceActive(state.instances)
 | 
			
		||||
        state.instances[activeIndex].frequentEmojis = state.instances[
 | 
			
		||||
          activeIndex
 | 
			
		||||
        ].frequentEmojis?.filter(emoji => {
 | 
			
		||||
          return action.payload.find(
 | 
			
		||||
            e =>
 | 
			
		||||
              e.shortcode === emoji.emoji.shortcode && e.url === emoji.emoji.url
 | 
			
		||||
          )
 | 
			
		||||
        })
 | 
			
		||||
      })
 | 
			
		||||
      .addCase(checkEmojis.rejected, (_, action) => {
 | 
			
		||||
        console.error(action.error)
 | 
			
		||||
      })
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
@@ -394,6 +467,10 @@ export const getInstanceMePage = ({ instances: { instances } }: RootState) =>
 | 
			
		||||
export const getInstanceDrafts = ({ instances: { instances } }: RootState) =>
 | 
			
		||||
  instances[findInstanceActive(instances)]?.drafts
 | 
			
		||||
 | 
			
		||||
export const getInstanceFrequentEmojis = ({
 | 
			
		||||
  instances: { instances }
 | 
			
		||||
}: RootState) => instances[findInstanceActive(instances)]?.frequentEmojis
 | 
			
		||||
 | 
			
		||||
export const {
 | 
			
		||||
  updateInstanceActive,
 | 
			
		||||
  updateInstanceAccount,
 | 
			
		||||
@@ -403,7 +480,8 @@ export const {
 | 
			
		||||
  clearPushLoading,
 | 
			
		||||
  disableAllPushes,
 | 
			
		||||
  updateInstanceTimelineLookback,
 | 
			
		||||
  updateInstanceMePage
 | 
			
		||||
  updateInstanceMePage,
 | 
			
		||||
  countInstanceEmoji
 | 
			
		||||
} = instancesSlice.actions
 | 
			
		||||
 | 
			
		||||
export default instancesSlice.reducer
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user