new conversation list (#496)

Co-authored-by: BlowaterNostr <blowater.nostr@proton.me>
This commit is contained in:
Bob 2024-07-10 17:05:01 +08:00 committed by GitHub
parent f037c3adbc
commit 9b7373de99
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 195 additions and 89 deletions

View File

@ -2,7 +2,7 @@
import { h, render } from "preact"; import { h, render } from "preact";
import { NewNav } from "./new-nav.tsx"; import { NewNav } from "./new-nav.tsx";
import { prepareProfileEvent, testEventBus } from "./_setup.test.ts"; import { prepareProfileEvent, testEventBus } from "./_setup.test.ts";
import { ConnectionPool, InMemoryAccountContext } from "@blowater/nostr-sdk"; import { ConnectionPool, InMemoryAccountContext, PublicKey } from "@blowater/nostr-sdk";
const pool = new ConnectionPool(); const pool = new ConnectionPool();
await pool.addRelayURLs( await pool.addRelayURLs(
@ -14,22 +14,22 @@ await pool.addRelayURLs(
"wss://relay.nostr.wirednet.jp", "wss://relay.nostr.wirednet.jp",
"wss://relay.nostr.moctane.com", "wss://relay.nostr.moctane.com",
"wss://remnant.cloud", "wss://remnant.cloud",
// "wss://nostr.cahlen.org", "wss://nostr.cahlen.org",
// "wss://fog.dedyn.io", "wss://fog.dedyn.io",
// "wss://global-relay.cesc.trade", "wss://global-relay.cesc.trade",
// "wss://nostr.dakukitsune.ca", "wss://nostr.dakukitsune.ca",
// "wss://africa.nostr.joburg", "wss://africa.nostr.joburg",
// "wss://nostr-relay.ktwo.io", "wss://nostr-relay.ktwo.io",
// "wss://bevo.nostr1.com", "wss://bevo.nostr1.com",
// "wss://relay.corpum.com", "wss://relay.corpum.com",
// "wss://relay.nostr.directory", "wss://relay.nostr.directory",
// "wss://nostr.1f52b.xyz", "wss://nostr.1f52b.xyz",
// "wss://lnbits.eldamar.icu/nostrrelay/relay", "wss://lnbits.eldamar.icu/nostrrelay/relay",
// "wss://relay.cosmicbolt.net", "wss://relay.cosmicbolt.net",
// "wss://island.nostr1.com", "wss://island.nostr1.com",
// "wss://nostr.codingarena.de", "wss://nostr.codingarena.de",
// "wss://nostr.madco.me", "wss://nostr.madco.me",
// "wss://nostr-relay.bitcoin.ninja", "wss://nostr-relay.bitcoin.ninja",
], ],
); );
const ctx = InMemoryAccountContext.Generate(); const ctx = InMemoryAccountContext.Generate();
@ -42,13 +42,29 @@ const profileEvent = await prepareProfileEvent(ctx, {
picture: "https://image.nostr.build/655007ae74f24ea1c611889f48b25cb485b83ab67408daddd98f95782f47e1b5.jpg", picture: "https://image.nostr.build/655007ae74f24ea1c611889f48b25cb485b83ab67408daddd98f95782f47e1b5.jpg",
}); });
let currentConversation: PublicKey | undefined;
const convoList = new Set<PublicKey>();
const pinList = new Set<PublicKey>();
for (let i = 0; i < 50; i++) {
const pubkey = InMemoryAccountContext.Generate().publicKey;
if (i % 4 == 0) pinList.add(pubkey);
if (i == 5) currentConversation = pubkey;
convoList.add(pubkey);
}
render( render(
<NewNav <NewNav
currentSpace="wss://blowater.nostr1.com" currentSpace="wss://blowater.nostr1.com"
emit={testEventBus.emit} emit={testEventBus.emit}
pool={pool} pool={pool}
activeNav="DM" activeNav={"Public"}
profile={profileEvent} profile={profileEvent}
currentConversation={currentConversation}
getters={{
getProfileByPublicKey: () => profileEvent,
getConversationList: () => convoList,
getPinList: () => pinList,
}}
/>, />,
document.body, document.body,
); );

View File

