Pull to load lists (#8)

This commit is contained in:
KoalaSat 2022-10-30 16:34:47 +00:00 committed by GitHub
commit 903b770a1c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 262 additions and 138 deletions

View File

@ -24,6 +24,7 @@ module.exports = {
'@typescript-eslint/no-dynamic-delete': 'off',
'@typescript-eslint/no-floating-promises': 'off',
'@typescript-eslint/no-misused-promises': 'off',
'@typescript-eslint/restrict-template-expressions': 'off',
},
settings: {
'import/resolver': {

View File

@ -1,6 +1,6 @@
import { Button, Card, Input, Layout, List, Modal, useTheme } from '@ui-kitten/components';
import React, { useContext, useEffect, useState } from 'react';
import { StyleSheet } from 'react-native';
import { Button, Card, Input, Layout, Modal, useTheme } from '@ui-kitten/components';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { RefreshControl, ScrollView, StyleSheet } from 'react-native';
import ActionButton from 'react-native-action-button';
import { AppContext } from '../../Contexts/AppContext';
import Icon from 'react-native-vector-icons/FontAwesome5';
@ -22,6 +22,7 @@ export const ContactsPage: React.FC = () => {
const { relayPool, publicKey, lastEventId, setLastEventId } = useContext(RelayPoolContext);
const theme = useTheme();
const [users, setUsers] = useState<User[]>([]);
const [refreshing, setRefreshing] = useState(false);
const [showAddContact, setShowAddContant] = useState<boolean>(false);
const [contactInput, setContactInput] = useState<string>();
const { t } = useTranslation('common');
@ -35,26 +36,33 @@ export const ContactsPage: React.FC = () => {
}, [lastEventId]);
useEffect(() => {
relayPool?.unsubscribeAll();
relayPool?.on('event', 'contacts', (relay: Relay, _subId?: string, event?: Event) => {
console.log('CONTACTS PAGE EVENT =======>', relay.url, event);
if (database && event?.id && event.kind === EventKind.petNames) {
insertUserContact(event, database).finally(() => setLastEventId(event?.id ?? ''));
relayPool?.subscribe('main-channel', {
kinds: [EventKind.meta],
authors: event.tags.map((tag) => tagToUser(tag).id),
});
relayPool?.removeOn('event', 'contacts');
}
});
if (publicKey) {
relayPool?.subscribe('main-channel', {
kinds: [EventKind.petNames],
authors: [publicKey],
});
}
subscribeContacts();
}, []);
const subscribeContacts: () => Promise<void> = async () => {
return await new Promise<void>((resolve, _reject) => {
relayPool?.unsubscribeAll();
relayPool?.on('event', 'contacts', (relay: Relay, _subId?: string, event?: Event) => {
console.log('CONTACTS PAGE EVENT =======>', relay.url, event);
if (database && event?.id && event.kind === EventKind.petNames) {
insertUserContact(event, database).finally(() => setLastEventId(event?.id ?? ''));
relayPool?.subscribe('main-channel', {
kinds: [EventKind.meta],
authors: event.tags.map((tag) => tagToUser(tag).id),
});
relayPool?.removeOn('event', 'contacts');
}
});
if (publicKey) {
relayPool?.subscribe('main-channel', {
kinds: [EventKind.petNames],
authors: [publicKey],
});
}
resolve();
});
};
const onPressAddContact: () => void = () => {
if (contactInput && relayPool && database && publicKey) {
addContact(contactInput, database).then(() => {
@ -64,6 +72,12 @@ export const ContactsPage: React.FC = () => {
}
};
const onRefresh = useCallback(() => {
setRefreshing(true);
relayPool?.unsubscribeAll();
subscribeContacts().finally(() => setRefreshing(false));
}, []);
const styles = StyleSheet.create({
container: {
flex: 1,
@ -94,7 +108,13 @@ export const ContactsPage: React.FC = () => {
return (
<>
<Layout style={styles.container} level='3'>
<List data={users} renderItem={(item) => <UserCard user={item.item} />} />
<ScrollView
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
>
{users.map((user) => (
<UserCard user={user} key={user.id} />
))}
</ScrollView>
</Layout>
<Modal
style={styles.modal}

View File

@ -1,7 +1,7 @@
import { Card, Layout, List, useTheme } from '@ui-kitten/components';
import React, { useContext, useEffect, useState } from 'react';
import { Card, Layout, useTheme } from '@ui-kitten/components';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { StyleSheet } from 'react-native';
import { RefreshControl, ScrollView, StyleSheet } from 'react-native';
import { AppContext } from '../../Contexts/AppContext';
import { getNotes, Note } from '../../Functions/DatabaseFunctions/Notes';
import NoteCard from '../NoteCard';
@ -20,37 +20,43 @@ export const HomePage: React.FC = () => {
const theme = useTheme();
const [notes, setNotes] = useState<Note[]>([]);
const [totalContacts, setTotalContacts] = useState<number>(-1);
const [refreshing, setRefreshing] = useState(false);
const { t } = useTranslation('common');
const loadNotes: () => void = () => {
if (database && publicKey) {
getNotes(database, { contacts: true, includeIds: [publicKey], limit: 30 }).then((notes) => {
getNotes(database, { contacts: true, includeIds: [publicKey], limit: 15 }).then((notes) => {
setNotes(notes);
});
}
};
const subscribeNotes: () => void = () => {
if (database && publicKey && relayPool) {
getNotes(database, { limit: 1 }).then((notes) => {
getUsers(database, { contacts: true, includeIds: [publicKey] }).then((users) => {
setTotalContacts(users.length);
let message: RelayFilters = {
kinds: [EventKind.textNote, EventKind.recommendServer],
authors: users.map((user) => user.id),
limit: 20,
};
if (notes.length !== 0) {
message = {
...message,
since: notes[0].created_at,
const subscribeNotes: () => Promise<void> = async () => {
return await new Promise<void>((resolve, reject) => {
if (database && publicKey && relayPool) {
getNotes(database, { limit: 1 }).then((notes) => {
getUsers(database, { contacts: true, includeIds: [publicKey] }).then((users) => {
setTotalContacts(users.length);
let message: RelayFilters = {
kinds: [EventKind.textNote, EventKind.recommendServer],
authors: users.map((user) => user.id),
limit: 15,
};
}
relayPool?.subscribe('main-channel', message);
if (notes.length !== 0) {
message = {
...message,
since: notes[0].created_at,
};
}
relayPool?.subscribe('main-channel', message);
resolve();
});
});
});
}
} else {
reject(new Error('Not Ready'));
}
});
};
useEffect(() => {
@ -66,6 +72,12 @@ export const HomePage: React.FC = () => {
subscribeNotes();
}, [database, publicKey, relayPool]);
const onRefresh = useCallback(() => {
setRefreshing(true);
relayPool?.unsubscribeAll();
subscribeNotes().finally(() => setRefreshing(false));
}, []);
const onPress: (note: Note) => void = (note) => {
if (note.kind !== EventKind.recommendServer) {
const replyEventId = getReplyEventId(note);
@ -79,7 +91,7 @@ export const HomePage: React.FC = () => {
const itemCard: (note: Note) => JSX.Element = (note) => {
return (
<Card onPress={() => onPress(note)}>
<Card onPress={() => onPress(note)} key={note.id ?? ''}>
<NoteCard note={note} />
</Card>
);
@ -101,7 +113,12 @@ export const HomePage: React.FC = () => {
{notes.length === 0 && totalContacts !== 0 ? (
<Loading />
) : (
<List data={notes} renderItem={(item) => itemCard(item.item)} />
<ScrollView
horizontal={false}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
>
{notes.map((note) => itemCard(note))}
</ScrollView>
)}
</Layout>
<ActionButton

View File

@ -2,16 +2,19 @@ import React, { useContext } from 'react';
import { BottomNavigation, BottomNavigationTab, useTheme } from '@ui-kitten/components';
import { AppContext } from '../../Contexts/AppContext';
import Icon from 'react-native-vector-icons/FontAwesome5';
import { RelayPoolContext } from '../../Contexts/RelayPoolContext';
export const NavigationBar: React.FC = () => {
const { goToPage, page } = useContext(AppContext);
const { publicKey } = useContext(RelayPoolContext);
const theme = useTheme();
const profilePage = `profile#${publicKey ?? ''}`;
const pageIndex: string[] = ['home', 'contacts', 'profile'];
const pageIndex: string[] = ['home', 'contacts', profilePage];
const getIndex: () => number = () => {
if (page.includes('profile')) {
return !page.includes('profile#') ? 2 : 1;
return page === profilePage ? 2 : 1;
} else if (page.includes('note#')) {
return 0;
} else {

View File

@ -1,12 +1,5 @@
import {
Card,
Layout,
List,
TopNavigation,
TopNavigationAction,
useTheme,
} from '@ui-kitten/components';
import React, { useContext, useEffect, useState } from 'react';
import { Card, Layout, TopNavigation, TopNavigationAction, useTheme } from '@ui-kitten/components';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { AppContext } from '../../Contexts/AppContext';
import { getNotes, Note } from '../../Functions/DatabaseFunctions/Notes';
import { RelayPoolContext } from '../../Contexts/RelayPoolContext';
@ -14,7 +7,7 @@ import Icon from 'react-native-vector-icons/FontAwesome5';
import NoteCard from '../NoteCard';
import { EventKind } from '../../lib/nostr/Events';
import { RelayFilters } from '../../lib/nostr/Relay';
import { StyleSheet } from 'react-native';
import { RefreshControl, ScrollView, StyleSheet } from 'react-native';
import Loading from '../Loading';
import ActionButton from 'react-native-action-button';
import { useTranslation } from 'react-i18next';
@ -25,6 +18,7 @@ export const NotePage: React.FC = () => {
const { lastEventId, relayPool } = useContext(RelayPoolContext);
const [note, setNote] = useState<Note>();
const [replies, setReplies] = useState<Note[]>();
const [refreshing, setRefreshing] = useState(false);
const theme = useTheme();
const { t } = useTranslation('common');
const breadcrump = page.split('%');
@ -43,33 +37,7 @@ export const NotePage: React.FC = () => {
useEffect(reload, []);
useEffect(() => {
if (database) {
getNotes(database, { filters: { id: eventId } }).then((events) => {
if (events.length > 0) {
const event = events[0];
setNote(event);
if (!replies) {
relayPool?.subscribe('main-channel', {
kinds: [EventKind.textNote],
'#e': [eventId],
});
}
getNotes(database, { filters: { reply_event_id: eventId } }).then((notes) => {
const rootReplies = getDirectReplies(event, notes);
if (rootReplies.length > 0) {
setReplies(rootReplies as Note[]);
const message: RelayFilters = {
kinds: [EventKind.meta],
authors: [...rootReplies.map((note) => note.pubkey), event.pubkey],
};
relayPool?.subscribe('main-channel', message);
} else {
setReplies([]);
}
});
}
});
}
subscribeNotes();
}, [lastEventId, page]);
const onPressBack: () => void = () => {
@ -119,16 +87,16 @@ export const NotePage: React.FC = () => {
}
};
const ItemCard: (note?: Note) => JSX.Element = (note) => {
const itemCard: (note?: Note) => JSX.Element = (note) => {
if (note?.id === eventId) {
return (
<Layout style={styles.main} level='2'>
<Layout style={styles.main} level='2' key={note.id ?? ''}>
<NoteCard note={note} />
</Layout>
);
} else if (note) {
return (
<Card onPress={() => onPressNote(note)}>
<Card onPress={() => onPressNote(note)} key={note.id ?? ''}>
<NoteCard note={note} />
</Card>
);
@ -137,6 +105,49 @@ export const NotePage: React.FC = () => {
}
};
const subscribeNotes: () => Promise<void> = async () => {
return await new Promise<void>((resolve, reject) => {
if (database) {
getNotes(database, { filters: { id: eventId } }).then((events) => {
if (events.length > 0) {
const event = events[0];
setNote(event);
if (!replies) {
relayPool?.subscribe('main-channel', {
kinds: [EventKind.textNote],
'#e': [eventId],
});
}
getNotes(database, { filters: { reply_event_id: eventId } }).then((notes) => {
const rootReplies = getDirectReplies(event, notes);
if (rootReplies.length > 0) {
setReplies(rootReplies as Note[]);
const message: RelayFilters = {
kinds: [EventKind.meta],
authors: [...rootReplies.map((note) => note.pubkey), event.pubkey],
};
relayPool?.subscribe('main-channel', message);
} else {
setReplies([]);
}
resolve();
});
} else {
resolve();
}
});
} else {
reject(new Error('Not Ready'));
}
});
};
const onRefresh = useCallback(() => {
setRefreshing(true);
relayPool?.unsubscribeAll();
subscribeNotes().finally(() => setRefreshing(false));
}, []);
const styles = StyleSheet.create({
main: {
paddingBottom: 32,
@ -159,7 +170,11 @@ export const NotePage: React.FC = () => {
/>
<Layout level='4'>
{note ? (
<List data={[note, ...(replies ?? [])]} renderItem={(item) => ItemCard(item?.item)} />
<ScrollView
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
>
{[note, ...(replies ?? [])].map((note) => itemCard(note))}
</ScrollView>
) : (
<Loading style={styles.loading} />
)}

View File

@ -1,15 +1,14 @@
import {
Card,
Layout,
List,
Spinner,
Text,
TopNavigation,
TopNavigationAction,
useTheme,
} from '@ui-kitten/components';
import React, { useContext, useEffect, useState } from 'react';
import { StyleSheet, TouchableOpacity } from 'react-native';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { RefreshControl, ScrollView, StyleSheet, TouchableOpacity } from 'react-native';
import { AppContext } from '../../Contexts/AppContext';
import UserAvatar from 'react-native-user-avatar';
import { getNotes, Note } from '../../Functions/DatabaseFunctions/Notes';
@ -39,50 +38,19 @@ export const ProfilePage: React.FC = () => {
const [notes, setNotes] = useState<Note[]>();
const { t } = useTranslation('common');
const [user, setUser] = useState<User>();
const [contacts, setContactsIds] = useState<string[]>();
const [contactsIds, setContactsIds] = useState<string[]>();
const [isContact, setIsContact] = useState<boolean>();
const [refreshing, setRefreshing] = useState(false);
const breadcrump = page.split('%');
const userId = breadcrump[breadcrump.length - 1].split('#')[1] ?? publicKey;
const username = user?.name === '' ? user?.id : user?.name;
useEffect(() => {
setContactsIds(undefined);
setNotes(undefined);
setUser(undefined);
relayPool?.subscribe('main-channel', {
kinds: [EventKind.meta, EventKind.petNames],
authors: [userId],
});
relayPool?.on('event', 'profile', (_relay: Relay, _subId?: string, event?: Event) => {
console.log('PROFILE EVENT =======>', event);
if (database) {
if (event?.id && event.pubkey === userId) {
if (event.kind === EventKind.petNames) {
const ids = event.tags.map((tag) => tagToUser(tag).id);
setContactsIds(ids);
} else if (event.kind === EventKind.meta) {
storeEvent(event, database).finally(() => {
if (event?.id) setLastEventId(event.id);
});
}
}
getNotes(database, { filters: { pubkey: userId }, limit: 1 }).then((results) => {
if (results) {
const notesEvent: RelayFilters = {
kinds: [EventKind.textNote, EventKind.recommendServer],
authors: [userId],
limit: 15,
};
if (results.length >= 15) {
notesEvent.since = results[0]?.created_at;
}
relayPool?.subscribe('main-channel', notesEvent);
}
});
}
});
}, [page, relayPool]);
loadProfile();
}, [page]);
useEffect(() => {
if (database) {
@ -97,11 +65,11 @@ export const ProfilePage: React.FC = () => {
setContactsIds(users.map((user) => user.id));
});
}
getNotes(database, { filters: { pubkey: userId } }).then((results) => {
getNotes(database, { filters: { pubkey: userId }, limit: 10 }).then((results) => {
if (results.length > 0) setNotes(results);
});
}
}, [lastEventId, database]);
}, [lastEventId]);
const removeAuthor: () => void = () => {
if (relayPool && database && publicKey) {
@ -171,6 +139,60 @@ export const ProfilePage: React.FC = () => {
}
};
const onRefresh = useCallback(() => {
setRefreshing(true);
relayPool?.unsubscribeAll();
loadProfile().finally(() => setRefreshing(false));
}, []);
const subscribeNotes: () => void = () => {
if (database) {
getNotes(database, { filters: { pubkey: userId }, limit: 10 }).then((results) => {
if (results) {
const notesEvent: RelayFilters = {
kinds: [EventKind.textNote, EventKind.recommendServer],
authors: [userId],
limit: 10,
};
if (results.length >= 10) {
notesEvent.since = results[0]?.created_at;
}
relayPool?.subscribe('main-channel', notesEvent);
}
});
}
};
const loadProfile: () => Promise<void> = async () => {
return await new Promise<void>((resolve, reject) => {
relayPool?.subscribe('main-channel', {
kinds: [EventKind.meta, EventKind.petNames],
authors: [userId],
});
relayPool?.on('event', 'profile', (_relay: Relay, _subId?: string, event?: Event) => {
console.log('PROFILE EVENT =======>', event);
if (database) {
if (event?.id && event.pubkey === userId) {
if (event.kind === EventKind.petNames) {
const ids = event.tags.map((tag) => tagToUser(tag).id);
setContactsIds(ids);
} else if (event.kind === EventKind.meta) {
storeEvent(event, database).finally(() => {
if (event?.id) setLastEventId(event.id);
});
}
subscribeNotes();
}
} else {
reject(new Error('Not Ready'));
}
});
resolve();
});
};
const styles = StyleSheet.create({
list: {
flex: 1,
@ -204,9 +226,11 @@ export const ProfilePage: React.FC = () => {
},
stats: {
flex: 1,
},
statsItem: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
marginBottom: 5,
},
description: {
marginTop: 16,
@ -216,7 +240,7 @@ export const ProfilePage: React.FC = () => {
const itemCard: (note: Note) => JSX.Element = (note) => {
return (
<Card onPress={() => onPressNote(note)}>
<Card onPress={() => onPressNote(note)} key={note.id ?? ''}>
<NoteCard note={note} />
</Card>
);
@ -238,6 +262,44 @@ export const ProfilePage: React.FC = () => {
// Clipboard.setString(user?.id ?? '');
};
const isFollowingUser: () => boolean = () => {
if (contactsIds !== undefined && publicKey) {
return contactsIds?.includes(publicKey);
}
return false;
};
const stats: () => JSX.Element = () => {
if (contactsIds === undefined) {
return (
<Layout style={styles.stats} level='3'>
<Spinner size='tiny' />
</Layout>
);
}
return (
<Layout style={styles.stats} level='3'>
<Layout style={styles.statsItem} level='3'>
<Icon name='address-book' size={16} color={theme['text-basic-color']} solid />
<Text>{` ${contactsIds?.length}`}</Text>
</Layout>
{publicKey !== userId && (
<Layout style={styles.statsItem} level='3'>
<Text>
<Icon
name='people-arrows'
size={16}
color={theme[isFollowingUser() ? 'text-basic-color' : 'color-primary-disabled']}
solid
/>
</Text>
</Layout>
)}
</Layout>
);
};
const profile: JSX.Element = (
<Layout style={styles.profile} level='3'>
<Layout style={styles.avatar} level='3'>
@ -258,10 +320,7 @@ export const ProfilePage: React.FC = () => {
<Layout style={styles.description} level='3'>
{user && (
<>
<Layout style={styles.stats} level='3'>
<Text>{contacts?.length ?? <Spinner size='tiny' />} </Text>
<Icon name='address-book' size={16} color={theme['text-basic-color']} solid />
</Layout>
{stats()}
<Layout style={styles.about} level='3'>
<Text numberOfLines={5} ellipsizeMode='tail'>
{user?.about}
@ -283,7 +342,15 @@ export const ProfilePage: React.FC = () => {
/>
{profile}
<Layout style={styles.list} level='3'>
{notes ? <List data={notes} renderItem={(item) => itemCard(item.item)} /> : <Loading />}
{notes ? (
<ScrollView
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
>
{notes.map((note) => itemCard(note))}
</ScrollView>
) : (
<Loading />
)}
</Layout>
{publicKey === userId && (
<ActionButton

View File

@ -3,6 +3,7 @@ import { initReactI18next } from 'react-i18next';
import en from './Locales/en.json';
i18n.use(initReactI18next).init({
compatibilityJSON: 'v3',
fallbackLng: 'en',
resources: {
en,