2024-07-03 07:46:31 +00:00
|
|
|
import { PublicKey } from "@blowater/nostr-sdk";
|
|
|
|
import { NostrAccountContext, NostrEvent, NostrKind } from "@blowater/nostr-sdk";
|
2023-12-22 12:20:34 +00:00
|
|
|
import { InvalidEvent } from "../features/dm.ts";
|
2024-01-01 17:28:10 +00:00
|
|
|
import { getTags } from "../nostr.ts";
|
2024-01-02 12:42:11 +00:00
|
|
|
import { UserBlocker } from "./app_update.tsx";
|
|
|
|
import { ConversationListRetriever, ConversationType, NewMessageChecker } from "./conversation-list.tsx";
|
2024-07-15 14:36:36 +00:00
|
|
|
import { RelayRecordGetter } from "../database.ts";
|
2024-07-17 10:16:21 +00:00
|
|
|
import { ValueSet } from "@blowater/collections";
|
|
|
|
import { url_identity } from "./_helper.ts";
|
2023-06-30 14:05:57 +00:00
|
|
|
|
2023-09-23 21:54:13 +00:00
|
|
|
export interface ConversationSummary {
|
2024-07-15 14:36:36 +00:00
|
|
|
readonly pubkey: PublicKey;
|
2023-10-15 22:39:21 +00:00
|
|
|
newestEventSendByMe?: NostrEvent;
|
|
|
|
newestEventReceivedByMe?: NostrEvent;
|
2024-07-17 10:16:21 +00:00
|
|
|
relays: ValueSet<URL>;
|
2023-06-30 14:05:57 +00:00
|
|
|
}
|
|
|
|
|
2024-01-02 12:42:11 +00:00
|
|
|
export class DM_List implements ConversationListRetriever, NewMessageChecker, UserBlocker {
|
2023-09-24 21:56:29 +00:00
|
|
|
readonly convoSummaries = new Map<string, ConversationSummary>();
|
2023-12-22 12:20:34 +00:00
|
|
|
readonly newMessages = new Map<string, number>();
|
2023-07-15 16:40:21 +00:00
|
|
|
|
2023-09-23 21:54:13 +00:00
|
|
|
constructor(
|
|
|
|
public readonly ctx: NostrAccountContext,
|
2024-07-15 14:36:36 +00:00
|
|
|
private readonly relayRecordGetter: RelayRecordGetter,
|
2023-09-23 21:54:13 +00:00
|
|
|
) {}
|
2023-07-15 16:40:21 +00:00
|
|
|
|
2024-03-15 13:44:17 +00:00
|
|
|
newNessageCount(pubkey: PublicKey): number {
|
2024-01-02 12:42:11 +00:00
|
|
|
return this.newMessages.get(pubkey.bech32()) || 0;
|
2023-12-22 13:03:59 +00:00
|
|
|
}
|
|
|
|
|
2024-03-15 13:44:17 +00:00
|
|
|
markRead(pubkey: PublicKey): void {
|
2024-01-02 12:42:11 +00:00
|
|
|
this.newMessages.set(pubkey.bech32(), 0);
|
2023-10-07 20:40:18 +00:00
|
|
|
}
|
|
|
|
|
2024-07-15 14:36:36 +00:00
|
|
|
getConversationList = (space?: URL) => {
|
|
|
|
const convs = [];
|
|
|
|
for (const convo of this.convoSummaries.values()) {
|
|
|
|
if (
|
|
|
|
convo.newestEventReceivedByMe != undefined ||
|
|
|
|
convo.newestEventSendByMe != undefined
|
|
|
|
) {
|
|
|
|
if (!space) {
|
|
|
|
convs.push(convo);
|
2024-07-17 10:16:21 +00:00
|
|
|
} else {
|
|
|
|
const recordedByRelay = convo.relays.has(space);
|
|
|
|
if (recordedByRelay) {
|
|
|
|
convs.push(convo);
|
|
|
|
}
|
2024-07-15 14:36:36 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return convs;
|
|
|
|
};
|
|
|
|
|
2023-09-13 14:52:01 +00:00
|
|
|
*getStrangers() {
|
2023-09-27 20:04:01 +00:00
|
|
|
for (const convoSummary of this.convoSummaries.values()) {
|
2023-09-13 14:52:01 +00:00
|
|
|
if (
|
2023-09-27 20:04:01 +00:00
|
|
|
(
|
|
|
|
convoSummary.newestEventReceivedByMe == undefined ||
|
|
|
|
convoSummary.newestEventSendByMe == undefined
|
|
|
|
) &&
|
|
|
|
!(
|
|
|
|
convoSummary.newestEventReceivedByMe == undefined &&
|
|
|
|
convoSummary.newestEventSendByMe == undefined
|
|
|
|
)
|
2023-09-13 14:52:01 +00:00
|
|
|
) {
|
2024-01-02 12:42:11 +00:00
|
|
|
if (this.isUserBlocked(convoSummary.pubkey)) {
|
|
|
|
continue;
|
|
|
|
}
|
2023-09-27 20:04:01 +00:00
|
|
|
yield convoSummary;
|
2023-09-13 14:52:01 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
*getContacts() {
|
2023-09-24 21:56:29 +00:00
|
|
|
for (const userInfo of this.convoSummaries.values()) {
|
2023-09-13 14:52:01 +00:00
|
|
|
if (
|
|
|
|
userInfo.newestEventReceivedByMe != undefined &&
|
|
|
|
userInfo.newestEventSendByMe != undefined
|
|
|
|
) {
|
2024-01-02 12:42:11 +00:00
|
|
|
if (this.isUserBlocked(userInfo.pubkey)) {
|
|
|
|
continue;
|
|
|
|
}
|
2023-09-13 14:52:01 +00:00
|
|
|
yield userInfo;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-15 13:44:17 +00:00
|
|
|
getConversationType(pubkey: PublicKey): ConversationType {
|
2024-01-02 12:42:11 +00:00
|
|
|
const contact = this.convoSummaries.get(pubkey.bech32());
|
2023-10-03 18:25:36 +00:00
|
|
|
if (contact == undefined) {
|
2024-01-02 12:42:11 +00:00
|
|
|
return "strangers";
|
|
|
|
}
|
|
|
|
if (this.isUserBlocked(pubkey)) {
|
|
|
|
return "blocked";
|
2023-10-03 18:25:36 +00:00
|
|
|
}
|
|
|
|
if (
|
|
|
|
contact.newestEventReceivedByMe == undefined || contact.newestEventSendByMe == undefined
|
|
|
|
) {
|
2024-01-02 12:42:11 +00:00
|
|
|
return "strangers";
|
2023-10-03 18:25:36 +00:00
|
|
|
} else {
|
2024-01-02 12:42:11 +00:00
|
|
|
return "contacts";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
*getConversations(keys: Iterable<string>): Iterable<ConversationSummary> {
|
|
|
|
for (const key of keys) {
|
|
|
|
const convo = this.convoSummaries.get(key);
|
|
|
|
if (convo) {
|
|
|
|
yield convo;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////
|
|
|
|
// implement UserBlocker //
|
|
|
|
///////////////////////////
|
|
|
|
blockUser(pubkey: PublicKey): void {
|
|
|
|
let blockedUsers = this.getBlockedUsers();
|
|
|
|
blockedUsers.add(pubkey.bech32());
|
|
|
|
localStorage.setItem("blocked-users", JSON.stringify(Array.from(blockedUsers)));
|
|
|
|
}
|
|
|
|
unblockUser(pubkey: PublicKey): void {
|
|
|
|
let blockedUsers = this.getBlockedUsers();
|
|
|
|
blockedUsers.delete(pubkey.bech32());
|
|
|
|
localStorage.setItem("blocked-users", JSON.stringify(Array.from(blockedUsers)));
|
|
|
|
}
|
2024-03-15 13:44:17 +00:00
|
|
|
isUserBlocked = (pubkey: PublicKey): boolean => {
|
2024-01-02 12:42:11 +00:00
|
|
|
const blockedUsers = this.getBlockedUsers();
|
|
|
|
return blockedUsers.has(pubkey.bech32());
|
2024-03-15 13:44:17 +00:00
|
|
|
};
|
2024-01-02 12:42:11 +00:00
|
|
|
getBlockedUsers() {
|
|
|
|
let blockedUsers: string | null = localStorage.getItem("blocked-users");
|
|
|
|
if (blockedUsers == null) {
|
|
|
|
blockedUsers = "[]";
|
2023-10-03 18:25:36 +00:00
|
|
|
}
|
2024-01-02 12:42:11 +00:00
|
|
|
return new Set(JSON.parse(blockedUsers) as string[]);
|
2023-10-03 18:25:36 +00:00
|
|
|
}
|
2024-01-02 12:42:11 +00:00
|
|
|
// end //
|
|
|
|
/////////
|
2023-10-03 18:25:36 +00:00
|
|
|
|
2023-10-04 15:07:40 +00:00
|
|
|
addEvents(
|
2023-12-17 20:41:49 +00:00
|
|
|
events: NostrEvent[],
|
2023-12-22 13:03:59 +00:00
|
|
|
newEvents: boolean,
|
2023-10-03 15:15:45 +00:00
|
|
|
) {
|
2023-07-15 16:40:21 +00:00
|
|
|
for (const event of events) {
|
2023-12-22 12:20:34 +00:00
|
|
|
if (event.kind != NostrKind.DIRECT_MESSAGE) {
|
|
|
|
continue;
|
|
|
|
}
|
2023-12-22 13:03:59 +00:00
|
|
|
const err = this.addEvent({
|
|
|
|
...event,
|
|
|
|
kind: event.kind,
|
|
|
|
}, newEvents);
|
|
|
|
if (err instanceof Error) {
|
|
|
|
return err;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-12-22 12:20:34 +00:00
|
|
|
|
2023-12-22 13:03:59 +00:00
|
|
|
private addEvent(event: NostrEvent<NostrKind.DIRECT_MESSAGE>, newEvent: boolean) {
|
2024-01-02 12:42:11 +00:00
|
|
|
let pubkey_I_TalkingTo;
|
|
|
|
{
|
|
|
|
let whoAm_I_TalkingTo = "";
|
|
|
|
if (event.pubkey == this.ctx.publicKey.hex) {
|
|
|
|
// I am the sender
|
|
|
|
whoAm_I_TalkingTo = getTags(event).p[0];
|
|
|
|
if (whoAm_I_TalkingTo == undefined) {
|
|
|
|
return new InvalidEvent(event, `event ${event.id} does not have p tags`);
|
|
|
|
}
|
|
|
|
} else if (getTags(event).p[0] == this.ctx.publicKey.hex) {
|
|
|
|
// I am the receiver
|
|
|
|
whoAm_I_TalkingTo = event.pubkey;
|
|
|
|
} else {
|
|
|
|
// I am neither. Possible because other user has used this device before
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
pubkey_I_TalkingTo = PublicKey.FromHex(whoAm_I_TalkingTo);
|
|
|
|
if (pubkey_I_TalkingTo instanceof Error) {
|
|
|
|
return new InvalidEvent(event, pubkey_I_TalkingTo.message);
|
2023-12-22 12:20:34 +00:00
|
|
|
}
|
2023-12-22 13:03:59 +00:00
|
|
|
}
|
2023-12-22 12:20:34 +00:00
|
|
|
|
2023-12-22 13:03:59 +00:00
|
|
|
if (newEvent && this.ctx.publicKey.hex != event.pubkey) {
|
2024-01-02 12:42:11 +00:00
|
|
|
this.newMessages.set(
|
|
|
|
pubkey_I_TalkingTo.bech32(),
|
2024-03-15 13:44:17 +00:00
|
|
|
this.newNessageCount(pubkey_I_TalkingTo) + 1,
|
2024-01-02 12:42:11 +00:00
|
|
|
);
|
2023-12-22 13:03:59 +00:00
|
|
|
}
|
2023-12-22 12:20:34 +00:00
|
|
|
|
2024-01-02 12:42:11 +00:00
|
|
|
const userInfo = this.convoSummaries.get(pubkey_I_TalkingTo.bech32());
|
2023-12-22 13:03:59 +00:00
|
|
|
if (userInfo) {
|
2024-07-15 14:36:36 +00:00
|
|
|
const spaceURLs = this.relayRecordGetter.getRelayRecord(event.id);
|
|
|
|
if (spaceURLs.size > 0) {
|
|
|
|
for (const url of spaceURLs) {
|
2024-07-17 10:16:21 +00:00
|
|
|
userInfo.relays.add(url);
|
2024-07-15 14:36:36 +00:00
|
|
|
}
|
|
|
|
}
|
2024-01-02 12:42:11 +00:00
|
|
|
if (pubkey_I_TalkingTo.hex == this.ctx.publicKey.hex) {
|
2023-12-22 13:03:59 +00:00
|
|
|
// talking to myself
|
|
|
|
if (userInfo.newestEventSendByMe) {
|
|
|
|
if (event.created_at > userInfo.newestEventSendByMe?.created_at) {
|
2023-12-22 12:20:34 +00:00
|
|
|
userInfo.newestEventSendByMe = event;
|
|
|
|
userInfo.newestEventReceivedByMe = event;
|
|
|
|
}
|
|
|
|
} else {
|
2023-12-22 13:03:59 +00:00
|
|
|
userInfo.newestEventSendByMe = event;
|
|
|
|
userInfo.newestEventReceivedByMe = event;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (this.ctx.publicKey.hex == event.pubkey) {
|
|
|
|
// I am the sender
|
|
|
|
if (userInfo.newestEventSendByMe) {
|
|
|
|
if (event.created_at > userInfo.newestEventSendByMe.created_at) {
|
2023-12-22 12:20:34 +00:00
|
|
|
userInfo.newestEventSendByMe = event;
|
2023-06-30 14:05:57 +00:00
|
|
|
}
|
2023-12-22 12:20:34 +00:00
|
|
|
} else {
|
2023-12-22 13:03:59 +00:00
|
|
|
userInfo.newestEventSendByMe = event;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// I am the receiver
|
|
|
|
if (userInfo.newestEventReceivedByMe) {
|
|
|
|
if (event.created_at > userInfo.newestEventReceivedByMe.created_at) {
|
2023-12-22 12:20:34 +00:00
|
|
|
userInfo.newestEventReceivedByMe = event;
|
2023-06-30 14:05:57 +00:00
|
|
|
}
|
2023-12-22 13:03:59 +00:00
|
|
|
} else {
|
|
|
|
userInfo.newestEventReceivedByMe = event;
|
2023-06-30 14:05:57 +00:00
|
|
|
}
|
2023-12-22 12:20:34 +00:00
|
|
|
}
|
2023-12-22 13:03:59 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
const newUserInfo: ConversationSummary = {
|
2024-01-02 12:42:11 +00:00
|
|
|
pubkey: pubkey_I_TalkingTo,
|
2023-12-22 13:03:59 +00:00
|
|
|
newestEventReceivedByMe: undefined,
|
|
|
|
newestEventSendByMe: undefined,
|
2024-07-17 10:16:21 +00:00
|
|
|
relays: new ValueSet(url_identity),
|
2023-12-22 13:03:59 +00:00
|
|
|
};
|
2024-07-15 14:36:36 +00:00
|
|
|
const spaceURLs = this.relayRecordGetter.getRelayRecord(event.id);
|
|
|
|
if (spaceURLs.size > 0) {
|
|
|
|
for (const url of spaceURLs) {
|
2024-07-17 10:16:21 +00:00
|
|
|
newUserInfo.relays.add(url);
|
2024-07-15 14:36:36 +00:00
|
|
|
}
|
|
|
|
}
|
2024-01-02 12:42:11 +00:00
|
|
|
if (pubkey_I_TalkingTo.hex == this.ctx.publicKey.hex) {
|
2023-12-22 13:03:59 +00:00
|
|
|
// talking to myself
|
|
|
|
newUserInfo.newestEventSendByMe = event;
|
|
|
|
newUserInfo.newestEventReceivedByMe = event;
|
2023-12-22 12:20:34 +00:00
|
|
|
} else {
|
2023-12-22 13:03:59 +00:00
|
|
|
if (this.ctx.publicKey.hex == event.pubkey) {
|
|
|
|
// I am the sender
|
2023-12-22 12:20:34 +00:00
|
|
|
newUserInfo.newestEventSendByMe = event;
|
|
|
|
} else {
|
2023-12-22 13:03:59 +00:00
|
|
|
// I am the receiver
|
|
|
|
newUserInfo.newestEventReceivedByMe = event;
|
2023-12-22 12:20:34 +00:00
|
|
|
}
|
2023-06-30 14:05:57 +00:00
|
|
|
}
|
2024-01-02 12:42:11 +00:00
|
|
|
this.convoSummaries.set(pubkey_I_TalkingTo.bech32(), newUserInfo);
|
2023-06-30 14:05:57 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-23 21:54:13 +00:00
|
|
|
export const sortUserInfo = (a: ConversationSummary, b: ConversationSummary) => {
|
2023-06-30 14:05:57 +00:00
|
|
|
return sortScore(b) - sortScore(a);
|
|
|
|
};
|
|
|
|
|
2023-09-23 21:54:13 +00:00
|
|
|
function sortScore(contact: ConversationSummary) {
|
2023-06-30 14:05:57 +00:00
|
|
|
let score = 0;
|
|
|
|
if (contact.newestEventSendByMe !== undefined) {
|
|
|
|
score += contact.newestEventSendByMe.created_at;
|
|
|
|
}
|
|
|
|
if (contact.newestEventReceivedByMe !== undefined) {
|
|
|
|
score += contact.newestEventReceivedByMe.created_at;
|
|
|
|
}
|
|
|
|
return score;
|
|
|
|
}
|