New UI Main Feed and notifications

This commit is contained in:
KoalaSat 2023-01-16 11:54:45 +01:00
parent b16e96c753
commit 0e0485c9bf
No known key found for this signature in database
GPG Key ID: 2F7F61C6146AB157
12 changed files with 514 additions and 539 deletions

View File

@ -12,7 +12,7 @@ import { populateRelay } from '../../Functions/RelayFunctions'
import { NostrosAvatar } from '../Avatar'
import { searchRelays } from '../../Functions/DatabaseFunctions/Relays'
import TextContent from '../../Components/TextContent'
import { usernamePubKey } from '../../Functions/RelayFunctions/Users'
import { formatPubKey, usernamePubKey } from '../../Functions/RelayFunctions/Users'
import { getReactionsCount, getUserReaction } from '../../Functions/DatabaseFunctions/Reactions'
import { UserContext } from '../../Contexts/UserContext'
import {
@ -32,9 +32,10 @@ import { push } from '../../lib/Navigation'
interface NoteCardProps {
note: Note
onPressOptions?: () => void
showAnswerData?: boolean
}
export const NoteCard: React.FC<NoteCardProps> = ({ note, onPressOptions = () => {} }) => {
export const NoteCard: React.FC<NoteCardProps> = ({ note, showAnswerData = true, onPressOptions = () => {} }) => {
const theme = useTheme()
const { publicKey, privateKey } = React.useContext(UserContext)
const { relayPool, lastEventId } = useContext(RelayPoolContext)
@ -88,12 +89,43 @@ export const NoteCard: React.FC<NoteCardProps> = ({ note, onPressOptions = () =>
}
const textNote: () => JSX.Element = () => {
return hide ? (
<Button mode='outlined' onPress={() => setHide(false)}>
{t('noteCard.contentWarning')}
</Button>
) : (
<TextContent event={note} />
return (
<>
{note.reply_event_id && showAnswerData && (
<TouchableRipple
onPress={() =>
note.kind !== EventKind.recommendServer && push('Note', { noteId: note.reply_event_id })
}
>
<Card.Content style={[styles.answerContent, { borderColor: theme.colors.onSecondary }]}>
<View style={styles.answerData}>
<MaterialCommunityIcons name='arrow-left-top' size={16} />
<Text>
{t('noteCard.answering', { username: formatPubKey(note.reply_event_id) })}
</Text>
</View>
<View>
<Text style={styles.link}>{t('noteCard.seeParent')}</Text>
</View>
</Card.Content>
</TouchableRipple>
)}
<TouchableRipple
onPress={() =>
note.kind !== EventKind.recommendServer && push('Note', { noteId: note.id })
}
>
<Card.Content style={[styles.content, { borderColor: theme.colors.onSecondary }]}>
{hide ? (
<Button mode='outlined' onPress={() => setHide(false)}>
{t('noteCard.contentWarning')}
</Button>
) : (
<TextContent event={note} />
)}
</Card.Content>
</TouchableRipple>
</>
)
}
@ -110,28 +142,34 @@ export const NoteCard: React.FC<NoteCardProps> = ({ note, onPressOptions = () =>
}
return (
<Card>
<Card.Content style={styles.title}>
<View>
<Avatar.Icon
size={54}
icon='chart-timeline-variant'
style={{
backgroundColor: theme.colors.tertiaryContainer,
}}
/>
</View>
<View>
<Text>{t('noteCard.recommendation')}</Text>
<Text>{relayName}</Text>
</View>
<TouchableRipple
onPress={() => note.kind !== EventKind.recommendServer && push('Note', { noteId: note.id })}
>
<Card.Content style={[styles.content, { borderColor: theme.colors.onSecondary }]}>
<Card>
<Card.Content style={styles.title}>
<View>
<Avatar.Icon
size={54}
icon='chart-timeline-variant'
style={{
backgroundColor: theme.colors.tertiaryContainer,
}}
/>
</View>
<View>
<Text>{t('noteCard.recommendation')}</Text>
<Text>{relayName}</Text>
</View>
</Card.Content>
{!relayAdded && REGEX_SOCKET_LINK.test(note.content) && (
<Card.Content style={[styles.actions, { borderColor: theme.colors.onSecondary }]}>
<Button onPress={addRelayItem}>{t('noteCard.addRelay')}</Button>
</Card.Content>
)}
</Card>
</Card.Content>
{!relayAdded && REGEX_SOCKET_LINK.test(note.content) && (
<Card.Content style={[styles.actions, { borderColor: theme.colors.onSecondary }]}>
<Button onPress={addRelayItem}>{t('noteCard.addRelay')}</Button>
</Card.Content>
)}
</Card>
</TouchableRipple>
)
}
@ -164,15 +202,7 @@ export const NoteCard: React.FC<NoteCardProps> = ({ note, onPressOptions = () =>
<IconButton icon='dots-vertical' size={25} onPress={onPressOptions} />
</View>
</Card.Content>
<TouchableRipple
onPress={() =>
note.kind !== EventKind.recommendServer && push('Note', { noteId: note.id })
}
>
<Card.Content style={[styles.content, { borderColor: theme.colors.onSecondary }]}>
{getNoteContent()}
</Card.Content>
</TouchableRipple>
{getNoteContent()}
<Card.Content style={[styles.actions, { borderColor: theme.colors.onSecondary }]}>
<Button
onPress={() => {
@ -182,7 +212,12 @@ export const NoteCard: React.FC<NoteCardProps> = ({ note, onPressOptions = () =>
publishReaction(false)
}
}}
icon={() => <MaterialCommunityIcons name={userDownvoted ? 'thumb-down' : 'thumb-down-outline'} size={25} />}
icon={() => (
<MaterialCommunityIcons
name={userDownvoted ? 'thumb-down' : 'thumb-down-outline'}
size={25}
/>
)}
>
{negaiveReactions === undefined || negaiveReactions === 0 ? '-' : negaiveReactions}
</Button>
@ -194,7 +229,12 @@ export const NoteCard: React.FC<NoteCardProps> = ({ note, onPressOptions = () =>
publishReaction(true)
}
}}
icon={() => <MaterialCommunityIcons name={userUpvoted ? 'thumb-up' : 'thumb-up-outline'} size={25} />}
icon={() => (
<MaterialCommunityIcons
name={userUpvoted ? 'thumb-up' : 'thumb-up-outline'}
size={25}
/>
)}
>
{positiveReactions === undefined || positiveReactions === 0 ? '-' : positiveReactions}
</Button>
@ -212,12 +252,21 @@ const styles = StyleSheet.create({
flexDirection: 'row',
justifyContent: 'space-between',
alignContent: 'center',
paddingBottom: 16
paddingBottom: 16,
},
titleUser: {
flexDirection: 'row',
alignContent: 'center',
},
answerData: {
flexDirection: 'row',
},
answerContent: {
flexDirection: 'row',
borderTopWidth: 1,
padding: 16,
justifyContent: 'space-between',
},
actions: {
paddingTop: 16,
flexDirection: 'row',
@ -233,6 +282,9 @@ const styles = StyleSheet.create({
borderTopWidth: 1,
padding: 16,
},
link: {
textDecorationLine: 'underline',
},
})
export default NoteCard

View File

@ -1,55 +0,0 @@
import React, { useContext } from 'react'
import { Card, Layout, Text } from '@ui-kitten/components'
import { User } from '../../Functions/DatabaseFunctions/Users'
import { StyleSheet } from 'react-native'
import { AppContext } from '../../Contexts/AppContext'
import Avatar from '../Avatar'
interface UserCardProps {
user: User
}
export const UserCard: React.FC<UserCardProps> = ({ user }) => {
const { goToPage } = useContext(AppContext)
const styles = StyleSheet.create({
layout: {
flex: 1,
flexDirection: 'row',
backgroundColor: 'transparent',
},
profile: {
flex: 1,
width: 38,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'transparent',
},
content: {
flex: 5,
backgroundColor: 'transparent',
},
actions: {
flex: 1,
backgroundColor: 'transparent',
},
})
return (
user && (
<Card onPress={() => goToPage(`profile#${user.id}`)}>
<Layout style={styles.layout} level='2'>
<Layout style={styles.profile}>
<Avatar name={user.name} src={user.picture} pubKey={user.id} />
</Layout>
<Layout style={styles.content} level='2'>
<Text>{user.name}</Text>
<Text appearance='hint'>{user.id}</Text>
</Layout>
</Layout>
</Card>
)
)
}
export default UserCard

View File

@ -4,4 +4,4 @@
export { Avatar } from './Avatar'
export { Button } from './Button'
export { UserCard } from './UserCard'
export { UserCard } from '../Pages/HomeFeed'

View File

@ -6,6 +6,7 @@ export interface Note extends Event {
name: string
picture: string
lnurl: string
reply_event_id: string
}
const databaseToEntity: (object: any) => Note = (object) => {

View File

@ -16,6 +16,9 @@
"configuration": "Configuration",
"about": "About",
"logout": "Logout"
},
"noteCard": {
"answering": "Answer to {{username}}"
}
}
}

View File

@ -1,18 +1,6 @@
import {
Card,
Input,
Layout,
Modal,
Tab,
TabBar,
TopNavigation,
useTheme,
} from '@ui-kitten/components'
import React, { useContext, useEffect, useState } from 'react'
import { ScrollView, StyleSheet, Text, TouchableOpacity } from 'react-native'
import { AppContext } from '../../Contexts/AppContext'
import Icon from 'react-native-vector-icons/FontAwesome5'
import { showMessage } from 'react-native-flash-message'
import { EventKind } from '../../lib/nostr/Events'
import { useTranslation } from 'react-i18next'
import { getUsers, updateUserContact, User } from '../../Functions/DatabaseFunctions/Users'
@ -21,8 +9,9 @@ import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { populatePets } from '../../Functions/RelayFunctions/Users'
import { getNip19Key } from '../../lib/nostr/Nip19'
import { UserContext } from '../../Contexts/UserContext'
import { useTheme } from 'react-native-paper'
export const ContactsPage: React.FC = () => {
export const ContactsFeed: React.FC = () => {
const { database } = useContext(AppContext)
const { publicKey, privateKey } = React.useContext(UserContext)
const { relayPool, lastEventId } = useContext(RelayPoolContext)
@ -95,64 +84,14 @@ export const ContactsPage: React.FC = () => {
setIsAddingContact(false) // restore sending status
})
.catch((err) => {
showMessage({
message: t('alerts.contactAddError'),
description: err.message,
type: 'danger',
})
setIsAddingContact(false) // restore sending status
})
}
}
const onPressBack: () => void = () => {
relayPool?.unsubscribeAll()
goBack()
}
const renderBackAction = (): JSX.Element => {
return (
<Button
accessoryRight={<Icon name='arrow-left' size={16} color={theme['text-basic-color']} />}
onPress={onPressBack}
appearance='ghost'
/>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
actionContainer: {
marginTop: 30,
marginBottom: 30,
paddingLeft: 12,
paddingRight: 12,
},
button: {
marginTop: 30,
},
icon: {
width: 32,
height: 32,
},
modal: {
paddingLeft: 32,
paddingRight: 32,
width: '100%',
},
backdrop: {
backgroundColor: 'rgba(0, 0, 0, 0.5)',
},
topBar: {
height: 64,
},
})
return (
<>
<TopNavigation alignment='center' accessoryLeft={renderBackAction} />
{/* <TopNavigation alignment='center' accessoryLeft={renderBackAction} />
<TabBar
style={styles.topBar}
selectedIndex={selectedTab}
@ -211,9 +150,9 @@ export const ContactsPage: React.FC = () => {
>
<Icon name='user-plus' size={30} color={theme['text-basic-color']} solid />
</TouchableOpacity>
)}
)} */}
</>
)
}
export default ContactsPage
export default ContactsFeed

View File

@ -0,0 +1,205 @@
import React, { useCallback, useContext, useState, useEffect } from 'react'
import { getUsers, User } from '../../Functions/DatabaseFunctions/Users'
import {
Dimensions,
NativeScrollEvent,
NativeSyntheticEvent,
RefreshControl,
ScrollView,
StyleSheet,
View,
} from 'react-native'
import { AppContext } from '../../Contexts/AppContext'
import { getMainNotes, Note } from '../../Functions/DatabaseFunctions/Notes'
import { handleInfinityScroll } from '../../Functions/NativeFunctions'
import { UserContext } from '../../Contexts/UserContext'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { EventKind } from '../../lib/nostr/Events'
import { RelayFilters } from '../../lib/nostr/RelayPool/intex'
import { ActivityIndicator, AnimatedFAB } from 'react-native-paper'
import NoteCard from '../../Components/NoteCard'
import RBSheet from 'react-native-raw-bottom-sheet'
import ProfileCard from '../../Components/ProfileCard'
import { useTheme } from '@react-navigation/native'
import { navigate } from '../../lib/Navigation'
export const HomeFeed: React.FC = () => {
const theme = useTheme()
const { database } = useContext(AppContext)
const { publicKey } = useContext(UserContext)
const { lastEventId, relayPool } = useContext(RelayPoolContext)
const initialPageSize = 10
const [notes, setNotes] = useState<Note[]>([])
const [authors, setAuthors] = useState<User[]>([])
const [pageSize, setPageSize] = useState<number>(initialPageSize)
const [refreshing, setRefreshing] = useState(true)
const [firstLoad, setFirstLoad] = useState(true)
const [profileCardPubkey, setProfileCardPubKey] = useState<string>()
const bottomSheetProfileRef = React.useRef<RBSheet>(null)
useEffect(() => {
relayPool?.unsubscribeAll()
if (relayPool && publicKey) {
setFirstLoad(false)
calculateInitialNotes().then(() => loadNotes())
}
}, [publicKey, relayPool])
useEffect(() => {
if (!firstLoad) {
loadNotes()
}
}, [lastEventId])
useEffect(() => {
if (pageSize > initialPageSize) {
relayPool?.unsubscribeAll()
subscribeNotes(authors, true)
loadNotes()
}
}, [pageSize])
const onRefresh = useCallback(() => {
setRefreshing(true)
relayPool?.unsubscribeAll()
if (relayPool && publicKey) {
calculateInitialNotes().then(() => loadNotes())
}
}, [])
const subscribeNotes: (users: User[], past?: boolean) => void = async (users, past) => {
if (!database || !publicKey) return
const lastNotes: Note[] = await getMainNotes(database, publicKey, initialPageSize)
const lastNote: Note = lastNotes[lastNotes.length - 1]
const message: RelayFilters = {
kinds: [EventKind.textNote, EventKind.recommendServer],
authors: [...users.map((user) => user.id), publicKey],
}
if (lastNote && lastNotes.length >= pageSize && !past) {
message.since = lastNote.created_at
} else {
message.limit = pageSize + initialPageSize
}
relayPool?.subscribe('homepage-main', [message])
}
const calculateInitialNotes: () => Promise<void> = async () => {
if (database && publicKey) {
relayPool?.subscribe('homepage-contacts', [
{
kinds: [EventKind.petNames],
authors: [publicKey],
},
])
const users = await getUsers(database, { contacts: true, includeIds: [publicKey] })
subscribeNotes(users)
setAuthors(users)
}
}
const onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void = (event) => {
if (handleInfinityScroll(event)) {
setPageSize(pageSize + initialPageSize)
}
}
const loadNotes: () => void = () => {
if (database && publicKey) {
getMainNotes(database, publicKey, pageSize).then((notes) => {
setNotes(notes)
setRefreshing(false)
relayPool?.subscribe('homepage-contacts-meta', [
{
kinds: [EventKind.meta],
authors: notes.map((note) => note.pubkey),
},
{
kinds: [EventKind.reaction],
'#e': notes.map((note) => note.id ?? ''),
},
])
})
}
}
const renderItem: (note: Note) => JSX.Element = (note) => {
return (
<View style={styles.noteCard} key={note.id}>
<NoteCard
note={note}
onPressOptions={() => {
setProfileCardPubKey(note.pubkey)
bottomSheetProfileRef.current?.open()
}}
/>
</View>
)
}
const bottomSheetStyles = React.useMemo(() => {
return {
container: {
backgroundColor: theme.colors.background,
padding: 16,
borderTopRightRadius: 28,
borderTopLeftRadius: 28,
},
draggableIcon: {
backgroundColor: '#000',
},
}
}, [])
return (
<>
{notes && notes.length > 0 && (
<ScrollView
onScroll={onScroll}
horizontal={false}
showsVerticalScrollIndicator={false}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
style={styles.list}
>
{notes.map((note) => renderItem(note))}
{notes.length >= 10 && <ActivityIndicator animating={true} />}
</ScrollView>
)}
<AnimatedFAB
style={[styles.fab, { top: Dimensions.get('window').height - 220 }]}
icon='pencil-outline'
label='Label'
onPress={() => navigate('Send')}
animateFrom='right'
iconMode='static'
extended={false}
/>
<RBSheet
ref={bottomSheetProfileRef}
closeOnDragDown={true}
height={280}
customStyles={bottomSheetStyles}
>
<ProfileCard userPubKey={profileCardPubkey ?? ''} bottomSheetRef={bottomSheetProfileRef} />
</RBSheet>
</>
)
}
const styles = StyleSheet.create({
list: {
padding: 16,
},
noteCard: {
marginBottom: 16,
},
fab: {
right: 16,
position: 'absolute',
},
})
export default HomeFeed

View File

@ -1,233 +1,31 @@
import React, { useCallback, useContext, useEffect, useState } from 'react'
import { t } from 'i18next'
import {
Dimensions,
NativeScrollEvent,
NativeSyntheticEvent,
RefreshControl,
ScrollView,
StyleSheet,
TouchableOpacity,
} from 'react-native'
import { AppContext } from '../../Contexts/AppContext'
import { getMainNotes, Note } from '../../Functions/DatabaseFunctions/Notes'
import NoteCard from '../../Components/NoteCard'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { EventKind } from '../../lib/nostr/Events'
import { getReplyEventId } from '../../Functions/RelayFunctions/Events'
import { getUsers, User } from '../../Functions/DatabaseFunctions/Users'
import { handleInfinityScroll } from '../../Functions/NativeFunctions'
import { RelayFilters } from '../../lib/nostr/RelayPool/intex'
import { AnimatedFAB, BottomNavigation, Text, useTheme } from 'react-native-paper'
import { navigate } from '../../lib/Navigation'
import React from 'react'
import { BottomNavigation } from 'react-native-paper'
import ContactsFeed from '../ContactsFeed'
import HomeFeed from '../HomeFeed'
import NotificationsFeed from '../NotificationsFeed'
export const HomePage: React.FC = () => {
const { database, goToPage } = useContext(AppContext)
const initialPageSize = 10
const { lastEventId, relayPool, publicKey, privateKey } = useContext(RelayPoolContext)
const theme = useTheme()
const [pageSize, setPageSize] = useState<number>(initialPageSize)
const [notes, setNotes] = useState<Note[]>([])
const [authors, setAuthors] = useState<User[]>([])
const [refreshing, setRefreshing] = useState(true)
const [firstLoad, setFirstLoad] = useState(true)
const [index, setIndex] = React.useState(0);
const [index, setIndex] = React.useState(0)
const [routes] = React.useState([
{ key: 'home', focusedIcon: 'home', unfocusedIcon: 'home-outline'},
{ key: 'feed', focusedIcon: 'home', unfocusedIcon: 'home-outline' },
{ key: 'contacts', focusedIcon: 'account-group', unfocusedIcon: 'account-group-outline' },
{ key: 'notifications', focusedIcon: 'bell', unfocusedIcon: 'bell-outline' },
])
const calculateInitialNotes: () => Promise<void> = async () => {
if (database && publicKey) {
relayPool?.subscribe('homepage-contacts', [
{
kinds: [EventKind.petNames],
authors: [publicKey],
},
])
const users = await getUsers(database, { contacts: true, includeIds: [publicKey] })
subscribeNotes(users)
setAuthors(users)
}
}
const subscribeNotes: (users: User[], past?: boolean) => void = async (users, past) => {
if (!database || !publicKey) return
const lastNotes: Note[] = await getMainNotes(database, publicKey, initialPageSize)
const lastNote: Note = lastNotes[lastNotes.length - 1]
const message: RelayFilters = {
kinds: [EventKind.textNote, EventKind.recommendServer],
authors: [...users.map((user) => user.id), publicKey],
}
if (lastNote && lastNotes.length >= pageSize && !past) {
message.since = lastNote.created_at
} else {
message.limit = pageSize + initialPageSize
}
relayPool?.subscribe('homepage-main', [message])
}
const loadNotes: () => void = () => {
if (database && publicKey) {
getMainNotes(database, publicKey, pageSize).then((notes) => {
setNotes(notes)
setRefreshing(false)
relayPool?.subscribe('homepage-contacts-meta', [
{
kinds: [EventKind.meta],
authors: notes.map((note) => note.pubkey),
},
{
kinds: [EventKind.reaction],
'#e': notes.map((note) => note.id ?? ''),
},
])
})
}
}
useEffect(() => {
relayPool?.unsubscribeAll()
if (relayPool && publicKey) {
setFirstLoad(false)
calculateInitialNotes().then(() => loadNotes())
}
}, [publicKey, relayPool])
useEffect(() => {
if (!firstLoad) {
loadNotes()
}
}, [lastEventId])
useEffect(() => {
if (pageSize > initialPageSize) {
relayPool?.unsubscribeAll()
subscribeNotes(authors, true)
loadNotes()
}
}, [pageSize])
const onPress: (note: Note) => void = (note) => {
if (note.kind !== EventKind.recommendServer) {
const replyEventId = getReplyEventId(note)
if (replyEventId) {
goToPage(`note#${replyEventId}`)
} else if (note.id) {
goToPage(`note#${note.id}`)
}
}
}
const itemCard: (note: Note) => JSX.Element = (note) => {
return (
// <Card onPress={() => onPress(note)} key={note.id ?? ''}>
// <NoteCard note={note} onlyContactsReplies={true} showReplies={true} />
// </Card>
<></>
)
}
const onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void = (event) => {
if (handleInfinityScroll(event)) {
setPageSize(pageSize + initialPageSize)
}
}
const onRefresh = useCallback(() => {
setRefreshing(true)
relayPool?.unsubscribeAll()
if (relayPool && publicKey) {
calculateInitialNotes().then(() => loadNotes())
}
}, [])
const HomeRoute: () => JSX.Element = () => <Text>MyFeedRoute</Text>
const renderScene = BottomNavigation.SceneMap({
home: HomeRoute,
feed: HomeFeed,
contacts: ContactsFeed,
notifications: NotificationsFeed,
})
return (
<>
{/* <Layout style={styles.container} level='3'>
{notes.length > 0 ? (
<ScrollView
onScroll={onScroll}
horizontal={false}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
>
{notes.map((note) => itemCard(note))}
{notes.length >= 10 && (
<Layout style={styles.spinner}>
<Spinner size='small' />
</Layout>
)}
</ScrollView>
) : (
<Layout style={styles.empty} level='3'>
<Layout style={styles.noContacts} level='3'>
<Text>{t('homePage.noContacts')}</Text>
</Layout>
<Button
onPress={() => goToPage('contacts')}
status='warning'
accessoryLeft={
<Icon name='address-book' size={16} color={theme['text-basic-color']} solid />
}
>
{t('homePage.addContacts')}
</Button>
</Layout>
)}
</Layout>
{privateKey && (
<TouchableOpacity
style={{
borderWidth: 1,
borderColor: 'rgba(0,0,0,0.2)',
alignItems: 'center',
justifyContent: 'center',
width: 65,
position: 'absolute',
bottom: 20,
right: 20,
height: 65,
backgroundColor: theme['color-warning-500'],
borderRadius: 100,
}}
onPress={() => goToPage('send')}
>
<Icon name='paper-plane' size={30} color={theme['text-basic-color']} solid />
</TouchableOpacity>
)} */}
<BottomNavigation
navigationState={{ index, routes }}
onIndexChange={setIndex}
renderScene={renderScene}
labeled={false}
/>
<AnimatedFAB
style={[styles.fab, { top: Dimensions.get('window').height - 220 }]}
icon='pencil-outline'
label='Label'
onPress={() => navigate('Send')}
animateFrom='right'
iconMode='static'
extended={false}
/>
</>
<BottomNavigation
navigationState={{ index, routes }}
onIndexChange={setIndex}
renderScene={renderScene}
labeled={false}
/>
)
}
const styles = StyleSheet.create({
fab: {
right: 16,
position: 'absolute',
},
})
export default HomePage

View File

@ -1,146 +0,0 @@
import { Card, Layout, Spinner, Text } from '@ui-kitten/components'
import React, { useContext, useEffect, useState } from 'react'
import { t } from 'i18next'
import { NativeScrollEvent, NativeSyntheticEvent, ScrollView, StyleSheet } from 'react-native'
import { AppContext } from '../../Contexts/AppContext'
import { getMentionNotes, Note } from '../../Functions/DatabaseFunctions/Notes'
import NoteCard from '../../Components/NoteCard'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { EventKind } from '../../lib/nostr/Events'
import { getReplyEventId } from '../../Functions/RelayFunctions/Events'
import { handleInfinityScroll } from '../../Functions/NativeFunctions'
export const MentionsPage: React.FC = () => {
const { database, goToPage } = useContext(AppContext)
const initialPageSize = 10
const { lastEventId, relayPool, publicKey } = useContext(RelayPoolContext)
const [pageSize, setPageSize] = useState<number>(initialPageSize)
const [notes, setNotes] = useState<Note[]>([])
const calculateInitialNotes: () => Promise<void> = async () => {
if (database && publicKey) {
subscribeNotes()
}
}
const subscribeNotes: () => void = async () => {
if (!database || !publicKey) return
relayPool?.subscribe('mentions-user', [
{
kinds: [EventKind.textNote],
'#p': [publicKey],
limit: pageSize,
},
{
kinds: [EventKind.textNote],
'#e': [publicKey],
limit: pageSize,
},
])
}
const loadNotes: () => void = () => {
if (database && publicKey) {
getMentionNotes(database, publicKey, pageSize).then((notes) => {
setNotes(notes)
const missingDataNotes = notes.map((note) => note.pubkey)
relayPool?.subscribe('mentions-answers', [
{
kinds: [EventKind.reaction],
'#e': notes.map((note) => note.id ?? ''),
},
{
kinds: [EventKind.meta],
authors: missingDataNotes,
},
])
})
}
}
useEffect(() => {
relayPool?.unsubscribeAll()
if (relayPool && publicKey) {
calculateInitialNotes().then(() => loadNotes())
}
}, [publicKey, relayPool])
useEffect(() => {
loadNotes()
}, [lastEventId])
useEffect(() => {
if (pageSize > initialPageSize) {
relayPool?.unsubscribeAll()
subscribeNotes()
loadNotes()
}
}, [pageSize])
const onPress: (note: Note) => void = (note) => {
if (note.kind !== EventKind.recommendServer) {
const replyEventId = getReplyEventId(note)
if (replyEventId) {
goToPage(`note#${replyEventId}`)
} else if (note.id) {
goToPage(`note#${note.id}`)
}
}
}
const itemCard: (note: Note) => JSX.Element = (note) => {
return (
<Card onPress={() => onPress(note)} key={note.id ?? ''}>
<NoteCard note={note} onlyContactsReplies={true} showReplies={true} />
</Card>
)
}
const onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void = (event) => {
if (handleInfinityScroll(event)) {
setPageSize(pageSize + initialPageSize)
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
icon: {
width: 32,
height: 32,
},
empty: {
height: 64,
justifyContent: 'center',
alignItems: 'center',
},
spinner: {
justifyContent: 'center',
alignItems: 'center',
height: 64,
},
})
return (
<Layout style={styles.container} level='3'>
{notes && notes.length > 0 ? (
<ScrollView onScroll={onScroll} horizontal={false}>
{notes.map((note) => itemCard(note))}
{notes.length >= 10 && (
<Layout style={styles.spinner}>
<Spinner size='small' />
</Layout>
)}
</ScrollView>
) : (
<Layout style={styles.empty} level='3'>
<Text>{t('mentionsPage.noMentions')}</Text>
</Layout>
)}
</Layout>
)
}
export default MentionsPage

View File

@ -144,6 +144,7 @@ export const NotePage: React.FC<NotePageProps> = ({ route }) => {
setProfileCardPubKey(note.pubkey)
bottomSheetProfileRef.current?.open()
}}
showAnswerData={false}
/>
</View>
)

View File

@ -0,0 +1,172 @@
import React, { useCallback, useContext, useEffect, useState } from 'react'
import {
NativeScrollEvent,
NativeSyntheticEvent,
RefreshControl,
ScrollView,
StyleSheet,
View,
} from 'react-native'
import { AppContext } from '../../Contexts/AppContext'
import { getMentionNotes, Note } from '../../Functions/DatabaseFunctions/Notes'
import NoteCard from '../../Components/NoteCard'
import { RelayPoolContext } from '../../Contexts/RelayPoolContext'
import { EventKind } from '../../lib/nostr/Events'
import { handleInfinityScroll } from '../../Functions/NativeFunctions'
import { UserContext } from '../../Contexts/UserContext'
import RBSheet from 'react-native-raw-bottom-sheet'
import { ActivityIndicator, useTheme } from 'react-native-paper'
import ProfileCard from '../../Components/ProfileCard'
export const NotificationsFeed: React.FC = () => {
const theme = useTheme()
const { database } = useContext(AppContext)
const { publicKey } = useContext(UserContext)
const initialPageSize = 10
const { lastEventId, relayPool } = useContext(RelayPoolContext)
const [pageSize, setPageSize] = useState<number>(initialPageSize)
const [notes, setNotes] = useState<Note[]>([])
const bottomSheetProfileRef = React.useRef<RBSheet>(null)
const [refreshing, setRefreshing] = useState(true)
const [profileCardPubkey, setProfileCardPubKey] = useState<string>()
useEffect(() => {
relayPool?.unsubscribeAll()
if (relayPool && publicKey) {
calculateInitialNotes().then(() => loadNotes())
}
}, [publicKey, relayPool])
useEffect(() => {
loadNotes()
}, [lastEventId])
useEffect(() => {
if (pageSize > initialPageSize) {
relayPool?.unsubscribeAll()
subscribeNotes()
loadNotes()
}
}, [pageSize])
const calculateInitialNotes: () => Promise<void> = async () => {
if (database && publicKey) {
subscribeNotes()
}
}
const subscribeNotes: () => void = async () => {
if (!database || !publicKey) return
relayPool?.subscribe('mentions-user', [
{
kinds: [EventKind.textNote],
'#p': [publicKey],
limit: pageSize,
},
{
kinds: [EventKind.textNote],
'#e': [publicKey],
limit: pageSize,
},
])
}
const loadNotes: () => void = () => {
if (database && publicKey) {
getMentionNotes(database, publicKey, pageSize).then((notes) => {
setNotes(notes)
setRefreshing(false)
const missingDataNotes = notes.map((note) => note.pubkey)
relayPool?.subscribe('mentions-answers', [
{
kinds: [EventKind.reaction],
'#e': notes.map((note) => note.id ?? ''),
},
{
kinds: [EventKind.meta],
authors: missingDataNotes,
},
])
})
}
}
const onRefresh = useCallback(() => {
setRefreshing(true)
relayPool?.unsubscribeAll()
if (relayPool && publicKey) {
calculateInitialNotes().then(() => loadNotes())
}
}, [])
const renderItem: (note: Note) => JSX.Element = (note) => {
return (
<View style={styles.noteCard} key={note.id}>
<NoteCard
note={note}
onPressOptions={() => {
setProfileCardPubKey(note.pubkey)
bottomSheetProfileRef.current?.open()
}}
/>
</View>
)
}
const onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void = (event) => {
if (handleInfinityScroll(event)) {
setPageSize(pageSize + initialPageSize)
}
}
const bottomSheetStyles = React.useMemo(() => {
return {
container: {
backgroundColor: theme.colors.background,
padding: 16,
borderTopRightRadius: 28,
borderTopLeftRadius: 28,
},
draggableIcon: {
backgroundColor: '#000',
},
}
}, [])
return (
<>
{notes && notes.length > 0 && (
<ScrollView
onScroll={onScroll}
horizontal={false}
showsVerticalScrollIndicator={false}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
style={styles.list}
>
{notes.map((note) => renderItem(note))}
{notes.length >= 10 && <ActivityIndicator animating={true} />}
</ScrollView>
)}
<RBSheet
ref={bottomSheetProfileRef}
closeOnDragDown={true}
height={280}
customStyles={bottomSheetStyles}
>
<ProfileCard userPubKey={profileCardPubkey ?? ''} bottomSheetRef={bottomSheetProfileRef} />
</RBSheet>
</>
)
}
const styles = StyleSheet.create({
list: {
padding: 16,
},
noteCard: {
marginBottom: 16,
},
})
export default NotificationsFeed

View File

@ -41,11 +41,14 @@ export const SendPage: React.FC<SendPageProps> = ({ route }) => {
const match = text.match(/@(.*)$/)
const note: Note | undefined = route.params?.note
if (database && match && match?.length > 0) {
let request = getUsers(database, { name: match[1], order: 'contact' })
let request = getUsers(database, { name: match[1], order: 'contact DESC,name ASC' })
if (match[1] === '' && note) {
const taggedPubKeys = getTaggedPubKeys(note)
request = getUsers(database, { includeIds: [...taggedPubKeys, note.pubkey], order: 'contact' })
request = getUsers(database, {
includeIds: [...taggedPubKeys, note.pubkey],
order: 'contact DESC,name ASC',
})
}
request.then((results) => {
@ -153,17 +156,20 @@ export const SendPage: React.FC<SendPageProps> = ({ route }) => {
onChangeText={onChangeText}
/>
</View>
{/* FIXME: can't find this color */}
<View style={styles.actions}>
{/* flexDirection: 'column-reverse' */}
{userSuggestions.length > 0 ? (
<FlatList
style={styles.contactsList}
ItemSeparatorComponent={Divider}
data={userSuggestions}
renderItem={renderContactItem}
/>
// FIXME: can't find this color
<View style={{ backgroundColor: '#001C37' }}>
<FlatList
style={styles.contactsList}
ItemSeparatorComponent={Divider}
data={userSuggestions}
renderItem={renderContactItem}
/>
</View>
) : (
// FIXME: can't find this color
<View style={{ backgroundColor: '#001C37' }}>
<View style={styles.contentWarning}>
<Text>{t('sendPage.contentWarning')}</Text>
@ -204,7 +210,6 @@ const styles = StyleSheet.create({
flexDirection: 'row',
justifyContent: 'space-between',
width: '100%',
backgroundColor: '#001C37', // FIXME: somehow it can't be imported from theme
},
contactName: {
paddingLeft: 16,