@ -1,35 +1,38 @@
/** @jsx h */ /** @jsx h */
import { Component, Fragment, h } from "preact"; import { Component, h } from "preact";
import { emitFunc, EventSubscriber } from "../event-bus.ts"; import { emitFunc } from "../event-bus.ts";
import { NavigationUpdate, NavTabID, SelectSpace, ShowProfileSetting } from "./nav.tsx"; import { NavigationUpdate, NavTabID, SelectSpace, ShowProfileSetting } from "./nav.tsx";
import { ViewSpaceSettings } from "./setting.tsx"; import { ViewSpaceSettings } from "./setting.tsx";
import { ConnectionPool, getRelayInformation, RelayInformation, robohash } from "@blowater/nostr-sdk"; import {
ConnectionPool,
getRelayInformation,
PublicKey,
RelayInformation,
robohash,
} from "@blowater/nostr-sdk";
import { setState } from "./_helper.ts"; import { setState } from "./_helper.ts";
import { Avatar, RelayAvatar } from "./components/avatar.tsx"; import { Avatar, RelayAvatar } from "./components/avatar.tsx";
import { CaretDownIcon } from "./icons/caret-down-icon.tsx"; import { CaretDownIcon } from "./icons/caret-down-icon.tsx";
import { PoundIcon } from "./icons/pound-icon.tsx"; import { PoundIcon } from "./icons/pound-icon.tsx";
import { Profile_Nostr_Event } from "../nostr.ts"; import { Profile_Nostr_Event } from "../nostr.ts";
import { ConversationSummary } from "./conversation-list.ts"; import { ContactUpdate } from "./conversation-list.tsx";
import { ProfileData } from "../features/profile.ts";
import { PinIcon } from "./icons/pin-icon.tsx";
import { PrimaryTextColor } from "./style/colors.ts";
import {
ContactUpdate,
ConversationListRetriever,
ConversationType,
NewMessageChecker,
PinListGetter,
} from "./conversation-list.tsx";
import { SearchUpdate, SelectConversation } from "./search_model.ts";
import { TagSelected } from "./contact-tags.tsx"; import { TagSelected } from "./contact-tags.tsx";
import { ViewUserDetail } from "./message-panel.tsx"; import { ViewUserDetail } from "./message-panel.tsx";
import { UI_Interaction_Event, UserBlocker } from "./app_update.tsx"; import { func_GetProfileByPublicKey } from "./search.tsx";
import { func_GetProfileByPublicKey, func_GetProfilesByText } from "./search.tsx"; import { PinIcon } from "./icons/pin-icon.tsx";
import { SelectConversation } from "./search_model.ts";
type NewNavProps = { type NewNavProps = {
pool: ConnectionPool; pool: ConnectionPool;
activeNav: NavTabID; activeNav: NavTabID;
currentSpace: string; currentSpace: string;
profile: Profile_Nostr_Event | undefined;
currentConversation: PublicKey | undefined;
getters: {
getProfileByPublicKey: func_GetProfileByPublicKey;
getConversationList: func_GetConversationList;
getPinList: func_GetPinList;
};
emit: emitFunc< emit: emitFunc<
| SelectSpace | SelectSpace
| NavigationUpdate | NavigationUpdate
@ -39,22 +42,26 @@ type NewNavProps = {
| TagSelected | TagSelected
| ViewUserDetail | ViewUserDetail
>; >;
profile: Profile_Nostr_Event | undefined;
}; };
export class NewNav extends Component<NewNavProps> { export class NewNav extends Component<NewNavProps> {
render(props: NewNavProps) { render(props: NewNavProps) {
return ( return (
<div class="h-screen w-64 flex flex-col gap-y-4 overflow-y-auto bg-neutral-900 p-2 items-center"> <div class="h-screen w-64 flex flex-col gap-y-4 overflow-y-auto bg-neutral-900 p-2 items-center select-none">
<SpaceDropDownPanel <SpaceDropDownPanel
currentSpace={props.currentSpace} currentSpace={props.currentSpace}
spaceList={new Set(Array.from(props.pool.getRelays()).map((r) => r.url))} spaceList={new Set(Array.from(props.pool.getRelays()).map((r) => r.url))}
emit={props.emit} emit={props.emit}
/> />
{/* <GlobalSearch /> */} {/* <GlobalSearch /> */}
<GroupChatList /> <GroupChatList emit={props.emit} activeNav={props.activeNav} />
<DirectMessageList profile={props.profile} /> <DirectMessageList
<ProfileMenu profile={props.profile} emit={props.emit} /> emit={props.emit}
currentSpace={props.currentSpace}
currentConversation={props.currentConversation}
getters={props.getters}
/>
<UserIndicator profile={props.profile} emit={props.emit} />
</div> </div>
); );
} }
@ -111,13 +118,13 @@ class SpaceDropDownPanel extends Component<SpaceDropDownPanelProps, SpaceDropDow
} }
return ( return (
<div class="w-full"> <div class="w-full">
{this.TopIconButton()} {this.CurrentSpaceIndicator()}
{this.state.showDropDown ? this.DropDown(spaceList) : undefined} {this.state.showDropDown ? this.DropDown(spaceList) : undefined}
</div> </div>
); );
} }
TopIconButton = () => { CurrentSpaceIndicator = () => {
return ( return (
<div <div
class="flex flex-row items-center gap-1 p-1 rounded cursor-pointer hover:bg-neutral-500" class="flex flex-row items-center gap-1 p-1 rounded cursor-pointer hover:bg-neutral-500"
@ -139,7 +146,9 @@ class SpaceDropDownPanel extends Component<SpaceDropDownPanelProps, SpaceDropDow
: <div>current space url</div>} : <div>current space url</div>}
</div> </div>
<div class="w-8 h-8 flex justify-center items-center rounded"> <div class="w-8 h-8 flex justify-center items-center rounded">
<CaretDownIcon class="w-6 h-6 text-neutral-600" /> {this.state.showDropDown
? <CaretDownIcon class="w-6 h-6 text-neutral-600 transition-transform -rotate-180" />
: <CaretDownIcon class="w-6 h-6 text-neutral-600 transition-transform" />}
</div> </div>
</div> </div>
); );
@ -147,7 +156,7 @@ class SpaceDropDownPanel extends Component<SpaceDropDownPanelProps, SpaceDropDow
DropDown = (spaceList: h.JSX.Element[]) => { DropDown = (spaceList: h.JSX.Element[]) => {
return ( return (
<div class="absolute z-10 min-w-64 rounded-lg bg-neutral-700 text-white p-4"> <div class="absolute z-10 min-w-64 rounded-lg bg-neutral-700 text-white p-4 mt-1">
{this.SettingsButton()} {this.SettingsButton()}
{/* {this.InviteButton()} */} {/* {this.InviteButton()} */}
<div class="border border-neutral-600 my-3"></div> <div class="border border-neutral-600 my-3"></div>
@ -296,67 +305,148 @@ function GlobalSearch() {
return <div>Search</div>; return <div>Search</div>;
} }
function GroupChatList() { type GroupChatListProps = {
activeNav: NavTabID;
emit: emitFunc<NavigationUpdate>;
};
class GroupChatList extends Component<GroupChatListProps> {
render(props: GroupChatListProps) {
return ( return (
<div class="flex items-center justify-start gap-1 w-full p-2"> <div
class={`flex items-center justify-start gap-1 w-full p-2 rounded-md px-2 py-1 hover:bg-neutral-950 cursor-pointer ${
props.activeNav == "Public" ? "bg-neutral-800" : ""
}`}
onClick={() => {
props.emit({
type: "ChangeNavigation",
id: "Public",
});
}}
>
<PoundIcon class="w-4 h-4 text-neutral-500" /> <PoundIcon class="w-4 h-4 text-neutral-500" />
<div class="text-white text-sm font-medium font-sans leading-5">Public</div> <div class="text-white text-sm font-medium font-sans leading-5">Public</div>
</div> </div>
); );
} }
}
type func_GetConversationList = () => Set<PublicKey>;
type func_GetPinList = () => Set<PublicKey>;
type DirectMessageListProps = { type DirectMessageListProps = {
//TODO: The list is based on private messages both received and sent, as well as other sources. emit: emitFunc<ContactUpdate>;
profile: Profile_Nostr_Event | undefined; currentSpace: string;
currentConversation: PublicKey | undefined;
getters: {
getProfileByPublicKey: func_GetProfileByPublicKey;
getConversationList: func_GetConversationList;
getPinList: func_GetPinList;
};
}; };
// type DirectMessageListState = {};
class DirectMessageList extends Component<DirectMessageListProps> { class DirectMessageList extends Component<DirectMessageListProps> {
render(props: DirectMessageListProps) { render(props: DirectMessageListProps) {
const pinList = props.getters.getPinList();
const pinned = [];
const unpinned = [];
for (const pubkey of props.getters.getConversationList()) {
if (pinList.has(pubkey)) {
pinned.push(pubkey);
} else {
unpinned.push(pubkey);
}
}
return ( return (
<div class="flex-1 w-full"> <div class="flex-1 w-full overflow-y-auto">
<div class="flex flex-col gap-2 w-full justify-start items-center"> <div class="flex flex-col gap-1 w-full justify-start items-center">
<div class="flex gap-2 rounded-md px-2 py-1 bg-neutral-800 w-full"> {pinned.map((pubkey: PublicKey) => (
<div class="w-4 flex justify-center items-center"> <DireactMessageItem
<Avatar picture={props.profile?.profile.picture || "./logo.webp"} /> emit={props.emit}
</div> pubkey={pubkey}
<div class="text-white text-sm font-medium font-sans leading-5"> isPined={true}
{props.profile?.profile.name || props.profile?.profile.display_name || currentConversation={props.currentConversation}
props.profile?.pubkey} currentSpace={props.currentSpace}
</div> getters={props.getters}
</div> />
<div class="flex gap-2 rounded-md px-2 py-1 w-full"> ))}
<div class="w-4 flex justify-center items-center"> {unpinned.map((pubkey: PublicKey) => (
<Avatar picture={props.profile?.profile.picture || "./logo.webp"} /> <DireactMessageItem
</div> emit={props.emit}
<div class="text-white text-sm font-medium font-sans leading-5"> pubkey={pubkey}
{props.profile?.profile.name || props.profile?.profile.display_name || isPined={false}
props.profile?.pubkey} currentConversation={props.currentConversation}
</div> currentSpace={props.currentSpace}
</div> getters={props.getters}
<div class="flex gap-2 rounded-md px-2 py-1 w-full"> />
<div class="w-4 flex justify-center items-center"> ))}
<Avatar picture={props.profile?.profile.picture || "./logo.webp"} />
</div>
<div class="text-white text-sm font-medium font-sans leading-5">
{props.profile?.profile.name || props.profile?.profile.display_name ||
props.profile?.pubkey}
</div>
</div>
</div> </div>
</div> </div>
); );
} }
} }
type ProfileMenuProps = { type DireactMessageItemProps = {
pubkey: PublicKey;
emit: emitFunc<SelectConversation>;
isPined: boolean;
currentSpace: string;
currentConversation: PublicKey | undefined;
getters: {
getProfileByPublicKey: func_GetProfileByPublicKey;
};
};
class DireactMessageItem extends Component<DireactMessageItemProps> {
render(props: DireactMessageItemProps) {
const profile = props.getters.getProfileByPublicKey(props.pubkey, new URL(props.currentSpace))
?.profile;
const picture = profile?.picture || "./logo.webp";
const name = profile?.name || profile?.display_name || profile?.pubkey;
return (
<div
class={`flex gap-2 rounded-md px-2 py-1 w-full hover:bg-neutral-950 cursor-pointer ${
props.pubkey.hex == props.currentConversation?.hex ? "bg-neutral-800" : ""
}`}
onClick={() => {
props.emit({
type: "SelectConversation",
pubkey: props.pubkey,
});
}}
>
<div class="w-4 flex justify-center items-center">
<Avatar picture={picture} />
</div>
<div class="text-white text-sm font-medium font-sans leading-5 flex-1">
{name}
</div>
{props.isPined
? (
<div class="flex justify-center items-center">
<PinIcon
class={`w-3 h-3`}
style={{
fill: "rgb(185, 187, 190)",
stroke: "rgb(185, 187, 190)",
strokeWidth: 2,
}}
/>
</div>
)
: undefined}
</div>
);
}
}
type UserIndicatorProps = {
profile: Profile_Nostr_Event | undefined; profile: Profile_Nostr_Event | undefined;
emit: emitFunc<ShowProfileSetting>; emit: emitFunc<ShowProfileSetting>;
}; };
class ProfileMenu extends Component<ProfileMenuProps> { class UserIndicator extends Component<UserIndicatorProps> {
render(props: ProfileMenuProps) { render(props: UserIndicatorProps) {
return ( return (
<div class="w-full"> <div class="w-full">
<div <div