1
0
mirror of https://github.com/tooot-app/app synced 2025-06-05 22:19:13 +02:00

Removed autolinker

This commit is contained in:
xmflsct
2022-09-20 22:23:01 +02:00
parent fb3cfa0db1
commit 7ec7f85893
70 changed files with 253 additions and 7171 deletions

View File

@ -5,22 +5,12 @@ import CustomText from '@components/Text'
import HeaderSharedCreated from '@components/Timeline/Shared/HeaderShared/Created'
import { useNavigation } from '@react-navigation/native'
import { useAppDispatch } from '@root/store'
import {
getInstanceDrafts,
removeInstanceDraft
} from '@utils/slices/instancesSlice'
import { getInstanceDrafts, removeInstanceDraft } 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,
Image,
Modal,
Platform,
Pressable,
View
} from 'react-native'
import { Dimensions, Image, Modal, Platform, Pressable, View } from 'react-native'
import { PanGestureHandler } from 'react-native-gesture-handler'
import { SwipeListView } from 'react-native-swipe-list-view'
import { useSelector } from 'react-redux'
@ -42,8 +32,7 @@ const ComposeDraftsListRoot: React.FC<Props> = ({ timestamp }) => {
draft => draft.timestamp !== timestamp
)
const actionWidth =
StyleConstants.Font.Size.L + StyleConstants.Spacing.Global.PagePadding * 4
const actionWidth = StyleConstants.Font.Size.L + StyleConstants.Spacing.Global.PagePadding * 4
const [checkingAttachments, setCheckingAttachments] = useState(false)
@ -81,17 +70,9 @@ const ComposeDraftsListRoot: React.FC<Props> = ({ timestamp }) => {
}
tempDraft.spoiler?.length &&
formatText({
textInput: 'text',
composeDispatch,
content: tempDraft.spoiler
})
formatText({ textInput: 'text', composeDispatch, content: tempDraft.spoiler })
tempDraft.text?.length &&
formatText({
textInput: 'text',
composeDispatch,
content: tempDraft.text
})
formatText({ textInput: 'text', composeDispatch, content: tempDraft.text })
composeDispatch({
type: 'loadDraft',
payload: tempDraft
@ -110,9 +91,7 @@ const ComposeDraftsListRoot: React.FC<Props> = ({ timestamp }) => {
color: colors.primaryDefault
}}
>
{item.text ||
item.spoiler ||
t('content.draftsList.content.textEmpty')}
{item.text || item.spoiler || t('content.draftsList.content.textEmpty')}
</CustomText>
{item.attachments?.uploads.length ? (
<View
@ -139,9 +118,7 @@ const ComposeDraftsListRoot: React.FC<Props> = ({ timestamp }) => {
marginLeft: index !== 0 ? StyleConstants.Spacing.S : 0
}}
source={{
uri:
attachment.local?.thumbnail ||
attachment.remote?.preview_url
uri: attachment.local?.thumbnail || attachment.remote?.preview_url
}}
/>
))}
@ -173,10 +150,7 @@ const ComposeDraftsListRoot: React.FC<Props> = ({ timestamp }) => {
size={StyleConstants.Font.Size.M}
style={{ marginRight: StyleConstants.Spacing.S }}
/>
<CustomText
fontStyle='S'
style={{ flexShrink: 1, color: colors.secondary }}
>
<CustomText fontStyle='S' style={{ flexShrink: 1, color: colors.secondary }}>
{t('content.draftsList.warning')}
</CustomText>
</View>
@ -196,8 +170,7 @@ const ComposeDraftsListRoot: React.FC<Props> = ({ timestamp }) => {
<Pressable
style={{
flexBasis:
StyleConstants.Font.Size.L +
StyleConstants.Spacing.Global.PagePadding * 4,
StyleConstants.Font.Size.L + StyleConstants.Spacing.Global.PagePadding * 4,
justifyContent: 'center',
alignItems: 'center'
}}

View File

@ -44,19 +44,28 @@ const ComposeRoot = React.memo(
const { composeState, composeDispatch } = useContext(ComposeContext)
const mapSchemaToType = () => {
if (composeState.tag) {
switch (composeState.tag?.schema) {
case '@':
return 'accounts'
case '#':
return 'hashtags'
}
} else {
return undefined
}
}
const { isFetching, data, refetch } = useSearchQuery({
type:
composeState.tag?.type === 'accounts' || composeState.tag?.type === 'hashtags'
? composeState.tag.type
: undefined,
term: composeState.tag?.text.substring(1),
type: mapSchemaToType(),
term: composeState.tag?.raw.substring(1),
options: { enabled: false }
})
useEffect(() => {
if (
(composeState.tag?.type === 'accounts' || composeState.tag?.type === 'hashtags') &&
composeState.tag?.text
(composeState.tag?.schema === '@' || composeState.tag?.schema === '#') &&
composeState.tag?.raw
) {
refetch()
}
@ -104,20 +113,14 @@ const ComposeRoot = React.memo(
return (
<View style={{ flex: 1 }}>
<FlatList
renderItem={({ item }) => (
<ComposeRootSuggestion
item={item}
composeState={composeState}
composeDispatch={composeDispatch}
/>
)}
renderItem={({ item }) => <ComposeRootSuggestion item={item} />}
ListEmptyComponent={listEmpty}
keyboardShouldPersistTaps='always'
ListHeaderComponent={ComposeRootHeader}
ListFooterComponent={Footer}
ItemSeparatorComponent={ComponentSeparator}
// @ts-ignore
data={data ? data[composeState.tag?.type] : undefined}
data={data ? data[mapSchemaToType()] : undefined}
keyExtractor={() => Math.random().toString()}
/>
<ComposeActions />

View File

@ -1,4 +1,5 @@
import analytics from '@components/analytics'
import { emojis } from '@components/Emojis'
import EmojisContext from '@components/Emojis/helpers/EmojisContext'
import Icon from '@components/Icon'
import { useActionSheet } from '@expo/react-native-action-sheet'
@ -149,14 +150,14 @@ const ComposeActions: React.FC = () => {
const { emojisState, emojisDispatch } = useContext(EmojisContext)
const emojiColor = useMemo(() => {
if (!emojisState.emojis.length) return colors.disabled
if (!emojis.current?.length) return colors.disabled
if (emojisState.targetIndex !== -1) {
return colors.primaryDefault
} else {
return colors.secondary
}
}, [emojisState.emojis.length, emojisState.targetIndex])
}, [emojis.current?.length, emojisState.targetIndex])
const emojiOnPress = () => {
analytics('compose_actions_emojis_press', {
current: emojisState.targetIndex !== -1
@ -243,7 +244,7 @@ const ComposeActions: React.FC = () => {
accessibilityLabel={t('content.root.actions.emoji.accessibilityLabel')}
accessibilityHint={t('content.root.actions.emoji.accessibilityHint')}
accessibilityState={{
disabled: emojisState.emojis.length ? false : true,
disabled: emojis.current?.length ? false : true,
expanded: emojisState.targetIndex !== -1
}}
style={styles.button}

View File

@ -2,50 +2,56 @@ import ComponentAccount from '@components/Account'
import analytics from '@components/analytics'
import haptics from '@components/haptics'
import ComponentHashtag from '@components/Hashtag'
import React, { Dispatch, useCallback } from 'react'
import updateText from '../updateText'
import { ComposeAction, ComposeState } from '../utils/types'
import React, { useContext, useEffect } from 'react'
import formatText from '../formatText'
import ComposeContext from '../utils/createContext'
const ComposeRootSuggestion = React.memo(
({
item,
composeState,
composeDispatch
}: {
item: Mastodon.Account & Mastodon.Tag
composeState: ComposeState
composeDispatch: Dispatch<ComposeAction>
}) => {
const onPress = useCallback(() => {
analytics('compose_suggestion_press', {
type: item.acct ? 'account' : 'hashtag'
})
const focusedInput = composeState.textInputFocus.current
updateText({
composeState: {
...composeState,
[focusedInput]: {
...composeState[focusedInput],
selection: {
start: composeState.tag!.offset,
end: composeState.tag!.offset + composeState.tag!.text.length + 1
}
}
},
composeDispatch,
newText: item.acct ? `@${item.acct}` : `#${item.name}`,
type: 'suggestion'
})
haptics('Light')
}, [])
type Props = { item: Mastodon.Account & Mastodon.Tag }
return item.acct ? (
<ComponentAccount account={item} onPress={onPress} origin='suggestion' />
) : (
<ComponentHashtag hashtag={item} onPress={onPress} origin='suggestion' />
)
},
() => true
)
const ComposeRootSuggestion: React.FC<Props> = ({ item }) => {
const { composeState, composeDispatch } = useContext(ComposeContext)
useEffect(() => {
if (composeState.text.raw.length === 0) {
composeDispatch({ type: 'tag', payload: undefined })
}
}, [composeState.text.raw.length])
const onPress = () => {
analytics('compose_suggestion_press', {
type: item.acct ? 'account' : 'hashtag'
})
const focusedInput = composeState.textInputFocus.current
const updatedText = (): string => {
const main = item.acct ? `@${item.acct}` : `#${item.name}`
const textInput = composeState.textInputFocus.current
if (composeState.tag) {
const contentFront = composeState[textInput].raw.slice(0, composeState.tag.index)
const contentRear = composeState[textInput].raw.slice(composeState.tag.lastIndex)
const spaceFront =
composeState[textInput].raw.length === 0 || /\s/g.test(contentFront.slice(-1)) ? '' : ' '
const spaceRear = /\s/g.test(contentRear[0]) ? '' : ' '
return [contentFront, spaceFront, main, spaceRear, contentRear].join('')
} else {
return composeState[textInput].raw
}
}
formatText({
textInput: focusedInput,
composeDispatch,
content: updatedText(),
disableDebounce: true
})
haptics('Light')
}
return item.acct ? (
<ComponentAccount account={item} onPress={onPress} origin='suggestion' />
) : (
<ComponentHashtag hashtag={item} onPress={onPress} origin='suggestion' />
)
}
export default ComposeRootSuggestion

View File

@ -1,11 +1,12 @@
import LinkifyIt from 'linkify-it'
import { debounce, differenceWith, isEqual } from 'lodash'
import React, { createElement, Dispatch } from 'react'
import React, { Dispatch } from 'react'
import { FetchOptions } from 'react-query/types/core/query'
import Autolinker from '@root/modules/autolinker'
import { useTheme } from '@utils/styles/ThemeManager'
import { ComposeAction, ComposeState } from './utils/types'
import { instanceConfigurationStatusCharsURL } from './Root'
import CustomText from '@components/Text'
import { emojis } from '@components/Emojis'
export interface Params {
textInput: ComposeState['textInputFocus']['current']
@ -21,57 +22,76 @@ const TagText = ({ text }: { text: string }) => {
return <CustomText style={{ color: colors.blue }}>{text}</CustomText>
}
const linkify = new LinkifyIt()
linkify
.set({ fuzzyLink: false, fuzzyEmail: false })
.add('@', {
validate: function (text, pos, self) {
var tail = text.slice(pos)
if (!self.re.mention) {
self.re.mention = new RegExp('^\\S+')
}
if (self.re.mention.test(tail)) {
return tail.match(self.re.mention)![0].length
}
return 0
}
})
.add('#', {
validate: function (text, pos, self) {
var tail = text.slice(pos)
if (!self.re.hashtag) {
self.re.hashtag = new RegExp('^[A-Za-z0-9_]+')
}
if (self.re.hashtag.test(tail)) {
return tail.match(self.re.hashtag)![0].length
}
return 0
}
})
.add(':', {
validate: function (text, pos, self) {
var tail = text.slice(pos)
if (!self.re.emoji) {
self.re.emoji = new RegExp('^(?:([^:]+):)')
}
if (self.re.emoji.test(tail)) {
return tail.match(self.re.emoji)![0].length
}
return 0
}
})
const debouncedSuggestions = debounce(
(composeDispatch, tag) => {
composeDispatch({ type: 'tag', payload: tag })
},
500,
{
trailing: true
}
{ trailing: true }
)
let prevTags: ComposeState['tag'][] = []
const formatText = ({
textInput,
composeDispatch,
content,
disableDebounce = false
}: Params) => {
const tags: ComposeState['tag'][] = []
Autolinker.link(content, {
email: false,
phone: false,
mention: 'mastodon',
hashtag: 'twitter',
replaceFn: props => {
const type = props.getType()
let newType: 'url' | 'accounts' | 'hashtags'
switch (type) {
case 'mention':
newType = 'accounts'
break
case 'hashtag':
newType = 'hashtags'
break
default:
newType = 'url'
break
const formatText = ({ textInput, composeDispatch, content, disableDebounce = false }: Params) => {
const tags = linkify.match(content)
if (!tags) {
composeDispatch({
type: textInput,
payload: {
count: content.length,
raw: content,
formatted: <CustomText children={content} />
}
tags.push({
type: newType,
text: props.getMatchedText(),
offset: props.getOffset(),
length: props.getMatchedText().length
})
return
}
})
})
return
}
const changedTag = differenceWith(tags, prevTags, isEqual)
const changedTag: LinkifyIt.Match[] = differenceWith(tags, prevTags, isEqual)
if (changedTag.length > 0 && !disableDebounce) {
if (changedTag[0]?.type !== 'url') {
if (changedTag[0]?.schema === '@' || changedTag[0]?.schema === '#') {
debouncedSuggestions(composeDispatch, changedTag[0])
}
} else {
@ -84,33 +104,49 @@ const formatText = ({
let contentLength: number = 0
const children = []
tags.forEach((tag, index) => {
if (tag) {
const prev = _content.substr(0, tag.offset - pointer)
const main = _content.substr(tag.offset - pointer, tag.length)
const next = _content.substr(tag.offset - pointer + tag.length)
children.push(prev)
contentLength = contentLength + prev.length
children.push(<TagText key={index} text={main} />)
switch (tag.type) {
case 'url':
contentLength = contentLength + instanceConfigurationStatusCharsURL
break
case 'accounts':
const theMatch = main.match(/@/g)
if (theMatch && theMatch.length > 1) {
contentLength =
contentLength + main.split(new RegExp('(@.*?)@'))[1].length
} else {
contentLength = contentLength + main.length
}
break
case 'hashtags':
contentLength = contentLength + main.length
break
const prev = _content.substring(0, tag.index - pointer)
const main = _content.substring(tag.index - pointer, tag.lastIndex - pointer)
const next = _content.substring(tag.lastIndex - pointer)
children.push(prev)
contentLength = contentLength + prev.length
if (tag.schema === ':') {
if (emojis.current?.length) {
const matchedEmoji = emojis.current.filter(
emojisSection =>
emojisSection.data.filter(
emojisGroup => emojisGroup.filter(emoji => `:${emoji.shortcode}:` === main).length
).length
).length
if (matchedEmoji) {
children.push(<TagText key={index} text={main} />)
} else {
children.push(main)
}
}
_content = next
pointer = pointer + prev.length + tag.length
} else {
children.push(<TagText key={index} text={main} />)
}
switch (tag.schema) {
case '@':
const theMatch = main.match(/@/g)
if (theMatch && theMatch.length > 1) {
contentLength = contentLength + main.split(new RegExp('(@.*?)@'))[1].length
} else {
contentLength = contentLength + main.length
}
break
case '#':
case ':':
contentLength = contentLength + main.length
break
default:
contentLength = contentLength + instanceConfigurationStatusCharsURL
break
}
_content = next
pointer = pointer + prev.length + tag.lastIndex - tag.index
})
children.push(_content)
contentLength = contentLength + _content.length
@ -120,7 +156,7 @@ const formatText = ({
payload: {
count: contentLength,
raw: content,
formatted: createElement(CustomText, null, children)
formatted: <CustomText children={children} />
}
})
}

View File

@ -1,48 +0,0 @@
import { Dispatch } from 'react'
import formatText from './formatText'
import { ComposeAction, ComposeState } from './utils/types'
const updateText = ({
composeState,
composeDispatch,
newText,
type
}: {
composeState: ComposeState
composeDispatch: Dispatch<ComposeAction>
newText: string
type: 'emoji' | 'suggestion'
}) => {
const textInput = composeState.textInputFocus.current
if (composeState[textInput].raw.length) {
const contentFront = composeState[textInput].raw.slice(
0,
composeState[textInput].selection.start
)
const contentRear = composeState[textInput].raw.slice(
composeState[textInput].selection.end
)
const whiteSpaceFront = /\s/g.test(contentFront.slice(-1))
const whiteSpaceRear = /\s/g.test(contentRear.slice(-1))
const newTextWithSpace = `${whiteSpaceFront || type === 'suggestion' ? '' : ' '
}${newText}${whiteSpaceRear ? '' : ' '}`
formatText({
textInput,
composeDispatch,
content: [contentFront, newTextWithSpace, contentRear].join(''),
disableDebounce: true
})
} else {
formatText({
textInput,
composeDispatch,
content: `${newText} `,
disableDebounce: true
})
}
}
export default updateText

View File

@ -35,10 +35,14 @@ export type ComposeState = {
selection: { start: number; end?: number }
}
tag?: {
type: 'url' | 'accounts' | 'hashtags'
text: string
offset: number
length: number
schema: '@' | '#' | ':' | string
index: number
lastIndex: number
raw: string
// type: 'url' | 'accounts' | 'hashtags'
// text: string
// offset: number
// length: number
}
poll: {
active: boolean