Bring Back Pin Conversation List(#205)

without CRDT yet
This commit is contained in:
BlowaterNostr 2023-10-01 22:21:55 +00:00 committed by GitHub
parent b8616ed8bf
commit 1cf773d27c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 110 additions and 37 deletions

View File

@ -27,6 +27,7 @@ import { ProfileSyncer } from "../features/profile.ts";
import { Popover, PopOverInputChannel } from "./components/popover.tsx"; import { Popover, PopOverInputChannel } from "./components/popover.tsx";
import { Channel } from "https://raw.githubusercontent.com/BlowaterNostr/csp/master/csp.ts"; import { Channel } from "https://raw.githubusercontent.com/BlowaterNostr/csp/master/csp.ts";
import { GroupChatController } from "../group-chat.ts"; import { GroupChatController } from "../group-chat.ts";
import { OtherConfig } from "./config-other.ts";
export async function Start(database: DexieDatabase) { export async function Start(database: DexieDatabase) {
console.log("Start the application"); console.log("Start the application");
@ -92,6 +93,7 @@ export class App {
public readonly conversationLists: ConversationLists; public readonly conversationLists: ConversationLists;
public readonly relayConfig: RelayConfig; public readonly relayConfig: RelayConfig;
public readonly groupChatController: GroupChatController; public readonly groupChatController: GroupChatController;
public readonly otherConfig: OtherConfig = new OtherConfig();
constructor( constructor(
public readonly database: Database_Contextual_View, public readonly database: Database_Contextual_View,
@ -155,6 +157,8 @@ export class App {
} }
})(); })();
this.otherConfig.syncFromRelay(this.pool, this.ctx);
// create group synchronization // create group synchronization
(async () => { (async () => {
const stream = await this.pool.newSub("group creations", { const stream = await this.pool.newSub("group creations", {
@ -382,6 +386,7 @@ export function AppComponent(props: {
allUserInfo: app.conversationLists, allUserInfo: app.conversationLists,
profilesSyncer: app.profileSyncer, profilesSyncer: app.profileSyncer,
eventSyncer: app.eventSyncer, eventSyncer: app.eventSyncer,
pinListGetter: app.otherConfig,
})} })}
</div> </div>
); );

View File

@ -31,10 +31,10 @@ import {
DirectedMessage_Event, DirectedMessage_Event,
Encrypted_Event, Encrypted_Event,
getTags, getTags,
PinContact, PinConversation,
Profile_Nostr_Event, Profile_Nostr_Event,
Text_Note_Event, Text_Note_Event,
UnpinContact, UnpinConversation,
} from "../nostr.ts"; } from "../nostr.ts";
import { MessageThread } from "./dm.tsx"; import { MessageThread } from "./dm.tsx";
import { DexieDatabase } from "./dexie-db.ts"; import { DexieDatabase } from "./dexie-db.ts";
@ -58,8 +58,8 @@ export type UI_Interaction_Event =
| DirectMessagePanelUpdate | DirectMessagePanelUpdate
| BackToContactList | BackToContactList
| MyProfileUpdate | MyProfileUpdate
| PinContact | PinConversation
| UnpinContact | UnpinConversation
| SignInEvent | SignInEvent
| RelayConfigChange | RelayConfigChange
| CreateGroupChat | CreateGroupChat
@ -194,8 +194,20 @@ export async function* UI_Interaction_Update(args: {
model.dm.currentSelectedContact = undefined; model.dm.currentSelectedContact = undefined;
} else if (event.type == "SelectConversationType") { } else if (event.type == "SelectConversationType") {
model.dm.selectedContactGroup = event.group; model.dm.selectedContactGroup = event.group;
} else if (event.type == "PinContact" || event.type == "UnpinContact") { } else if (event.type == "PinConversation") {
console.log("todo: handle", event.type); app.otherConfig.addPin(event.pubkey);
const err = app.otherConfig.saveToRelay(pool, app.ctx);
if (err instanceof Error) {
console.error(err);
continue;
}
} else if (event.type == "UnpinConversation") {
app.otherConfig.removePin(event.pubkey);
const err = app.otherConfig.saveToRelay(pool, app.ctx);
if (err instanceof Error) {
console.error(err);
continue;
}
} // } //
// //
// Editor // Editor

View File

@ -7,7 +7,7 @@ import { PrivateKey } from "../lib/nostr-ts/key.ts";
Deno.test("Other Configs", async () => { Deno.test("Other Configs", async () => {
{ {
const config = OtherConfig.Empty(); const config = OtherConfig.Empty();
assertEquals(config.pinList, new Set()); assertEquals(config.getPinList(), new Set());
} }
const ctx = InMemoryAccountContext.Generate(); const ctx = InMemoryAccountContext.Generate();
@ -25,7 +25,7 @@ Deno.test("Other Configs", async () => {
}); });
const config = await OtherConfig.FromNostrEvent(event, ctx); const config = await OtherConfig.FromNostrEvent(event, ctx);
if (config instanceof Error) fail(config.message); if (config instanceof Error) fail(config.message);
assertEquals(config.pinList, new Set([pub.bech32(), pub2.bech32()])); assertEquals(config.getPinList(), new Set([pub.hex, pub2.hex]));
// encode back to events // encode back to events
const event_2 = await config.toNostrEvent(ctx); const event_2 = await config.toNostrEvent(ctx);
@ -33,6 +33,6 @@ Deno.test("Other Configs", async () => {
const config_2 = await OtherConfig.FromNostrEvent(event_2, ctx); const config_2 = await OtherConfig.FromNostrEvent(event_2, ctx);
if (config_2 instanceof Error) fail(config_2.message); if (config_2 instanceof Error) fail(config_2.message);
assertEquals(config.pinList, config_2.pinList); assertEquals(config.getPinList(), config_2.getPinList());
} }
}); });

View File

@ -1,13 +1,26 @@
import { prepareParameterizedEvent } from "../lib/nostr-ts/event.ts"; import { prepareParameterizedEvent } from "../lib/nostr-ts/event.ts";
import { PublicKey } from "../lib/nostr-ts/key.ts"; import { PublicKey } from "../lib/nostr-ts/key.ts";
import { NostrAccountContext, NostrEvent, NostrKind } from "../lib/nostr-ts/nostr.ts"; import { NostrAccountContext, NostrEvent, NostrKind } from "../lib/nostr-ts/nostr.ts";
import { ConnectionPool } from "../lib/nostr-ts/relay.ts";
import { PinListGetter } from "./conversation-list.tsx";
export class OtherConfig { export class OtherConfig implements PinListGetter {
static Empty() { static Empty() {
return new OtherConfig(); return new OtherConfig();
} }
readonly pinList = new Set<string>(); // set of pubkeys in npub format private pinList = new Set<string>(); // set of pubkeys in npub format
getPinList(): Set<string> {
return this.pinList;
}
addPin(pubkey: string) {
this.pinList.add(pubkey);
}
removePin(pubkey: string) {
this.pinList.delete(pubkey);
}
static async FromNostrEvent(event: NostrEvent<NostrKind.Custom_App_Data>, ctx: NostrAccountContext) { static async FromNostrEvent(event: NostrEvent<NostrKind.Custom_App_Data>, ctx: NostrAccountContext) {
const decrypted = await ctx.decrypt(ctx.publicKey.hex, event.content); const decrypted = await ctx.decrypt(ctx.publicKey.hex, event.content);
@ -17,11 +30,11 @@ export class OtherConfig {
const pinList = JSON.parse(decrypted); const pinList = JSON.parse(decrypted);
const c = new OtherConfig(); const c = new OtherConfig();
for (const pin of pinList) { for (const pin of pinList) {
const pubkey = PublicKey.FromBech32(pin); const pubkey = PublicKey.FromString(pin);
if (pubkey instanceof Error) { if (pubkey instanceof Error) {
continue; continue;
} }
c.pinList.add(pubkey.bech32()); c.pinList.add(pubkey.hex);
} }
return c; return c;
} }
@ -38,8 +51,46 @@ export class OtherConfig {
content: encryptedContent, content: encryptedContent,
d: OtherConfig.name, d: OtherConfig.name,
kind: NostrKind.Custom_App_Data, kind: NostrKind.Custom_App_Data,
created_at: Date.now() / 1000,
}); });
return event; return event;
} }
async saveToRelay(pool: ConnectionPool, ctx: NostrAccountContext) {
const nostrEvent = await this.toNostrEvent(ctx);
if (nostrEvent instanceof Error) {
return nostrEvent;
}
const err = pool.sendEvent(nostrEvent);
if (err instanceof Error) {
return err;
}
}
async syncFromRelay(pool: ConnectionPool, ctx: NostrAccountContext) {
const stream = await pool.newSub(OtherConfig.name, {
"#d": [OtherConfig.name],
authors: [ctx.publicKey.hex],
kinds: [NostrKind.Custom_App_Data],
});
if (stream instanceof Error) {
throw stream; // impossible
}
for await (const msg of stream.chan) {
if (msg.res.type == "EOSE") {
continue;
}
console.log("pin list", msg);
const config = await OtherConfig.FromNostrEvent(
// @ts-ignore
msg.res.event,
ctx,
);
if (config instanceof Error) {
console.error(config);
continue;
}
this.pinList = config.pinList;
console.log(this.pinList);
}
}
} }

