This commit is contained in:
KoalaSat 2023-03-14 20:18:17 +00:00 committed by GitHub
commit d207e307a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 176 additions and 91 deletions

View File

@ -1,6 +1,6 @@
import { t } from 'i18next'
import * as React from 'react'
import { StyleSheet, View, type ListRenderItem, Switch, FlatList } from 'react-native'
import { StyleSheet, View, type ListRenderItem, FlatList } from 'react-native'
import { Button, IconButton, List, Snackbar, Text, useTheme } from 'react-native-paper'
import { AppContext } from '../../Contexts/AppContext'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
@ -11,15 +11,15 @@ import LnPayment from '../LnPayment'
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'
import { navigate } from '../../lib/Navigation'
import RBSheet from 'react-native-raw-bottom-sheet'
import { getUserRelays, type NoteRelay } from '../../Functions/DatabaseFunctions/NotesRelays'
import { relayToColor } from '../../Functions/NativeFunctions'
import { type Relay } from '../../Functions/DatabaseFunctions/Relays'
import ProfileShare from '../ProfileShare'
import { ScrollView } from 'react-native-gesture-handler'
import { Kind } from 'nostr-tools'
import { getUnixTime } from 'date-fns'
import DatabaseModule from '../../lib/Native/DatabaseModule'
import { addMutedUsersList, removeMutedUsersList } from '../../Functions/RelayFunctions/Lists'
import { getRelayMetadata } from '../../Functions/DatabaseFunctions/RelayMetadatas'
import { getUserRelays } from '../../Functions/DatabaseFunctions/NotesRelays'
interface ProfileActionsProps {
user: User
@ -35,7 +35,8 @@ export const ProfileActions: React.FC<ProfileActionsProps> = ({
const theme = useTheme()
const { database } = React.useContext(AppContext)
const { publicKey, privateKey, mutedUsers, reloadLists } = React.useContext(UserContext)
const { relayPool, updateRelayItem, lastEventId, sendEvent } = React.useContext(RelayPoolContext)
const { relayPool, addRelayItem, lastEventId, sendEvent, relays } =
React.useContext(RelayPoolContext)
const [isContact, setIsContact] = React.useState<boolean>()
const [isMuted, setIsMuted] = React.useState<boolean>()
const [isGroupHidden, setIsGroupHidden] = React.useState<boolean>()
@ -44,13 +45,13 @@ export const ProfileActions: React.FC<ProfileActionsProps> = ({
const bottomSheetRelaysRef = React.useRef<RBSheet>(null)
const bottomSheetShareRef = React.useRef<RBSheet>(null)
const bottomSheetMuteRef = React.useRef<RBSheet>(null)
const [userRelays, setUserRelays] = React.useState<NoteRelay[]>([])
const [userRelays, setUserRelays] = React.useState<string[]>()
const [openLn, setOpenLn] = React.useState<boolean>(false)
React.useEffect(() => {
loadUser()
loadRelays()
if (publicKey) {
if (publicKey && user.id) {
relayPool?.subscribe('lists-muted-users', [
{
kinds: [10000],
@ -58,12 +59,20 @@ export const ProfileActions: React.FC<ProfileActionsProps> = ({
limit: 1,
},
])
relayPool?.subscribe(`card-user-${user.id.substring(0, 6)}`, [
{
kinds: [10002],
authors: [user.id],
limit: 1,
},
])
}
}, [])
React.useEffect(() => {
reloadLists()
loadUser()
loadRelays()
}, [lastEventId, isMuted])
const hideGroupsUser: () => void = () => {
@ -87,9 +96,13 @@ export const ProfileActions: React.FC<ProfileActionsProps> = ({
const loadRelays: () => void = () => {
if (database) {
getUserRelays(database, user.id).then((results) => {
if (results) {
setUserRelays(results)
getRelayMetadata(database, user.id).then((resultMeta) => {
if (resultMeta) {
setUserRelays(resultMeta.tags.map((relayMeta) => relayMeta[1]))
} else {
getUserRelays(database, user.id).then((resultRelays) => {
setUserRelays(resultRelays.map((relay) => relay.url))
})
}
})
}
@ -149,21 +162,6 @@ export const ProfileActions: React.FC<ProfileActionsProps> = ({
}
}
const activeRelay: (relay: Relay) => void = (relay) => {
relay.active = 1
updateRelayItem(relay).then(() => {
setShowNotificationRelay('active')
})
}
const desactiveRelay: (relay: Relay) => void = (relay) => {
relay.active = 0
relay.global_feed = 0
updateRelayItem(relay).then(() => {
setShowNotificationRelay('desactive')
})
}
const bottomSheetStyles = React.useMemo(() => {
return {
container: {
@ -178,25 +176,34 @@ export const ProfileActions: React.FC<ProfileActionsProps> = ({
}
}, [])
const renderRelayItem: ListRenderItem<NoteRelay> = ({ index, item }) => {
const onPressAddRelay: (url: string) => void = (url) => {
addRelayItem({ url })
}
const renderRelayItem: ListRenderItem<string> = ({ index, item }) => {
const userRelayUrls = relays.map((relay) => relay.url)
return (
<List.Item
key={index}
title={item.url}
title={item}
left={() => (
<MaterialCommunityIcons
style={styles.relayColor}
name='circle'
color={relayToColor(item.url)}
/>
)}
right={() => (
<Switch
style={styles.switch}
value={item.active !== undefined && item.active > 0}
onValueChange={() => (item.active ? desactiveRelay(item) : activeRelay(item))}
color={relayToColor(item)}
/>
)}
right={() => {
if (userRelayUrls.includes(item)) {
return <></>
} else {
return (
<Button mode='text' onPress={() => onPressAddRelay(item)}>
{t('profileCard.addRelay')}
</Button>
)
}
}}
/>
)
}

View File

@ -50,17 +50,19 @@ export const LinksPreview: React.FC<TextContentProps> = ({ urls, lnUrl }) => {
const getDefaultCover: () => number = () => {
if (!firstLink || !urls[firstLink]) return require(DEFAULT_COVER)
if (urls[firstLink] === 'magnet') return require(MAGNET_COVER)
if (urls[firstLink] === 'blueBird') return require(BLUEBIRD_COVER)
if (urls[firstLink] === 'audio') return require(MEDIA_COVER)
if (urls[firstLink] === 'video') return require(MEDIA_COVER)
if (urls[firstLink] === 'audio') return require(MEDIA_COVER)
if (urls[firstLink] === 'blueBird') return require(BLUEBIRD_COVER)
if (urls[firstLink] === 'tube') return require(MEDIA_COVER)
if (urls[firstLink] === 'magnet') return require(MAGNET_COVER)
return require(DEFAULT_COVER)
}
const videoPreview = (
<VideoPlayer
source={{uri: firstLink}}
source={{ uri: firstLink ?? '' }}
style={styles.videPlayer}
paused={true}
disableBack
disableVolume
disableFullscreen
@ -200,6 +202,6 @@ const styles = StyleSheet.create({
},
videPlayer: {
height: 195,
borderRadius: 16
}
borderRadius: 16,
},
})

View File

@ -9,7 +9,12 @@ import getUnixTime from 'date-fns/getUnixTime'
import { useTheme } from 'react-native-paper'
import { getNip19Key, getNpub } from '../../lib/nostr/Nip19'
import { navigate } from '../../lib/Navigation'
import { validBlueBirdUrl, validImageUrl, validMediaUrl } from '../../Functions/NativeFunctions'
import {
validBlueBirdUrl,
validImageUrl,
validMediaUrl,
validTubeUrl,
} from '../../Functions/NativeFunctions'
import { LinksPreview } from './LinksPreview'
interface TextContentProps {
@ -56,6 +61,12 @@ export const TextContent: React.FC<TextContentProps> = ({
}
}
const handleHashtagPress: (hashtag: string) => void = (hashtag) => {
if (hashtag) {
navigate('Search', { search: hashtag })
}
}
const handleNip05ProfilePress: (nip19: string) => void = (nip19) => {
const pubKey = getNip19Key(nip19)
@ -126,6 +137,8 @@ export const TextContent: React.FC<TextContentProps> = ({
return 'image'
} else if (validBlueBirdUrl(url)) {
return 'blueBird'
} else if (validTubeUrl(url)) {
return 'tube'
} else if (MAGNET_LINK.test(url)) {
return 'magnet'
}
@ -174,7 +187,7 @@ export const TextContent: React.FC<TextContentProps> = ({
pattern: /#\[(\d+)\]/,
style: styles.mention,
},
{ pattern: /#(\w+)/, style: styles.hashTag },
{ pattern: /#(\w+)/, style: styles.hashTag, onPress: handleHashtagPress },
{ pattern: /(lnbc)\S+/, style: styles.nip19, renderText: renderLnurl },
{ pattern: /(nevent1)\S+/, style: styles.nip19, onPress: handleNip05NotePress },
{

View File

@ -48,7 +48,7 @@ export const pickRandomItems = <T extends unknown>(arr: T[], n: number): T[] =>
export const validImageUrl: (url: string | undefined) => boolean = (url) => {
if (url) {
const regexp = /^(https?:\/\/.*\.?(png|jpg|jpeg|gif|webp)(\?.*)?)$/
const regexp = /^(https?:\/\/.*\.?(png|jpg|jpeg|gif|webp){1}(\?.*)?)$/
return regexp.test(url)
} else {
return false
@ -57,9 +57,15 @@ export const validImageUrl: (url: string | undefined) => boolean = (url) => {
export const validMediaUrl: (url: string | undefined) => boolean = (url) => {
if (url) {
const fileRegexp = /^(https?:\/\/.*\.?(mp4|mp3))$/
const serviceRegexp = /^(https?:\/\/?(youtube|youtu.be).*)$/
return fileRegexp.test(url) || serviceRegexp.test(url)
return /^(https?:\/\/.*\.(mp4|mp3){1})$/.test(url)
} else {
return false
}
}
export const validTubeUrl: (url: string | undefined) => boolean = (url) => {
if (url) {
return /^(https?:\/\/.*\.?(youtube|youtu.be){1}.*)$/.test(url)
} else {
return false
}
@ -67,7 +73,7 @@ export const validMediaUrl: (url: string | undefined) => boolean = (url) => {
export const validBlueBirdUrl: (url: string | undefined) => boolean = (url) => {
if (url) {
const serviceRegexp = /^(https?:\/\/?(twitter.com|t.co).*)$/
const serviceRegexp = /^(https?:\/\/.*\.?(twitter.com|t.co){1}.*)$/
return serviceRegexp.test(url)
} else {
return false
@ -76,7 +82,7 @@ export const validBlueBirdUrl: (url: string | undefined) => boolean = (url) => {
export const validNip21: (string: string | undefined) => boolean = (string) => {
if (string) {
const regexp = /^(nostr:)?(npub1|nprofile1|nevent1|nrelay1|note1)\S*$/
const regexp = /^(nostr:)?(npub1|nprofile1|nevent1|nrelay1|note1){1}\S*$/
return regexp.test(string)
} else {
return false

View File

@ -15,7 +15,7 @@
"searchPage": {
"placeholder": "Look for public keys, notes...",
"emptyTitle": "Tip",
"emptyDescription": "Start typing @ to find someone"
"emptyDescription": "Start typing @ to find someone\n\nStart typing # to search topics"
},
"qrReaderPage": {
"emptyTitle": "Berechtigung nicht gewährt",
@ -443,6 +443,7 @@
"userUnblocked": "Profil entblockt",
"userBlocked": "Profile geblockt"
},
"addRelay": "Connect relay",
"invoice": "Tip",
"message": "Nachricht",
"follow": "Folgen",

View File

@ -35,9 +35,9 @@
"markAllRead": "Mark all as read"
},
"searchPage": {
"placeholder": "Look for public keys, notes...",
"placeholder": "Look for public keys, notes, hashtags...",
"emptyTitle": "Tip",
"emptyDescription": "Start typing @ to find someone"
"emptyDescription": "Start typing @ to find someone\n\nStart typing # to search topics"
},
"conversationPage": {
"unableDecypt": "{{username}} is talking with others about you",
@ -450,6 +450,7 @@
"userUnblocked": "Profile unblocked",
"userBlocked": "Profile unblocked"
},
"addRelay": "Connect relay",
"invoice": "Tip",
"message": "Message",
"follow": "Follow",

View File

@ -15,7 +15,7 @@
"searchPage": {
"placeholder": "Busca for claves públicas, notas, ...",
"emptyTitle": "Consejo",
"emptyDescription": "Empieza escribiendo con @ para encontrar a alguien"
"emptyDescription": "Empieza escribiendo con @ para encontrar a alguien.\n\nEmpieza escribiendo # para buscar temas."
},
"qrReaderPage": {
"emptyTitle": "Permisos no concedidos",
@ -430,6 +430,7 @@
"userUnblocked": "Perfil desbloqueado",
"userBlocked": "Perfil bloqueado"
},
"addRelay": "Connectar a relay",
"invoice": "Propina",
"message": "Mensaje",
"follow": "Seguir",

View File

@ -13,9 +13,9 @@
"privateKeysSnackbarDescription": "Conservez votre clé privée dans un endroit sûr. Si vous la perdez, vous ne pourrez plus avoir accès ni récupérer votre compte."
},
"searchPage": {
"placeholder": "Look for public keys, notes...",
"placeholder": "Look for public keys, notes, hashtags...",
"emptyTitle": "Tip",
"emptyDescription": "Start typing @ to find someone"
"emptyDescription": "Start typing @ to find someone\n\nStart typing # to search topics"
},
"qrReaderPage": {
"emptyTitle": "Permissions not granted",
@ -414,6 +414,7 @@
"userUnblocked": "Profile unblocked",
"userBlocked": "Profile unblocked"
},
"addRelay": "Connect relay",
"invoice": "Tip",
"message": "Message",
"follow": "Abonné",

View File

@ -13,9 +13,9 @@
"privateKeysSnackbarDescription": "Keep your private key in a safe place, if you lose it you will not be able to access it again or recover your account."
},
"searchPage": {
"placeholder": "Look for public keys, notes...",
"placeholder": "Look for public keys, notes, hashtags...",
"emptyTitle": "Tip",
"emptyDescription": "Start typing @ to find someone"
"emptyDescription": "Start typing @ to find someone\n\nStart typing # to search topics"
},
"qrReaderPage": {
"emptyTitle": "Permissions not granted",
@ -422,6 +422,7 @@
"userUnblocked": "Profile unblocked",
"userBlocked": "Profile unblocked"
},
"addRelay": "Connect relay",
"invoice": "Tip",
"message": "Message",
"follow": "Follow",

View File

@ -13,9 +13,9 @@
"privateKeysSnackbarDescription": "请妥善保管您的私钥。如果遗失,您将无法访问或恢复您的账号。"
},
"searchPage": {
"placeholder": "Look for public keys, notes...",
"placeholder": "Look for public keys, notes, hashtags...",
"emptyTitle": "Tip",
"emptyDescription": "Start typing @ to find someone"
"emptyDescription": "Start typing @ to find someone\n\nStart typing # to search topics"
},
"qrReaderPage": {
"emptyTitle": "未授予权限",
@ -423,6 +423,7 @@
"userUnblocked": "已屏蔽",
"userBlocked": "已取消屏蔽"
},
"addRelay": "Connect relay",
"invoice": "赞赏",
"message": "私信",
"follow": "关注",

View File

@ -74,7 +74,9 @@ export const FirstStep: React.FC<FirstStepProps> = ({ nextStep }) => {
relays.forEach((relay) => {
removeRelayItem(relay)
})
metadata.tags.forEach(async (tag) => await addRelayItem({ url: tag[1] }))
metadata.tags.forEach(async (tag) => {
if (tag[0] === 'r') await addRelayItem({ url: tag[1] })
})
nextStep()
}
}

View File

@ -1,6 +1,8 @@
import { useFocusEffect } from '@react-navigation/native'
import { FlashList, ListRenderItem } from '@shopify/flash-list'
import { t } from 'i18next'
import debounce from 'lodash.debounce'
import { Kind } from 'nostr-tools'
import { decode } from 'nostr-tools/nip19'
import * as React from 'react'
import { StyleSheet, View } from 'react-native'
@ -9,6 +11,7 @@ import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityI
import NoteCard from '../../Components/NoteCard'
import ProfileData from '../../Components/ProfileData'
import { AppContext } from '../../Contexts/AppContext'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { getNotes, Note } from '../../Functions/DatabaseFunctions/Notes'
import { getUsers, User } from '../../Functions/DatabaseFunctions/Users'
import { validNip21 } from '../../Functions/NativeFunctions'
@ -16,17 +19,19 @@ import { navigate } from '../../lib/Navigation'
import { getNpub } from '../../lib/nostr/Nip19'
interface SearchPageProps {
route: { params: { urls: string[]; index?: number } }
route: { params: { search: string } }
}
export const SearchPage: React.FC<SearchPageProps> = ({ route }) => {
const pageSize = 30
const theme = useTheme()
const { database } = React.useContext(AppContext)
const { relayPool, lastEventId } = React.useContext(RelayPoolContext)
const [users, setUsers] = React.useState<User[]>([])
const [resultsUsers, setResultsUsers] = React.useState<User[]>([])
const [notes, setNotes] = React.useState<Note[]>([])
const [resultsNotes, setResultsNotes] = React.useState<Note[]>([])
const [searchInput, setSearchInput] = React.useState<string>('')
const [searchInput, setSearchInput] = React.useState<string>(route?.params?.search ?? '')
const inputRef = React.useRef<TextInput>(null)
useFocusEffect(
@ -46,40 +51,77 @@ export const SearchPage: React.FC<SearchPageProps> = ({ route }) => {
)
React.useEffect(() => {
if (searchInput !== '') {
if (/^#.*/.test(searchInput)) {
const search = searchInput.toLocaleLowerCase()
if (/^@.*/.test(search)) {
const searchUser = search.replace(/^@/, '')
setResultsUsers(
users.filter(
(user) =>
user.name?.toLocaleLowerCase().includes(searchUser) ??
user.nip05?.toLocaleLowerCase().includes(searchUser),
),
)
} else {
if (validNip21(search)) {
try {
const key = decode(search.replace('nostr:', ''))
if (key?.data) {
if (key.type === 'nevent') {
setSearchInput('')
navigate('Note', { noteId: key.data.id })
} else if (key.type === 'npub') {
setSearchInput('')
navigate('Profile', { pubKey: key.data })
} else if (key.type === 'nprofile' && key.data.pubkey) {
setSearchInput('')
navigate('Profile', { pubKey: key.data.pubkey })
}
}
} catch {}
}
setResultsNotes(notes.filter((note) => note.content.toLocaleLowerCase().includes(search)))
setResultsNotes(
notes.filter((note) => note.content.toLocaleLowerCase().includes(search.trim())),
)
}
}, [lastEventId])
const subscribeHandler = React.useMemo(
() =>
debounce((hastags) => {
relayPool?.subscribe('search-hastags', [
{
kinds: [Kind.Text],
'#t': hastags,
limit: pageSize,
},
])
}, 600),
[pageSize],
)
React.useEffect(() => {
if (/^#.*/.test(searchInput)) {
const hastags = [...searchInput.matchAll(/#([^#]\S+)/gi)].map((match) => {
return match[1]
})
if (hastags.length > 0) {
subscribeHandler(hastags)
}
}
}, [searchInput])
React.useEffect(() => {
if (/^@.*/.test(searchInput)) {
const searchUser = searchInput.replace(/^@/, '')
setResultsUsers(
users.filter(
(user) =>
user.name?.toLocaleLowerCase().includes(searchUser) ??
user.nip05?.toLocaleLowerCase().includes(searchUser),
),
)
} else {
const search = searchInput.toLocaleLowerCase()
setResultsNotes(
notes.filter((note) => note.content.toLocaleLowerCase().includes(search.trim())),
)
}
}, [searchInput, notes])
React.useEffect(() => {
if (searchInput !== '' && validNip21(searchInput)) {
try {
const key = decode(searchInput.replace('nostr:', ''))
if (key?.data) {
if (key.type === 'nevent') {
setSearchInput('')
navigate('Note', { noteId: key.data.id })
} else if (key.type === 'npub') {
setSearchInput('')
navigate('Profile', { pubKey: key.data })
} else if (key.type === 'nprofile' && key.data.pubkey) {
setSearchInput('')
navigate('Profile', { pubKey: key.data.pubkey })
}
}
} catch {}
}
}, [searchInput])
const renderItemNote: ListRenderItem<Note> = ({ item, index }) => {
return (
<View style={styles.noteCard} key={item.id}>
@ -173,7 +215,8 @@ const styles = StyleSheet.create({
paddingTop: 16,
},
container: {
padding: 16,
paddingLeft: 16,
paddingRight: 16,
flex: 1,
},
inputContainer: {

View File

@ -97,6 +97,10 @@ export const SendPage: React.FC<SendPageProps> = ({ route }) => {
})
}
;[...rawContent.matchAll(/#([^#]\S+)/gi)].forEach((match) => {
if (match[1]) tags.push(['t', match[1]])
})
const event: Event = {
content: rawContent,
created_at: getUnixTime(new Date()),
@ -104,6 +108,7 @@ export const SendPage: React.FC<SendPageProps> = ({ route }) => {
pubkey: publicKey,
tags,
}
sendEvent(event).catch(() => {})
}
}

View File

@ -7,6 +7,7 @@ export interface RelayFilters {
kinds?: number[]
'#e'?: string[]
'#p'?: string[]
'#t'?: string[]
since?: number
limit?: number
until?: number