mirror of
https://github.com/KoalaSat/nostros.git
synced 2024-09-29 14:40:43 +00:00
Reactions (#85)
This commit is contained in:
commit
57d26ca337
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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',
|
||||
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
67
frontend/Functions/DatabaseFunctions/Reactions/index.ts
Normal file
67
frontend/Functions/DatabaseFunctions/Reactions/index.ts
Normal 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
|
||||
}
|
@ -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))
|
||||
|
@ -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'>
|
||||
|
@ -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'>
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 />
|
||||
|
@ -16,6 +16,7 @@ export enum EventKind {
|
||||
recommendServer = 2,
|
||||
petNames = 3,
|
||||
directMessage = 4,
|
||||
reaction = 7,
|
||||
}
|
||||
|
||||
export const serializeEvent: (event: Event) => string = (event) => {
|
||||
|
Loading…
Reference in New Issue
Block a user