display reactions (#475)

This commit is contained in:
Bob 2024-06-01 14:57:02 +08:00 committed by GitHub
parent fd50df5d89
commit c9b0db17d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 203 additions and 25 deletions

View File

@ -221,6 +221,39 @@ export class App {
}));
})();
// Sync events since latest event in the database or beginning of time
{
const latestProfile = args.database.getLatestEvent(NostrKind.META_DATA);
const latestPublic = args.database.getLatestEvent(NostrKind.TEXT_NOTE);
const latestDeletion = args.database.getLatestEvent(NostrKind.DELETE);
const latestReaction = args.database.getLatestEvent(NostrKind.REACTION);
// NOTE:
// 48 hours ago is because the latestEvent now does not distinguish space(relay).
// After adding space, it can be subscribed to as the most recent timestamp.
// So adding "hours ago" can at least have some content.
forever(sync_profile_events({
database: args.database,
pool: args.pool,
since: latestProfile ? hours_ago(latestProfile.created_at, 48) : undefined,
}));
forever(sync_public_notes({
pool: args.pool,
database: args.database,
since: latestPublic ? hours_ago(latestPublic.created_at, 48) : undefined,
}));
forever(sync_deletion_events({
pool: args.pool,
database: args.database,
since: latestDeletion ? hours_ago(latestDeletion.created_at, 48) : undefined,
}));
forever(sync_reaction_events({
pool: args.pool,
database: args.database,
since: latestReaction ? hours_ago(latestReaction.created_at, 48) : undefined,
}));
}
const app = new App(
args.database,
args.model,
@ -243,12 +276,9 @@ export class App {
private initApp = async (installPrompt: InstallPrompt) => {
console.log("App.initApp");
// Sync events
// Sync event limit one
{
forever(sync_client_specific_data(this.pool, this.ctx, this.database));
forever(sync_profile_events(this.database, this.pool));
forever(sync_public_notes(this.pool, this.database));
forever(sync_deletion_events(this.pool, this.database));
}
(async () => {
@ -371,6 +401,7 @@ export class AppComponent extends Component<AppProps, {
isUserBlocked: app.conversationLists.isUserBlocked,
getEventByID: app.database.getEventByID,
isAdmin: this.state.isAdmin,
getReactionsByEventID: app.database.getReactionEvents,
}}
userBlocker={app.conversationLists}
/>
@ -398,6 +429,7 @@ export class AppComponent extends Component<AppProps, {
isUserBlocked: app.conversationLists.isUserBlocked,
getEventByID: app.database.getEventByID,
isAdmin: this.state.isAdmin,
getReactionsByEventID: app.database.getReactionEvents,
}}
messages={Array.from(
map(
@ -547,12 +579,16 @@ async function sync_dm_events(
}
async function sync_profile_events(
database: Database_View,
pool: ConnectionPool,
args: {
database: Database_View;
pool: ConnectionPool;
since: number | undefined;
},
) {
const { database, pool, since } = args;
const messageStream = await pool.newSub("sync_profile_events", {
kinds: [NostrKind.META_DATA],
since: hours_ago(12),
since,
});
if (messageStream instanceof Error) {
return messageStream;
@ -567,10 +603,17 @@ async function sync_profile_events(
}
}
const sync_public_notes = async (pool: ConnectionPool, database: Database_View) => {
const sync_public_notes = async (
args: {
pool: ConnectionPool;
database: Database_View;
since: number | undefined;
},
) => {
const { pool, database, since } = args;
const stream = await pool.newSub("sync_public_notes", {
kinds: [NostrKind.TEXT_NOTE, NostrKind.Long_Form],
since: hours_ago(3),
since,
});
if (stream instanceof Error) {
return stream;
@ -613,12 +656,16 @@ const sync_client_specific_data = async (
};
const sync_deletion_events = async (
pool: ConnectionPool,
database: Database_View,
args: {
pool: ConnectionPool;
database: Database_View;
since: number | undefined;
},
) => {
const { pool, database, since } = args;
const stream = await pool.newSub("sync_deletion_events", {
kinds: [NostrKind.DELETE],
since: hours_ago(48),
since,
});
if (stream instanceof Error) {
return stream;
@ -639,6 +686,37 @@ const sync_deletion_events = async (
}
};
export function hours_ago(hours: number) {
return Math.floor(Date.now() / 1000) - hours * 60 * 60;
const sync_reaction_events = async (
args: {
pool: ConnectionPool;
database: Database_View;
since: number | undefined;
},
) => {
const { pool, database, since } = args;
const stream = await pool.newSub("sync_reaction_events", {
kinds: [NostrKind.REACTION],
since,
});
if (stream instanceof Error) {
return stream;
}
for await (const msg of stream.chan) {
if (msg.res.type === "EOSE") {
continue;
} else if (msg.res.type === "NOTICE") {
console.log(`Notice: ${msg.res.note}`);
continue;
}
const ok = await database.addEvent(msg.res.event, msg.url);
if (ok instanceof Error) {
console.error(msg);
console.error(ok);
}
}
};
export function hours_ago(time: number, hours: number) {
return time - hours * 60 * 60;
}

View File

@ -169,18 +169,18 @@ const handle_update_event = async (chan: PutChannel<true>, args: {
continue;
}
const current_relay = pool.getRelay(model.currentRelay);
if (current_relay == undefined) {
console.error(Array.from(pool.getRelays()));
continue;
}
const blowater_relay = pool.getRelay(default_blowater_relay);
if (blowater_relay == undefined) {
console.error(Array.from(pool.getRelays()));
continue;
}
let current_relay = pool.getRelay(model.currentRelay);
if (current_relay == undefined) {
current_relay = blowater_relay;
rememberCurrentRelay(default_blowater_relay);
}
// All events below are only valid after signning in
if (event.type == "SelectRelay") {
rememberCurrentRelay(event.relay.url);

View File

@ -17,7 +17,7 @@ import {
NewMessageChecker,
PinListGetter,
} from "./conversation-list.tsx";
import { func_GetEventByID, func_IsAdmin } from "./message-list.tsx";
import { func_GetEventByID, func_GetReactionsByEventID, func_IsAdmin } from "./message-list.tsx";
export type DM_Model = {
currentConversation: PublicKey | undefined;
@ -37,6 +37,7 @@ type DirectMessageContainerProps = {
isUserBlocked: (pubkey: PublicKey) => boolean;
getEventByID: func_GetEventByID;
isAdmin: func_IsAdmin | undefined;
getReactionsByEventID: func_GetReactionsByEventID;
};
userBlocker: UserBlocker;
} & DM_Model;

View File

@ -55,6 +55,7 @@ interface Props {
relayRecordGetter: RelayRecordGetter;
getEventByID: func_GetEventByID;
isAdmin: func_IsAdmin | undefined;
getReactionsByEventID: func_GetReactionsByEventID;
};
}
@ -282,6 +283,8 @@ export type func_GetEventByID = (
id: string | NoteID,
) => Parsed_Event | undefined;
export type func_GetReactionsByEventID = (id: string) => Set<Parsed_Event>;
function MessageBoxGroup(props: {
authorProfile: ProfileData | undefined;
messages: ChatMessage[];
@ -300,6 +303,7 @@ function MessageBoxGroup(props: {
relayRecordGetter: RelayRecordGetter;
getEventByID: func_GetEventByID;
isAdmin: func_IsAdmin | undefined;
getReactionsByEventID: func_GetReactionsByEventID;
};
}) {
const first_message = props.messages[0];
@ -353,6 +357,10 @@ function MessageBoxGroup(props: {
props.getters,
)}
</pre>
<Reactions
myPublicKey={props.myPublicKey}
events={props.getters.getReactionsByEventID(first_message.event.id)}
/>
</div>
</div>
</li>,
@ -374,7 +382,7 @@ function MessageBoxGroup(props: {
})}
{Time(msg.created_at)}
<div
class={`flex-1`}
className={`flex-1`}
style={{
maxWidth: "calc(100% - 2.75rem)",
}}
@ -384,6 +392,10 @@ function MessageBoxGroup(props: {
>
{ParseMessageContent(msg, props.emit, props.getters)}
</pre>
<Reactions
myPublicKey={props.myPublicKey}
events={props.getters.getReactionsByEventID(msg.event.id)}
/>
</div>
</li>,
);
@ -579,3 +591,55 @@ function ReplyTo(
</div>
);
}
function Reactions(
props: {
myPublicKey: PublicKey;
events: Set<Parsed_Event>;
},
) {
const reactions: Map<string, {
count: number;
clicked: boolean;
}> = new Map();
for (const event of props.events) {
let reaction = event.content;
if (!reaction) continue;
if (reaction === "+") reaction = "👍";
if (reaction === "-") reaction = "👎";
const pre = reactions.get(reaction);
const isClicked = event.pubkey === props.myPublicKey.hex;
if (pre) {
reactions.set(reaction, {
count: pre.count + 1,
clicked: pre.clicked || isClicked,
});
} else {
reactions.set(reaction, {
count: 1,
clicked: isClicked,
});
}
}
return (
reactions.size > 0
? (
<div class={`flex flex-row justify-start items-center gap-1 py-1`}>
{Array.from(reactions).map(([reaction, { count, clicked }]) => {
return (
<div
class={`flex justify-center items-center rounded-full p-1 text-xs text-neutral-400 leading-4 cursor-default ${
clicked ? "bg-neutral-500" : "bg-neutral-700"
}`}
>
<div class={`flex justify-center items-center w-4`}>{reaction}</div>
{count > 1 && <div>{` ${count}`}</div>}
</div>
);
})}
</div>
)
: null
);
}

View File

@ -26,6 +26,7 @@ import { BlockUser, UnblockUser } from "./user-detail.tsx";
import {
DeleteEvent,
func_GetEventByID,
func_GetReactionsByEventID,
func_IsAdmin,
MessageList,
ReplyToMessage,
@ -75,6 +76,7 @@ interface MessagePanelProps {
isUserBlocked: (pubkey: PublicKey) => boolean;
getEventByID: func_GetEventByID;
isAdmin: func_IsAdmin | undefined;
getReactionsByEventID: func_GetReactionsByEventID;
};
}

View File

@ -13,7 +13,7 @@ import { NostrAccountContext } from "../../libs/nostr.ts/nostr.ts";
import { MessagePanel } from "./message-panel.tsx";
import { PublicKey } from "../../libs/nostr.ts/key.ts";
import { ChatMessage } from "./message.ts";
import { func_GetEventByID, func_IsAdmin } from "./message-list.tsx";
import { func_GetEventByID, func_GetReactionsByEventID, func_IsAdmin } from "./message-list.tsx";
import { Filter, FilterContent } from "./filter.tsx";
import { NoteID } from "../../libs/nostr.ts/nip19.ts";
@ -38,6 +38,7 @@ type Props = {
isUserBlocked: func_IsUserBlocked;
getEventByID: func_GetEventByID;
isAdmin: func_IsAdmin | undefined;
getReactionsByEventID: func_GetReactionsByEventID;
};
} & Public_Model;

View File

@ -66,6 +66,11 @@ export class Database_View implements ProfileSetter, ProfileGetter, EventRemover
private readonly caster = csp.multi<{ event: Parsed_Event; relay?: string }>(this.sourceOfChange);
private readonly profiles = new Map<string, Profile_Nostr_Event>();
private readonly deletionEvents = new Map</* event id */ string, /* deletion event */ Parsed_Event>();
private readonly reactionEvents = new Map<
/* event id */ string,
/* reaction events */ Set<Parsed_Event>
>();
private readonly latestEvents = new Map<NostrKind, Parsed_Event>();
private constructor(
private readonly eventsAdapter: EventsAdapter,
@ -126,6 +131,7 @@ export class Database_View implements ProfileSetter, ProfileGetter, EventRemover
new Set(all_removed_events.map((mark) => mark.event_id)),
);
console.log("Datebase_View:New time spent", Date.now() - t);
for (const event of db.events.values()) {
if (event.kind == NostrKind.META_DATA) {
// @ts-ignore
@ -139,9 +145,21 @@ export class Database_View implements ProfileSetter, ProfileGetter, EventRemover
event.parsedTags.e.forEach((event_id) => {
db.deletionEvents.set(event_id, event);
});
} else if (event.kind == NostrKind.REACTION) {
const eventId = event.parsedTags.e[0];
const events = db.reactionEvents.get(event.parsedTags.e[0]) || new Set<Parsed_Event>();
events.add(event);
db.reactionEvents.set(eventId, events);
}
// update latest event
const preLatest = db.latestEvents.get(event.kind);
if (preLatest === undefined || preLatest.created_at < event.created_at) {
db.latestEvents.set(event.kind, event);
}
}
console.log(`Datebase_View:Deletion events size: ${db.deletionEvents.size}`);
console.log(`Datebase_View:Reaction events size: ${db.reactionEvents.size}`);
return db;
}
@ -164,6 +182,10 @@ export class Database_View implements ProfileSetter, ProfileGetter, EventRemover
}
}
getLatestEvent = (kind: NostrKind) => {
return this.latestEvents.get(kind);
};
isDeleted(id: string, admin?: string) {
const deletionEvent = this.deletionEvents.get(id);
if (deletionEvent == undefined) {
@ -177,6 +199,10 @@ export class Database_View implements ProfileSetter, ProfileGetter, EventRemover
deletionEvent.pubkey == admin;
}
getReactionEvents = (id: string) => {
return this.reactionEvents.get(id) || new Set<Parsed_Event>();
};
async remove(id: string): Promise<void> {
this.removedEvents.add(id);
await this.eventMarker.markEvent(id, "removed");
@ -296,6 +322,11 @@ export class Database_View implements ProfileSetter, ProfileGetter, EventRemover
parsedEvent.parsedTags.e.forEach((event_id) => {
this.deletionEvents.set(event_id, parsedEvent);
});
} else if (parsedEvent.kind == NostrKind.REACTION) {
const eventId = parsedEvent.parsedTags.e[0];
const events = this.reactionEvents.get(eventId) || new Set<Parsed_Event>();
events.add(parsedEvent);
this.reactionEvents.set(eventId, events);
}
await this.eventsAdapter.put(event);

View File

@ -27,7 +27,8 @@
"vendor",
"*cov_profile*",
"*tauri*",
"app/UI/assets/"
"app/UI/assets/",
"*/tailwind.js"
],
"indentWidth": 4,
"lineWidth": 110

@ -1 +1 @@
Subproject commit 98b715e219dfe2a956736e239e69dc346dec1301
Subproject commit bc371c443d9846be0d3caec17a1e24d93cd4e6a0