sliding right panel & contact tags (#370)

This commit is contained in:
BlowaterNostr 2024-01-02 18:09:25 +08:00 committed by GitHub
parent c73b485189
commit feaa3f2ead
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 180 additions and 194 deletions

View File

@ -1,12 +1,8 @@
/** @jsx h */
import { h, render } from "https://esm.sh/preact@10.17.1";
import { setup } from "https://esm.sh/twind@0.16.16";
import { NewIndexedDB } from "./dexie-db.ts";
import { TWConfig } from "./tw.config.ts";
import { Start } from "./app.tsx";
setup(TWConfig);
const database = NewIndexedDB();
if (database instanceof Error) {
console.error(database);

View File

@ -417,7 +417,6 @@ export function AppComponent(props: {
dmVNode = (
<DirectMessageContainer
{...model.dm}
rightPanelModel={model.rightPanelModel}
bus={app.eventBus}
ctx={myAccountCtx}
profileGetter={app.database}

View File

@ -3,7 +3,6 @@ import { SearchInitModel, SearchModel } from "./search_model.ts";
import { ProfileData } from "../features/profile.ts";
import { EditorModel } from "./editor.tsx";
import { DM_Model } from "./dm.tsx";
import { RightPanelModel } from "./right-panel.tsx";
import { App } from "./app.tsx";
export type Model = {
@ -23,7 +22,6 @@ export type Model = {
// UI
navigationModel: NavigationModel;
rightPanelModel: RightPanelModel;
};
export function initialModel(): Model {
@ -45,9 +43,6 @@ export function initialModel(): Model {
navigationModel: {
activeNav: "DM",
},
rightPanelModel: {
show: false,
},
myProfile: undefined,
};
}

View File

@ -42,6 +42,7 @@ import { Search } from "./search.tsx";
import { SearchUpdate, SelectConversation } from "./search_model.ts";
import { RelayConfigChange, ViewRelayDetail } from "./setting.tsx";
import { SignInEvent } from "./signIn.tsx";
import { TagSelected } from "./contact-tags.tsx";
export type UI_Interaction_Event =
| SearchUpdate
@ -60,7 +61,8 @@ export type UI_Interaction_Event =
| StartEditGroupChatProfile
| StartInvite
| InviteUsersToGroup
| ViewRelayDetail;
| ViewRelayDetail
| TagSelected;
type BackToContactList = {
type: "BackToContactList";
@ -151,9 +153,6 @@ export async function* UI_Interaction_Update(args: {
else if (event.type == "SelectConversation") {
model.navigationModel.activeNav = "DM";
model.search.isSearching = false;
model.rightPanelModel = {
show: false,
};
updateConversation(app.model, event.pubkey, event.isGroupChat);
if (!model.dm.focusedContent.get(event.pubkey.hex)) {
@ -237,9 +236,6 @@ export async function* UI_Interaction_Update(args: {
//
else if (event.type == "ChangeNavigation") {
model.navigationModel.activeNav = event.id;
model.rightPanelModel = {
show: false,
};
} //
//
// DM
@ -260,8 +256,6 @@ export async function* UI_Interaction_Update(args: {
continue;
}
}
} else if (event.type == "ToggleRightPanel") {
model.rightPanelModel.show = event.show;
} else if (event.type == "ViewThread") {
if (model.navigationModel.activeNav == "DM") {
if (model.dm.currentEditor) {
@ -271,23 +265,19 @@ export async function* UI_Interaction_Update(args: {
);
}
}
model.rightPanelModel.show = true;
} else if (event.type == "ViewUserDetail") {
if (model.dm.currentEditor) {
const currentFocus = model.dm.focusedContent.get(model.dm.currentEditor.pubkey.hex);
if (
model.rightPanelModel.show == true &&
currentFocus instanceof PublicKey &&
currentFocus.hex == event.pubkey.hex &&
currentFocus.hex == model.dm.currentEditor.pubkey.hex
) {
model.rightPanelModel.show = false;
} else {
model.dm.focusedContent.set(
model.dm.currentEditor.pubkey.hex,
event.pubkey,
);
model.rightPanelModel.show = true;
}
}
} else if (event.type == "OpenNote") {

View File

@ -1,7 +1,6 @@
/** @jsx h */
import { createRef, h, render } from "https://esm.sh/preact@10.17.1";
import { Toast, ToastChannel } from "./toast.tsx";
import { TWConfig } from "../tw.config.ts";
import { Channel } from "https://raw.githubusercontent.com/BlowaterNostr/csp/master/csp.ts";
import { CenterClass, inputBorderClass } from "./tw.ts";

View File

@ -0,0 +1,12 @@
/** @jsx h */
import { h, render } from "https://esm.sh/preact@10.17.1";
import { ContactTags } from "./contact-tags.tsx";
import { testEventBus } from "./_setup.test.ts";
render(
<ContactTags
tags={["contacts", "strangers"]}
emit={testEventBus.emit}
/>,
document.body,
);

54
app/UI/contact-tags.tsx Normal file
View File

@ -0,0 +1,54 @@
/** @jsx h */
import { Component, h } from "https://esm.sh/preact@10.17.1";
import { HintTextColor, PrimaryTextColor, SecondaryTextColor } from "./style/colors.ts";
import { emitFunc } from "../event-bus.ts";
export type TagSelected = {
type: "tagSelected";
tag: Tag;
};
export type Tag = "contacts" | "strangers" | "blocked";
type Props = {
tags: Tag[];
emit: emitFunc<TagSelected>;
};
type State = {
selectedTag: Tag | undefined;
};
export class ContactTags extends Component<Props, State> {
state: State = {
selectedTag: undefined,
};
render() {
return (
<div>
{/* https://tailwindcolor.com/ */}
{Array.from(this.props.tags).map((tag) => (
<div
class={`m-1 px-2 rounded-full inline-block
hover:cursor-pointer select-none
text-white hover:text-black
${tag == this.state.selectedTag ? "border-2 border-cyan-300" : ""}
bg-green-400 hover:bg-white`}
onClick={(e) => {
this.setState({
selectedTag: tag,
});
this.props.emit({
type: "tagSelected",
tag: tag,
});
}}
>
{tag}
</div>
))}
</div>
);
}
}

View File

@ -18,6 +18,7 @@ import { UnpinIcon } from "./icons/unpin-icon.tsx";
import { ProfileGetter } from "./search.tsx";
import { SearchUpdate, SelectConversation } from "./search_model.ts";
import { ErrorColor, PrimaryTextColor, SecondaryBackgroundColor } from "./style/colors.ts";
import { ContactTags, TagSelected } from "./contact-tags.tsx";
export interface ConversationListRetriever {
getContacts: () => Iterable<ConversationSummary>;
@ -43,7 +44,7 @@ export interface NewMessageChecker {
}
type Props = {
emit: emitFunc<ContactUpdate | SearchUpdate>;
emit: emitFunc<ContactUpdate | SearchUpdate | TagSelected>;
eventBus: EventSubscriber<UI_Interaction_Event>;
convoListRetriever: ConversationListRetriever;
groupChatListGetter: GroupMessageListGetter;
@ -72,6 +73,14 @@ export class ConversationList extends Component<Props, State> {
e.isGroupChat,
),
});
} else if (e.type == "tagSelected") {
let group: ConversationType = "Strangers";
if (e.tag == "contacts") {
group = "Contacts";
}
this.setState({
selectedContactGroup: group,
});
}
}
}
@ -114,7 +123,7 @@ export class ConversationList extends Component<Props, State> {
<div
// https://tailwindcss.com/docs/hover-focus-and-other-states#quick-reference
class={`
h-screen w-80 max-sm:w-full
h-screen w-60 max-sm:w-full
flex flex-col bg-[${SecondaryBackgroundColor}]`}
>
<div
@ -160,52 +169,10 @@ export class ConversationList extends Component<Props, State> {
</div>
</div>
<ul class={tw`bg-[#36393F] w-full flex h-[3rem] border-b border-[#36393F]`}>
<li
class={tw`h-full flex-1 cursor-pointer hover:text-[#F7F7F7] text-[#96989D] bg-[#2F3136] hover:bg-[#42464D] ${CenterClass} ${
this.state.selectedContactGroup == "Contacts"
? "border-b-2 border-[#54D48C] bg-[#42464D] text-[#F7F7F7]"
: ""
}`}
onClick={() => this.setState({ selectedContactGroup: "Contacts" })}
>
Contacts: {contacts.length}
</li>
<li class={tw`w-[0.05rem] h-full bg-[#2F3136]`}></li>
<li
class={tw`h-full flex-1 cursor-pointer hover:text-[#F7F7F7] text-[#96989D] bg-[#2F3136] hover:bg-[#42464D] ${CenterClass} ${
this.state.selectedContactGroup == "Strangers"
? "border-b-2 border-[#54D48C] bg-[#42464D] text-[#F7F7F7]"
: ""
}`}
onClick={() => {
this.setState({
selectedContactGroup: "Strangers",
});
}}
>
Strangers: {strangers.length}
</li>
{IS_BETA_VERSION
? (
<li
class={tw`h-full flex-1 cursor-pointer hover:text-[#F7F7F7] text-[#96989D] bg-[#2F3136] hover:bg-[#42464D] ${CenterClass} ${
this.state.selectedContactGroup == "Group"
? "border-b-2 border-[#54D48C] bg-[#42464D] text-[#F7F7F7]"
: ""
}`}
onClick={() => {
this.setState({
selectedContactGroup: "Group",
});
}}
>
Group: {groups.length}
</li>
)
: undefined}
</ul>
<div class="py-1 border-b border-[#36393F]">
<ContactTags tags={["contacts", "strangers"]} emit={this.props.emit}>
</ContactTags>
</div>
<ContactGroup
contacts={Array.from(convoListToRender.values())}
@ -264,7 +231,7 @@ function ContactGroup(props: ConversationListProps) {
props.isGroupChat == props.currentSelected.isGroupChat
? "bg-[#42464D] text-[#FFFFFF]"
: "bg-[#42464D] text-[#96989D]"
} cursor-pointer p-2 hover:bg-[#3C3F45] my-2 rounded-lg flex items-center w-full relative group`}
} cursor-pointer p-2 hover:bg-[#3C3F45] mb-2 rounded-lg flex items-center w-full relative group`}
onClick={selectConversation(
props.emit,
contact.conversation.pubkey,
@ -279,7 +246,10 @@ function ContactGroup(props: ConversationListProps) {
/>
<button
class={tw`w-6 h-6 absolute hidden group-hover:flex top-[-0.75rem] right-[0.75rem] ${IconButtonClass} bg-[#42464D] hover:bg-[#2F3136]`}
class={tw`
w-6 h-6 absolute hidden group-hover:flex top-[-0.75rem] right-[0.75rem]
focus:outline-none focus-visible:outline-none rounded-full hover:bg-[#42464D] ${CenterClass}
bg-[#42464D] hover:bg-[#2F3136]`}
style={{
boxShadow: "2px 2px 5px 0 black",
}}
@ -331,7 +301,10 @@ function ContactGroup(props: ConversationListProps) {
/>
<button
class={tw`w-6 h-6 absolute hidden group-hover:flex top-[-0.75rem] right-[0.75rem] ${IconButtonClass} bg-[#42464D] hover:bg-[#2F3136]`}
class={tw`
w-6 h-6 absolute hidden group-hover:flex top-[-0.75rem] right-[0.75rem]
focus:outline-none focus-visible:outline-none rounded-full hover:bg-[#42464D] ${CenterClass}
bg-[#42464D] hover:bg-[#2F3136]`}
style={{
boxShadow: "2px 2px 5px 0 black",
}}

View File

@ -19,7 +19,6 @@ import { SettingIcon } from "./icons/setting-icon.tsx";
import { UserIcon } from "./icons/user-icon.tsx";
import { InviteButton } from "./invite-button.tsx";
import { MessagePanel, NewMessageListener } from "./message-panel.tsx";
import { RightPanelModel } from "./right-panel.tsx";
import { ProfileGetter } from "./search.tsx";
import { PrimaryTextColor } from "./style/colors.ts";
import {
@ -36,7 +35,6 @@ export type DM_Model = {
};
type DirectMessageContainerProps = {
rightPanelModel: RightPanelModel;
ctx: NostrAccountContext;
pool: ConnectionPool;
bus: EventBus<UI_Interaction_Event>;
@ -178,13 +176,12 @@ export class DirectMessageContainer extends Component<DirectMessageContainerProp
buttons={buttons}
currentEditor={this.state.currentEditor}
profileGetter={this.props.profileGetter}
showRightPanel={this.props.rightPanelModel.show}
/>
<div class={`flex-1 overflow-auto`}>
<MessagePanel
myPublicKey={props.ctx.publicKey}
rightPanelModel={props.rightPanelModel}
emit={props.bus.emit}
eventSub={props.bus}
newMessageListener={props.newMessageListener}
focusedContent={getFocusedContent(
props.focusedContent.get(this.state.currentEditor.pubkey.hex),
@ -213,7 +210,6 @@ function TopBar(props: {
bus: EventBus<UI_Interaction_Event>;
currentEditor: EditorModel;
profileGetter: ProfileGetter;
showRightPanel: boolean;
buttons: VNode[];
}) {
return (
@ -228,7 +224,7 @@ function TopBar(props: {
type: "BackToContactList",
});
}}
class={`w-6 h-6 mobile:mr-2 desktop:hidden ${IconButtonClass}`}
class={`w-6 h-6 mx-2 ${IconButtonClass}`}
>
<LeftArrowIcon
class={`w-4 h-4`}
@ -241,8 +237,8 @@ function TopBar(props: {
// https://tailwindcss.com/docs/customizing-colors
// https://tailwindcss.com/docs/cursor
class={`text-[#F3F4EA] text-[1.2rem]
hover:text-[#60a5fa] hover:cursor-pointer
ml-4 mobile:text-base whitespace-nowrap truncate`}
hover:text-[#60a5fa] hover:cursor-pointer
whitespace-nowrap truncate`}
onClick={() => {
if (!props.currentEditor) {
return;
@ -263,28 +259,23 @@ function TopBar(props: {
<div>
{props.buttons}
{!props.showRightPanel
? (
<button
class={`absolute z-10 w-6 h-6 transition-transform duration-100 ease-in-out right-4 mobile:right-0 top-4${
props.showRightPanel ? " rotate-180" : ""
} ${IconButtonClass}`}
onClick={() => {
props.bus.emit({
type: "ToggleRightPanel",
show: !props.showRightPanel,
});
}}
>
<LeftArrowIcon
class={`w-4 h-4`}
style={{
fill: "#F3F4EA",
}}
/>
</button>
)
: undefined}
<button
class={`absolute z-10 w-6 h-6 transition-transform duration-100 ease-in-out
right-4 mobile:right-0 top-4 ${IconButtonClass}`}
onClick={() => {
props.bus.emit({
type: "ToggleRightPanel",
show: true,
});
}}
>
<LeftArrowIcon
class={`w-4 h-4`}
style={{
fill: "#F3F4EA",
}}
/>
</button>
</div>
</div>
);

View File

@ -6,11 +6,11 @@ import { PublicKey } from "../../libs/nostr.ts/key.ts";
import { NoteID } from "../../libs/nostr.ts/nip19.ts";
import { NostrEvent, NostrKind } from "../../libs/nostr.ts/nostr.ts";
import { RelayRecordGetter } from "../database.ts";
import { emitFunc } from "../event-bus.ts";
import { emitFunc, EventSubscriber } from "../event-bus.ts";
import { ProfileData, ProfileSyncer } from "../features/profile.ts";
import { Parsed_Event, PinConversation, UnpinConversation } from "../nostr.ts";
import { isMobile } from "./_helper.ts";
import { ChatMessagesGetter } from "./app_update.tsx";
import { ChatMessagesGetter, UI_Interaction_Event } from "./app_update.tsx";
import { Avatar } from "./components/avatar.tsx";
import { IconButtonClass } from "./components/tw.ts";
import { Editor, EditorEvent, EditorModel } from "./editor.tsx";
@ -28,17 +28,14 @@ import {
} from "./message.ts";
import { NoteCard } from "./note-card.tsx";
import { ProfileCard } from "./profile-card.tsx";
import { RightPanel, RightPanelModel } from "./right-panel.tsx";
import { RightPanel } from "./right-panel.tsx";
import { ProfileGetter } from "./search.tsx";
import { SelectConversation } from "./search_model.ts";
import { DividerBackgroundColor, ErrorColor, LinkColor, PrimaryTextColor } from "./style/colors.ts";
import { UserDetail } from "./user-detail.tsx";
export type DirectMessagePanelUpdate =
| {
type: "ToggleRightPanel";
show: boolean;
}
| ToggleRightPanel
| ViewThread
| ViewUserDetail
| OpenNote
@ -47,6 +44,11 @@ export type DirectMessagePanelUpdate =
message: ChatMessage;
};
export type ToggleRightPanel = {
type: "ToggleRightPanel";
show: boolean;
};
export type OpenNote = {
type: "OpenNote";
event: NostrEvent;
@ -74,11 +76,10 @@ interface DirectMessagePanelProps {
pubkey: PublicKey;
} | undefined;
rightPanelModel: RightPanelModel;
emit: emitFunc<
EditorEvent | DirectMessagePanelUpdate | PinConversation | UnpinConversation | SelectConversation
>;
eventSub: EventSubscriber<UI_Interaction_Event>;
profilesSyncer: ProfileSyncer;
eventSyncer: EventSyncer;
profileGetter: ProfileGetter;
@ -111,34 +112,32 @@ export class MessagePanel extends Component<DirectMessagePanelProps> {
render() {
const props = this.props;
let rightPanel;
if (props.rightPanelModel.show) {
let rightPanelChildren: h.JSX.Element | undefined;
if (props.focusedContent) {
if (props.focusedContent.type == "ProfileData") {
rightPanelChildren = (
<UserDetail
targetUserProfile={{
name: props.focusedContent?.data?.name,
picture: props.focusedContent?.data?.picture,
about: props.focusedContent?.data?.about,
website: props.focusedContent?.data?.website,
}}
pubkey={props.focusedContent.pubkey}
emit={props.emit}
/>
);
}
let rightPanelChildren: h.JSX.Element | undefined;
if (props.focusedContent) {
if (props.focusedContent.type == "ProfileData") {
rightPanelChildren = (
<UserDetail
targetUserProfile={{
name: props.focusedContent?.data?.name,
picture: props.focusedContent?.data?.picture,
about: props.focusedContent?.data?.about,
website: props.focusedContent?.data?.website,
}}
pubkey={props.focusedContent.pubkey}
emit={props.emit}
/>
);
}
rightPanel = (
<RightPanel
emit={props.emit}
rightPanelModel={props.rightPanelModel}
>
{rightPanelChildren}
</RightPanel>
);
}
let rightPanel = (
<RightPanel
emit={props.emit}
eventSub={props.eventSub}
>
{rightPanelChildren}
</RightPanel>
);
let vnode = (
<div class={tw`flex h-full w-full relative bg-[#36393F]`}>
<div class={tw`flex flex-col h-full flex-1 overflow-hidden`}>

View File

@ -7,9 +7,7 @@ render(
<div class="border">
<RightPanel
emit={testEventBus.emit}
rightPanelModel={{
show: true,
}}
eventSub={testEventBus}
>
Test
</RightPanel>

View File

@ -1,35 +1,49 @@
/** @jsx h */
import { Component, h } from "https://esm.sh/preact@10.17.1";
import { emitFunc } from "../event-bus.ts";
import { Component, createRef, h } from "https://esm.sh/preact@10.17.1";
import { emitFunc, EventSubscriber } from "../event-bus.ts";
import { IconButtonClass } from "./components/tw.ts";
import { CloseIcon } from "./icons/close-icon.tsx";
import { DirectMessagePanelUpdate } from "./message-panel.tsx";
export type RightPanelModel = {
show: boolean;
};
import { DirectMessagePanelUpdate, ToggleRightPanel } from "./message-panel.tsx";
import { tw } from "https://esm.sh/twind@0.16.16";
import { UI_Interaction_Event } from "./app_update.tsx";
type RightPanelProps = {
emit: emitFunc<DirectMessagePanelUpdate>;
rightPanelModel: RightPanelModel;
eventSub: EventSubscriber<UI_Interaction_Event>;
};
type RightPanelState = {
show: boolean;
};
export class RightPanel extends Component<RightPanelProps, {}> {
ref = createRef<HTMLDivElement>();
events = this.props.eventSub.onChange();
export class RightPanel extends Component<RightPanelProps, RightPanelState> {
state: RightPanelState = {
show: true,
};
async componentDidMount() {
for await (const event of this.events) {
if (event.type == "ToggleRightPanel") {
if (event.show) {
const ele = this.ref.current;
if (ele) ele.classList.remove("translate-x-full");
} else {
const ele = this.ref.current;
if (ele) ele.classList.add("translate-x-full");
}
}
}
}
componentWillUnmount() {
this.events.close();
}
render() {
const { emit, children } = this.props;
const { show } = this.state;
return (
<div
class={`border-l fixed top-0 right-0 h-full bg-[#2F3136] overflow-hidden overflow-y-auto z-20 transition-all duration-300 ease-in-out w-96 max-w-full`}
ref={this.ref}
class={tw`fixed top-0 right-0 border-l
h-full bg-[#2F3136]
z-20 transition duration-150 ease-in-out w-96 max-w-full
translate-x-full`}
>
<button
class={`w-6 min-w-[1.5rem] h-6 ml-4 ${IconButtonClass} hover:bg-[#36393F] absolute right-2 top-3 z-10 border-2`}
@ -38,7 +52,6 @@ export class RightPanel extends Component<RightPanelProps, RightPanelState> {
type: "ToggleRightPanel",
show: false,
});
this.setState({ show: !show });
}}
>
<CloseIcon

View File

@ -1,33 +0,0 @@
import { Configuration } from "https://esm.sh/twind@0.16.16";
export const TWConfig: Configuration = {
theme: {
fontFamily: {
roboto: ["Roboto", "sans-serif"],
},
screens: {
"mobile": { "max": "1023px" },
"desktop": { "min": "1024px" },
},
extend: {
keyframes: {
toast: {
"0%": { transform: "translateX(0)" },
"16.66%": { transform: "translateX(calc(-100% - 1rem))" },
"83.34%": { transform: "translateX(calc(-100% - 1rem))" },
"100%": { transform: "translateX(0)" },
},
},
animation: {
"toast": "toast 3s cubic-bezier(0.68, -0.55, 0.25, 1.35)",
},
},
},
// https://twind.dev/handbook/extended-functionality.html
// https://sass-lang.com/documentation/style-rules/parent-selector/
variants: {
"children": "& > *",
"firstChild": "& > *:first-child",
"lastChild": "& > *:last-child",
},
};