mirror of
https://github.com/BlowaterNostr/blowater.git
synced 2024-10-18 07:33:22 +00:00
display reactions (#475)
This commit is contained in:
parent
fd50df5d89
commit
c9b0db17d4
106
app/UI/app.tsx
106
app/UI/app.tsx
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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
|
Loading…
Reference in New Issue
Block a user