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 { NewNav } from "./new-nav.tsx";
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();
await pool.addRelayURLs(
@ -14,22 +14,22 @@ await pool.addRelayURLs(
"wss://relay.nostr.wirednet.jp",
"wss://relay.nostr.moctane.com",
"wss://remnant.cloud",
// "wss://nostr.cahlen.org",
// "wss://fog.dedyn.io",
// "wss://global-relay.cesc.trade",
// "wss://nostr.dakukitsune.ca",
// "wss://africa.nostr.joburg",
// "wss://nostr-relay.ktwo.io",
// "wss://bevo.nostr1.com",
// "wss://relay.corpum.com",
// "wss://relay.nostr.directory",
// "wss://nostr.1f52b.xyz",
// "wss://lnbits.eldamar.icu/nostrrelay/relay",
// "wss://relay.cosmicbolt.net",
// "wss://island.nostr1.com",
// "wss://nostr.codingarena.de",
// "wss://nostr.madco.me",
// "wss://nostr-relay.bitcoin.ninja",
"wss://nostr.cahlen.org",
"wss://fog.dedyn.io",
"wss://global-relay.cesc.trade",
"wss://nostr.dakukitsune.ca",
"wss://africa.nostr.joburg",
"wss://nostr-relay.ktwo.io",
"wss://bevo.nostr1.com",
"wss://relay.corpum.com",
"wss://relay.nostr.directory",
"wss://nostr.1f52b.xyz",
"wss://lnbits.eldamar.icu/nostrrelay/relay",
"wss://relay.cosmicbolt.net",
"wss://island.nostr1.com",
"wss://nostr.codingarena.de",
"wss://nostr.madco.me",
"wss://nostr-relay.bitcoin.ninja",
],
);
const ctx = InMemoryAccountContext.Generate();
@ -42,13 +42,29 @@ const profileEvent = await prepareProfileEvent(ctx, {
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(
<NewNav
currentSpace="wss://blowater.nostr1.com"
emit={testEventBus.emit}
pool={pool}
activeNav="DM"
activeNav={"Public"}
profile={profileEvent}
currentConversation={currentConversation}
getters={{
getProfileByPublicKey: () => profileEvent,
getConversationList: () => convoList,
getPinList: () => pinList,
}}
/>,
document.body,
);

View File

@ -1,35 +1,38 @@
/** @jsx h */
import { Component, Fragment, h } from "preact";
import { emitFunc, EventSubscriber } from "../event-bus.ts";
import { Component, h } from "preact";
import { emitFunc } from "../event-bus.ts";
import { NavigationUpdate, NavTabID, SelectSpace, ShowProfileSetting } from "./nav.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 { Avatar, RelayAvatar } from "./components/avatar.tsx";
import { CaretDownIcon } from "./icons/caret-down-icon.tsx";
import { PoundIcon } from "./icons/pound-icon.tsx";
import { Profile_Nostr_Event } from "../nostr.ts";
import { ConversationSummary } from "./conversation-list.ts";
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 { ContactUpdate } from "./conversation-list.tsx";
import { TagSelected } from "./contact-tags.tsx";
import { ViewUserDetail } from "./message-panel.tsx";
import { UI_Interaction_Event, UserBlocker } from "./app_update.tsx";
import { func_GetProfileByPublicKey, func_GetProfilesByText } from "./search.tsx";
import { func_GetProfileByPublicKey } from "./search.tsx";
import { PinIcon } from "./icons/pin-icon.tsx";
import { SelectConversation } from "./search_model.ts";
type NewNavProps = {
pool: ConnectionPool;
activeNav: NavTabID;
currentSpace: string;
profile: Profile_Nostr_Event | undefined;
currentConversation: PublicKey | undefined;
getters: {
getProfileByPublicKey: func_GetProfileByPublicKey;
getConversationList: func_GetConversationList;
getPinList: func_GetPinList;
};
emit: emitFunc<
| SelectSpace
| NavigationUpdate
@ -39,22 +42,26 @@ type NewNavProps = {
| TagSelected
| ViewUserDetail
>;
profile: Profile_Nostr_Event | undefined;
};
export class NewNav extends Component<NewNavProps> {
render(props: NewNavProps) {
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
currentSpace={props.currentSpace}
spaceList={new Set(Array.from(props.pool.getRelays()).map((r) => r.url))}
emit={props.emit}
/>
{/* <GlobalSearch /> */}
<GroupChatList />
<DirectMessageList profile={props.profile} />
<ProfileMenu profile={props.profile} emit={props.emit} />
<GroupChatList emit={props.emit} activeNav={props.activeNav} />
<DirectMessageList
emit={props.emit}
currentSpace={props.currentSpace}
currentConversation={props.currentConversation}
getters={props.getters}
/>
<UserIndicator profile={props.profile} emit={props.emit} />
</div>
);
}
@ -111,13 +118,13 @@ class SpaceDropDownPanel extends Component<SpaceDropDownPanelProps, SpaceDropDow
}
return (
<div class="w-full">
{this.TopIconButton()}
{this.CurrentSpaceIndicator()}
{this.state.showDropDown ? this.DropDown(spaceList) : undefined}
</div>
);
}
TopIconButton = () => {
CurrentSpaceIndicator = () => {
return (
<div
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>
<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>
);
@ -147,7 +156,7 @@ class SpaceDropDownPanel extends Component<SpaceDropDownPanelProps, SpaceDropDow
DropDown = (spaceList: h.JSX.Element[]) => {
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.InviteButton()} */}
<div class="border border-neutral-600 my-3"></div>
@ -296,67 +305,148 @@ function GlobalSearch() {
return <div>Search</div>;
}
function GroupChatList() {
type GroupChatListProps = {
activeNav: NavTabID;
emit: emitFunc<NavigationUpdate>;
};
class GroupChatList extends Component<GroupChatListProps> {
render(props: GroupChatListProps) {
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" />
<div class="text-white text-sm font-medium font-sans leading-5">Public</div>
</div>
);
}
}
type func_GetConversationList = () => Set<PublicKey>;
type func_GetPinList = () => Set<PublicKey>;
type DirectMessageListProps = {
//TODO: The list is based on private messages both received and sent, as well as other sources.
profile: Profile_Nostr_Event | undefined;
emit: emitFunc<ContactUpdate>;
currentSpace: string;
currentConversation: PublicKey | undefined;
getters: {
getProfileByPublicKey: func_GetProfileByPublicKey;
getConversationList: func_GetConversationList;
getPinList: func_GetPinList;
};
};
// type DirectMessageListState = {};
class DirectMessageList extends Component<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 (
<div class="flex-1 w-full">
<div class="flex flex-col gap-2 w-full justify-start items-center">
<div class="flex gap-2 rounded-md px-2 py-1 bg-neutral-800 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 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 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 class="flex-1 w-full overflow-y-auto">
<div class="flex flex-col gap-1 w-full justify-start items-center">
{pinned.map((pubkey: PublicKey) => (
<DireactMessageItem
emit={props.emit}
pubkey={pubkey}
isPined={true}
currentConversation={props.currentConversation}
currentSpace={props.currentSpace}
getters={props.getters}
/>
))}
{unpinned.map((pubkey: PublicKey) => (
<DireactMessageItem
emit={props.emit}
pubkey={pubkey}
isPined={false}
currentConversation={props.currentConversation}
currentSpace={props.currentSpace}
getters={props.getters}
/>
))}
</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;
emit: emitFunc<ShowProfileSetting>;
};
class ProfileMenu extends Component<ProfileMenuProps> {
render(props: ProfileMenuProps) {
class UserIndicator extends Component<UserIndicatorProps> {
render(props: UserIndicatorProps) {
return (
<div class="w-full">
<div