View File

@ -8,10 +8,6 @@ export interface ConversationSummary {
profile: Profile_Nostr_Event | undefined; profile: Profile_Nostr_Event | undefined;
newestEventSendByMe: NostrEvent | undefined; newestEventSendByMe: NostrEvent | undefined;
newestEventReceivedByMe: NostrEvent | undefined; newestEventReceivedByMe: NostrEvent | undefined;
pinEvent: {
readonly created_at: number;
readonly content: CustomAppData;
} | undefined;
} }
export function getConversationSummaryFromPublicKey(k: PublicKey, users: Map<string, ConversationSummary>) { export function getConversationSummaryFromPublicKey(k: PublicKey, users: Map<string, ConversationSummary>) {
@ -78,7 +74,6 @@ export class ConversationLists implements ConversationListRetriever {
} }
} else { } else {
const newUserInfo: ConversationSummary = { const newUserInfo: ConversationSummary = {
pinEvent: undefined,
pubkey: PublicKey.FromHex(event.pubkey) as PublicKey, pubkey: PublicKey.FromHex(event.pubkey) as PublicKey,
newestEventReceivedByMe: undefined, newestEventReceivedByMe: undefined,
newestEventSendByMe: undefined, newestEventSendByMe: undefined,
@ -139,7 +134,6 @@ export class ConversationLists implements ConversationListRetriever {
} else { } else {
const newUserInfo: ConversationSummary = { const newUserInfo: ConversationSummary = {
pubkey: PublicKey.FromHex(whoAm_I_TalkingTo) as PublicKey, pubkey: PublicKey.FromHex(whoAm_I_TalkingTo) as PublicKey,
pinEvent: undefined,
newestEventReceivedByMe: undefined, newestEventReceivedByMe: undefined,
newestEventSendByMe: undefined, newestEventSendByMe: undefined,
profile: undefined, profile: undefined,

View File

@ -8,11 +8,12 @@ import { emitFunc } from "../event-bus.ts";
import { PinIcon, UnpinIcon } from "./icons/mod.tsx"; import { PinIcon, UnpinIcon } from "./icons/mod.tsx";
import { SearchUpdate } from "./search_model.ts"; import { SearchUpdate } from "./search_model.ts";
import { PublicKey } from "../lib/nostr-ts/key.ts"; import { PublicKey } from "../lib/nostr-ts/key.ts";
import { PinContact, UnpinContact } from "../nostr.ts"; import { PinConversation, UnpinConversation } from "../nostr.ts";
import { PrimaryTextColor } from "./style/colors.ts"; import { PrimaryTextColor } from "./style/colors.ts";
import { ButtonGroup } from "./components/button-group.tsx"; import { ButtonGroup } from "./components/button-group.tsx";
import { ChatIcon } from "./icons2/chat-icon.tsx"; import { ChatIcon } from "./icons2/chat-icon.tsx";
import { StartCreateGroupChat } from "./create-group.tsx"; import { StartCreateGroupChat } from "./create-group.tsx";
import { OtherConfig } from "./config-other.ts";
export interface ConversationListRetriever { export interface ConversationListRetriever {
getContacts: () => Iterable<ConversationSummary>; getContacts: () => Iterable<ConversationSummary>;
@ -25,8 +26,8 @@ export type ConversationType = "Contacts" | "Strangers" | "Group";
export type ContactUpdate = export type ContactUpdate =
| SelectConversationType | SelectConversationType
| SearchUpdate | SearchUpdate
| PinContact | PinConversation
| UnpinContact | UnpinConversation
| StartCreateGroupChat; | StartCreateGroupChat;
export type SelectConversationType = { export type SelectConversationType = {
@ -39,6 +40,7 @@ type Props = {
convoListRetriever: ConversationListRetriever; convoListRetriever: ConversationListRetriever;
currentSelected: PublicKey | undefined; currentSelected: PublicKey | undefined;
selectedContactGroup: ConversationType; selectedContactGroup: ConversationType;
pinListGetter: PinListGetter;
hasNewMessages: Set<string>; hasNewMessages: Set<string>;
}; };
export function ConversationList(props: Props) { export function ConversationList(props: Props) {
@ -153,15 +155,21 @@ export function ConversationList(props: Props) {
<ContactGroup <ContactGroup
contacts={Array.from(convoListToRender.values())} contacts={Array.from(convoListToRender.values())}
currentSelected={props.currentSelected} currentSelected={props.currentSelected}
pinListGetter={props.pinListGetter}
emit={props.emit} emit={props.emit}
/> />
</div> </div>
); );
} }
export interface PinListGetter {
getPinList(): Set<string>;
}
type ConversationListProps = { type ConversationListProps = {
contacts: { userInfo: ConversationSummary; isMarked: boolean }[]; contacts: { userInfo: ConversationSummary; isMarked: boolean }[];
currentSelected: PublicKey | undefined; currentSelected: PublicKey | undefined;
pinListGetter: PinListGetter;
emit: emitFunc<ContactUpdate>; emit: emitFunc<ContactUpdate>;
}; };
@ -170,10 +178,11 @@ function ContactGroup(props: ConversationListProps) {
props.contacts.sort((a, b) => { props.contacts.sort((a, b) => {
return sortUserInfo(a.userInfo, b.userInfo); return sortUserInfo(a.userInfo, b.userInfo);
}); });
const pinList = props.pinListGetter.getPinList();
const pinned = []; const pinned = [];
const unpinned = []; const unpinned = [];
for (const contact of props.contacts) { for (const contact of props.contacts) {
if (contact.userInfo.pinEvent && contact.userInfo.pinEvent.content.type == "PinContact") { if (pinList.has(contact.userInfo.pubkey.hex)) {
pinned.push(contact); pinned.push(contact);
} else { } else {
unpinned.push(contact); unpinned.push(contact);
@ -201,6 +210,7 @@ function ContactGroup(props: ConversationListProps) {
<ConversationListItem <ConversationListItem
userInfo={contact.userInfo} userInfo={contact.userInfo}
isMarked={contact.isMarked} isMarked={contact.isMarked}
isPinned={true}
/> />
<button <button
@ -211,7 +221,7 @@ function ContactGroup(props: ConversationListProps) {
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
props.emit({ props.emit({
type: "UnpinContact", type: "UnpinConversation",
pubkey: contact.userInfo.pubkey.hex, pubkey: contact.userInfo.pubkey.hex,
}); });
}} }}
@ -246,6 +256,7 @@ function ContactGroup(props: ConversationListProps) {
<ConversationListItem <ConversationListItem
userInfo={contact.userInfo} userInfo={contact.userInfo}
isMarked={contact.isMarked} isMarked={contact.isMarked}
isPinned={false}
/> />
<button <button
@ -256,7 +267,7 @@ function ContactGroup(props: ConversationListProps) {
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
props.emit({ props.emit({
type: "PinContact", type: "PinConversation",
pubkey: contact.userInfo.pubkey.hex, pubkey: contact.userInfo.pubkey.hex,
}); });
}} }}
@ -280,6 +291,7 @@ function ContactGroup(props: ConversationListProps) {
type ListItemProps = { type ListItemProps = {
userInfo: ConversationSummary; userInfo: ConversationSummary;
isMarked: boolean; isMarked: boolean;
isPinned: boolean;
}; };
function ConversationListItem(props: ListItemProps) { function ConversationListItem(props: ListItemProps) {
@ -316,7 +328,7 @@ function ConversationListItem(props: ListItemProps) {
</span> </span>
) )
: undefined} : undefined}
{props.userInfo.pinEvent != undefined && props.userInfo.pinEvent.content.type == "PinContact" {props.isPinned
? ( ? (
<PinIcon <PinIcon
class={tw`w-3 h-3 absolute top-0 right-0`} class={tw`w-3 h-3 absolute top-0 right-0`}

View File

@ -28,6 +28,7 @@ type DirectMessageContainerProps = {
allUserInfo: ConversationLists; allUserInfo: ConversationLists;
profilesSyncer: ProfileSyncer; profilesSyncer: ProfileSyncer;
eventSyncer: EventSyncer; eventSyncer: EventSyncer;
pinListGetter: cl.PinListGetter;
} & DM_Model; } & DM_Model;
export type MessageThread = { export type MessageThread = {

View File

@ -15,10 +15,10 @@ import { NostrEvent, NostrKind } from "../lib/nostr-ts/nostr.ts";
import { import {
DirectedMessage_Event, DirectedMessage_Event,
Parsed_Event, Parsed_Event,
PinContact, PinConversation,
Profile_Nostr_Event, Profile_Nostr_Event,
Text_Note_Event, Text_Note_Event,
UnpinContact, UnpinConversation,
} from "../nostr.ts"; } from "../nostr.ts";
import { ProfileData, ProfileSyncer } from "../features/profile.ts"; import { ProfileData, ProfileSyncer } from "../features/profile.ts";
import { MessageThread } from "./dm.tsx"; import { MessageThread } from "./dm.tsx";
@ -83,7 +83,7 @@ interface DirectMessagePanelProps {
db: Database_Contextual_View; db: Database_Contextual_View;
emit: emitFunc< emit: emitFunc<
EditorEvent | DirectMessagePanelUpdate | PinContact | UnpinContact EditorEvent | DirectMessagePanelUpdate | PinConversation | UnpinConversation
>; >;
profilesSyncer: ProfileSyncer; profilesSyncer: ProfileSyncer;
eventSyncer: EventSyncer; eventSyncer: EventSyncer;

View File

@ -102,7 +102,6 @@ export class RelayConfig {
this.config = Automerge.change(this.config, "add", (config) => { this.config = Automerge.change(this.config, "add", (config) => {
config[url] = true; config[url] = true;
}); });
const hex = secp256k1.utils.bytesToHex(this.save());
} }
async remove(url: string) { async remove(url: string) {

View File

@ -43,7 +43,6 @@ export class GroupChatController {
this.conversationLists.groupChatSummaries.set(args.groupKey.bech32, { this.conversationLists.groupChatSummaries.set(args.groupKey.bech32, {
newestEventReceivedByMe: undefined, newestEventReceivedByMe: undefined,
newestEventSendByMe: undefined, newestEventSendByMe: undefined,
pinEvent: undefined,
profile: undefined, profile: undefined,
pubkey: args.groupKey.toPublicKey(), pubkey: args.groupKey.toPublicKey(),
}); });

View File

@ -55,15 +55,15 @@ export type DirectedMessage_Event = Parsed_Event<NostrKind.DIRECT_MESSAGE> & {
}; };
export type Encrypted_Event = DirectedMessage_Event; export type Encrypted_Event = DirectedMessage_Event;
export type CustomAppData = PinContact | UnpinContact | UserLogin; export type CustomAppData = PinConversation | UnpinConversation | UserLogin;
export type PinContact = { export type PinConversation = {
type: "PinContact"; type: "PinConversation";
pubkey: string; pubkey: string;
}; };
export type UnpinContact = { export type UnpinConversation = {
type: "UnpinContact"; type: "UnpinConversation";
pubkey: string; pubkey: string;
}; };