Reactions (#85)

This commit is contained in:
KoalaSat 2023-01-02 16:00:56 +00:00 committed by GitHub
commit 57d26ca337
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 241 additions and 53 deletions

View File

@ -47,6 +47,8 @@ public class Event {
}
} else if (kind.equals("4")) {
saveDirectMessage(database);
} else if (kind.equals("7")) {
saveReaction(database);
}
} catch (JSONException e) {
e.printStackTrace();
@ -99,11 +101,11 @@ public class Event {
}
protected Boolean getUserMentioned(String userPubKey) {
JSONArray eTags = filterTags("p");
JSONArray pTags = filterTags("p");
Boolean userMentioned = false;
try {
for (int i = 0; i < eTags.length(); ++i) {
JSONArray tag = eTags.getJSONArray(i);
for (int i = 0; i < pTags.length(); ++i) {
JSONArray tag = pTags.getJSONArray(i);
if (tag.getString(1).equals(userPubKey)) {
userMentioned = true;
}
@ -115,26 +117,6 @@ public class Event {
return userMentioned;
}
protected String saveFollower(String pubKey) {
JSONArray eTags = filterTags("p");
String mainEventId = null;
try {
for (int i = 0; i < eTags.length(); ++i) {
JSONArray tag = eTags.getJSONArray(i);
if (tag.getString(3).equals("reply")) {
mainEventId = tag.getString(1);
}
}
if (mainEventId == null && eTags.length() > 0) {
mainEventId = eTags.getJSONArray(eTags.length() - 1).getString(1);
}
} catch (JSONException e) {
e.printStackTrace();
}
return mainEventId;
}
protected JSONArray filterTags(String kind) {
JSONArray filtered = new JSONArray();
@ -190,13 +172,29 @@ public class Event {
@SuppressLint("Recycle") Cursor cursor = database.rawQuery(query, new String[] {id});
if (cursor.getCount() == 0) {
database.insert("nostros_direct_messages", null, values);
} else {
String whereClause = "id = ?";
String[] whereArgs = new String[] {
id
};
values.put("read", cursor.getInt(0));
database.update("nostros_direct_messages", values, whereClause, whereArgs);
}
}
protected void saveReaction(SQLiteDatabase database) throws JSONException {
JSONArray pTags = filterTags("p");
JSONArray eTags = filterTags("e");
ContentValues values = new ContentValues();
values.put("id", id);
values.put("content", content.replace("'", "''"));
values.put("created_at", created_at);
values.put("kind", kind);
values.put("pubkey", pubkey);
values.put("sig", sig);
values.put("tags", tags.toString());
values.put("positive", !content.equals("-"));
values.put("reacted_event_id", eTags.getJSONArray(eTags.length() - 1).getString(1));
values.put("reacted_user_id", pTags.getJSONArray(pTags.length() - 1).getString(1));
String query = "SELECT id FROM nostros_reactions WHERE id = ?";
@SuppressLint("Recycle") Cursor cursor = database.rawQuery(query, new String[] {id});
if (cursor.getCount() == 0) {
database.insert("nostros_reactions", null, values);
}
}

View File

@ -68,6 +68,18 @@ public class DatabaseModule {
try {
database.execSQL("ALTER TABLE nostros_users ADD COLUMN created_at INT DEFAULT 0;");
} catch (SQLException e) { }
database.execSQL("CREATE TABLE IF NOT EXISTS nostros_reactions(\n" +
" id TEXT PRIMARY KEY NOT NULL, \n" +
" content TEXT NOT NULL,\n" +
" created_at INT NOT NULL,\n" +
" kind INT NOT NULL,\n" +
" pubkey TEXT NOT NULL,\n" +
" sig TEXT NOT NULL,\n" +
" tags TEXT NOT NULL,\n" +
" positive BOOLEAN DEFAULT TRUE,\n" +
" reacted_event_id TEXT,\n" +
" reacted_user_id TEXT\n" +
" );");
}
public void saveEvent(JSONObject data, String userPubKey) throws JSONException {

View File

@ -13,6 +13,7 @@ import {
getReplyEventId,
isContentWarning,
} from '../../Functions/RelayFunctions/Events'
import { Event } from '../../../lib/nostr/Events'
import moment from 'moment'
import { populateRelay } from '../../Functions/RelayFunctions'
import Avatar from '../Avatar'
@ -20,6 +21,7 @@ import { searchRelays } from '../../Functions/DatabaseFunctions/Relays'
import { RelayFilters } from '../../lib/nostr/RelayPool/intex'
import TextContent from '../../Components/TextContent'
import { formatPubKey } from '../../Functions/RelayFunctions/Users'
import { getReactionsCount, getUserReaction } from '../../Functions/DatabaseFunctions/Reactions'
interface NoteCardProps {
note: Note
@ -33,12 +35,36 @@ export const NoteCard: React.FC<NoteCardProps> = ({
onlyContactsReplies = false,
}) => {
const theme = useTheme()
const { relayPool, publicKey } = useContext(RelayPoolContext)
const { relayPool, publicKey, lastEventId } = useContext(RelayPoolContext)
const { database, goToPage } = useContext(AppContext)
const [relayAdded, setRelayAdded] = useState<boolean>(false)
const [replies, setReplies] = useState<Note[]>([])
const [positiveReactions, setPositiveReactions] = useState<number>(0)
const [negaiveReactions, setNegativeReactions] = useState<number>(0)
const [userUpvoted, setUserUpvoted] = useState<boolean>(false)
const [userDownvoted, setUserDownvoted] = useState<boolean>(false)
const [hide, setHide] = useState<boolean>(isContentWarning(note))
useEffect(() => {
if (database && publicKey && note.id) {
getReactionsCount(database, { positive: true, eventId: note.id }).then((result) => {
setPositiveReactions(result ?? 0)
})
getReactionsCount(database, { positive: false, eventId: note.id }).then((result) => {
setNegativeReactions(result ?? 0)
})
getUserReaction(database, publicKey, { eventId: note.id }).then((results) => {
results.forEach((reaction) => {
if (reaction.positive) {
setUserUpvoted(true)
} else {
setUserDownvoted(true)
}
})
})
}
}, [lastEventId])
useEffect(() => {
if (database && note) {
searchRelays(note.content, database).then((result) => {
@ -63,6 +89,17 @@ export const NoteCard: React.FC<NoteCardProps> = ({
}
}, [database])
const publishReaction: (positive: boolean) => void = (positive) => {
const event: Event = {
content: positive ? '+' : '-',
created_at: moment().unix(),
kind: EventKind.reaction,
pubkey: publicKey,
tags: [...note.tags, ['e', note.id], ['p', note.pubkey]],
}
relayPool?.sendEvent(event)
}
const textNote: (note: Note) => JSX.Element = (note) => {
return (
<>
@ -99,6 +136,54 @@ export const NoteCard: React.FC<NoteCardProps> = ({
</Layout>
<Layout style={styles.footer}>
<Text appearance='hint'>{moment.unix(note.created_at).format('HH:mm DD-MM-YY')}</Text>
<Layout style={styles.reactions}>
<Button
appearance='ghost'
status={userUpvoted ? 'success' : 'primary'}
onPress={() => {
if (!userUpvoted) {
setUserUpvoted(true)
setPositiveReactions((prev) => prev + 1)
publishReaction(true)
}
}}
accessoryLeft={
<Icon
name='arrow-up'
size={16}
color={userUpvoted ? theme['color-success-500'] : theme['color-primary-500']}
solid
/>
}
size='small'
>
{positiveReactions === undefined || positiveReactions === 0
? '-'
: positiveReactions}
</Button>
<Button
appearance='ghost'
status={userDownvoted ? 'danger' : 'primary'}
onPress={() => {
if (!userDownvoted) {
setUserDownvoted(true)
setNegativeReactions((prev) => prev + 1)
publishReaction(false)
}
}}
accessoryLeft={
<Icon
name='arrow-down'
size={16}
color={userDownvoted ? theme['color-danger-500'] : theme['color-primary-500']}
solid
/>
}
size='small'
>
{negaiveReactions === undefined || negaiveReactions === 0 ? '-' : negaiveReactions}
</Button>
</Layout>
</Layout>
</Layout>
</>
@ -192,6 +277,14 @@ export const NoteCard: React.FC<NoteCardProps> = ({
},
footer: {
backgroundColor: 'transparent',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginTop: 10,
},
reactions: {
backgroundColor: 'transparent',
flexDirection: 'row',
},
tags: {
backgroundColor: 'transparent',

View File

@ -112,7 +112,12 @@ export const TextContent: React.FC<TextContentProps> = ({ event, content, previe
{text}
</ParsedText>
{preview && containsUrl() && (
<LinkPreview text={text} renderText={() => ''} textContainerStyle={{ height: 0 }} />
<LinkPreview
text={text}
renderText={() => ''}
textContainerStyle={{ height: 0 }}
enableAnimation={true}
/>
)}
</>
)

View File

@ -0,0 +1,67 @@
import { QuickSQLiteConnection } from 'react-native-quick-sqlite'
import { getItems } from '..'
import { Event } from '../../../lib/nostr/Events'
export interface Reaction extends Event {
positive: boolean
reacted_event_id: string
reacted_user_id: string
}
const databaseToEntity: (object: object) => Reaction = (object) => {
return object as Reaction
}
export const getReactionsCount: (
db: QuickSQLiteConnection,
filters: {
eventId?: string
pubKey?: string
positive: boolean
},
) => Promise<number> = async (db, { eventId, pubKey, positive }) => {
let notesQuery = `
SELECT
COUNT(*)
FROM
nostros_reactions
WHERE positive = ${positive ? '1' : '0'}
`
if (eventId) {
notesQuery += `AND reacted_event_id = "${eventId}" `
} else if (pubKey) {
notesQuery += `AND reacted_user_id = "${pubKey}" `
}
const resultSet = await db.execute(notesQuery)
const item: { 'COUNT(*)': number } = resultSet?.rows?.item(0)
return item['COUNT(*)']
}
export const getUserReaction: (
db: QuickSQLiteConnection,
pubKey: string,
filters: {
eventId?: string
},
) => Promise<Reaction[]> = async (db, pubKey, { eventId }) => {
let notesQuery = `
SELECT
*
FROM
nostros_reactions
WHERE pubkey = '${pubKey}'
`
if (eventId) {
notesQuery += `AND reacted_event_id = "${eventId}" `
}
const resultSet = await db.execute(notesQuery)
const items: object[] = getItems(resultSet)
const reactions: Reaction[] = items.map((object) => databaseToEntity(object))
return reactions
}

View File

@ -22,6 +22,8 @@ export const isContentWarning: (event: Event) => boolean = (event) => {
}
export const isDirectReply: (mainEvent: Event, reply: Event) => boolean = (mainEvent, reply) => {
if (mainEvent.id === reply.id) return false
const taggedMainEventsIds: string[] = getTaggedEventIds(mainEvent)
const taggedReplyEventsIds: string[] = getTaggedEventIds(reply)
const difference = taggedReplyEventsIds.filter((item) => !taggedMainEventsIds.includes(item))

View File

@ -49,7 +49,7 @@ export const HomePage: React.FC = () => {
authors: [...users.map((user) => user.id), publicKey],
}
if (lastNote && lastNotes.length > 0 && !past) {
if (lastNote && lastNotes.length >= pageSize && !past) {
message.since = lastNote.created_at
} else {
message.limit = pageSize + initialPageSize
@ -67,6 +67,10 @@ export const HomePage: React.FC = () => {
kinds: [EventKind.meta],
authors: notes.map((note) => note.pubkey),
})
relayPool?.subscribe('main-channel', {
kinds: [EventKind.reaction],
'#e': notes.map((note) => note.id ?? ''),
})
})
}
}
@ -158,9 +162,11 @@ export const HomePage: React.FC = () => {
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'>

View File

@ -42,6 +42,10 @@ export const MentionsPage: React.FC = () => {
if (database && publicKey) {
getMentionNotes(database, publicKey, pageSize).then((notes) => {
setNotes(notes)
relayPool?.subscribe('main-channel', {
kinds: [EventKind.reaction],
'#e': notes.map((note) => note.id ?? ''),
})
const missingDataNotes = notes
.filter((note) => !note.picture || note.picture === '')
.map((note) => note.pubkey)
@ -124,9 +128,11 @@ export const MentionsPage: React.FC = () => {
{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'>

View File

@ -76,17 +76,10 @@ export const NotePage: React.FC = () => {
kinds: [EventKind.textNote],
ids: [eventId],
})
const notes = await getNotes(database, { filters: { reply_event_id: eventId } })
const eventMessages: RelayFilters = {
kinds: [EventKind.textNote],
relayPool?.subscribe('main-channel', {
kinds: [EventKind.reaction, EventKind.textNote],
'#e': [eventId],
}
if (past) {
eventMessages.until = notes[notes.length - 1]?.created_at
} else {
eventMessages.since = notes[0]?.created_at
}
relayPool?.subscribe('main-channel', eventMessages)
})
}
}

View File

@ -67,6 +67,10 @@ export const ProfilePage: React.FC = () => {
getNotes(database, { filters: { pubkey: userId }, limit: pageSize }).then((results) => {
setNotes(results)
setRefreshing(false)
relayPool?.subscribe('main-channel', {
kinds: [EventKind.reaction],
'#e': results.map((note) => note.id ?? ''),
})
})
}
}
@ -79,7 +83,6 @@ export const ProfilePage: React.FC = () => {
authors: [userId],
limit: pageSize,
}
relayPool?.subscribe('main-channel', message)
}
@ -331,9 +334,11 @@ export const ProfilePage: React.FC = () => {
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
>
{notes.map((note) => itemCard(note))}
{notes.length >= 10 && (
<Layout style={styles.spinner}>
<Spinner size='small' />
</Layout>
)}
</ScrollView>
) : (
<Loading />

View File

@ -16,6 +16,7 @@ export enum EventKind {
recommendServer = 2,
petNames = 3,
directMessage = 4,
reaction = 7,
}
export const serializeEvent: (event: Event) => string = (event) => {