mirror of
https://github.com/tooot-app/app
synced 2025-06-05 22:19:13 +02:00
Removed autolinker
This commit is contained in:
@ -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'
|
||||
}}
|
||||
|
@ -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 />
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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} />
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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
|
12
src/screens/Compose/utils/types.d.ts
vendored
12
src/screens/Compose/utils/types.d.ts
vendored
@ -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
|
||||
|
Reference in New Issue
Block a user