This commit is contained in:
Kieran 2023-06-21 15:28:17 +01:00
parent 9c88f2e28f
commit 6f0dc2e430
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
11 changed files with 154 additions and 47 deletions

View File

@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@snort/system-react": "^1.0.2",
"@snort/system-react": "^1.0.3",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^13.0.0",
"@testing-library/user-event": "^13.2.1",
@ -39,6 +39,7 @@
]
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"typescript": "^5.1.3"
}
}

View File

@ -8,6 +8,7 @@ import AsyncButton from "./async-button";
import { Profile } from "./profile";
import { Icon } from "./icon";
import Spinner from "./spinner";
import { useLogin } from "hooks/login";
export interface LiveChatOptions {
canWrite?: boolean,
@ -17,6 +18,7 @@ export interface LiveChatOptions {
export function LiveChat({ link, options }: { link: NostrLink, options?: LiveChatOptions }) {
const [chat, setChat] = useState("");
const messages = useLiveChatFeed(link);
const login = useLogin();
async function sendChatMessage() {
const pub = await EventPublisher.nip7();
@ -35,6 +37,31 @@ export function LiveChat({ link, options }: { link: NostrLink, options?: LiveCha
setChat("");
}
}
function writeMessage() {
return <>
<div>
<input
type="text"
autoFocus={false}
onChange={v => setChat(v.target.value)}
value={chat}
placeholder="Message"
onKeyDown={async e => {
if (e.code === "Enter") {
e.preventDefault();
await sendChatMessage();
}
}}
/>
<Icon name="message" size={15} />
</div>
<AsyncButton onClick={sendChatMessage} className="btn btn-border">
Send
</AsyncButton>
</>
}
return (
<div className="live-chat">
{(options?.showHeader ?? true) && <div className="header">
@ -49,25 +76,7 @@ export function LiveChat({ link, options }: { link: NostrLink, options?: LiveCha
{messages.data === undefined && <Spinner />}
</div>
{(options?.canWrite ?? true) && <div className="write-message">
<div>
<input
type="text"
autoFocus={false}
onChange={v => setChat(v.target.value)}
value={chat}
placeholder="Message"
onKeyDown={async e => {
if (e.code === "Enter") {
e.preventDefault();
await sendChatMessage();
}
}}
/>
<Icon name="message" size={15} />
</div>
<AsyncButton onClick={sendChatMessage} className="btn btn-border">
Send
</AsyncButton>
{login ? writeMessage() : <p>Please login to write messages!</p>}
</div>}
</div>
);

View File

@ -1,12 +1,24 @@
import "./profile.css";
import { useUserProfile } from "@snort/system-react";
import { UserMetadata } from "@snort/system";
import { hexToBech32 } from "@snort/shared";
import { System } from "index";
export function Profile({ pubkey }: { pubkey: string }) {
export interface ProfileOptions {
showName?: boolean,
suffix?: string
}
export function getName(pk: string, user?: UserMetadata) {
const shortPubkey = hexToBech32("npub", pk).slice(0, 12);
return user?.display_name ?? user?.name ?? shortPubkey
}
export function Profile({ pubkey, options }: { pubkey: string, options?: ProfileOptions }) {
const profile = useUserProfile(System, pubkey);
return <div className="profile">
<img src={profile?.picture} />
{profile?.display_name ?? profile?.name}
{(options?.showName ?? true) && getName(pubkey, profile)}
</div>
}

6
src/hooks/login.ts Normal file
View File

@ -0,0 +1,6 @@
import { Login } from "index";
import { useSyncExternalStore } from "react";
export function useLogin() {
return useSyncExternalStore(c => Login.hook(c), () => Login.snapshot());
}

View File

@ -65,4 +65,10 @@ a {
.btn-primary {
background: linear-gradient(94.73deg, #2BD9FF 0%, #8C8DED 47.4%, #F838D9 100%);
color: white;
}
.btn>span {
display: flex;
align-items: center;
gap: 8px;
}

View File

@ -7,10 +7,12 @@ import { RouterProvider, createBrowserRouter } from 'react-router-dom';
import { LayoutPage } from 'pages/layout';
import { StreamPage } from 'pages/stream-page';
import { ChatPopout } from 'pages/chat-popout';
import { LoginStore } from 'login';
export const System = new NostrSystem({
});
export const Login = new LoginStore();
[
"wss://relay.snort.social",

29
src/login.ts Normal file
View File

@ -0,0 +1,29 @@
import { ExternalStore } from "@snort/shared";
export interface LoginSession {
pubkey: string
}
export class LoginStore extends ExternalStore<LoginSession | undefined> {
#session?: LoginSession;
constructor() {
super();
const json = window.localStorage.getItem("session");
if (json) {
this.#session = JSON.parse(json);
}
}
loginWithPubkey(pk: string) {
this.#session = {
pubkey: pk
};
window.localStorage.setItem("session", JSON.stringify(this.#session));
this.notifyChange();
}
takeSnapshot() {
return this.#session ? { ...this.#session } : undefined;
}
}

View File

@ -54,4 +54,9 @@ header button {
display: flex;
align-items: center;
gap: 8px;
}
header .profile img {
width: 48px;
height: 48px;
}

View File

@ -1,9 +1,48 @@
import { Icon } from "element/icon";
import "./layout.css";
import { EventPublisher } from "@snort/system";
import { Outlet, useNavigate } from "react-router-dom";
import AsyncButton from "element/async-button";
import { Login } from "index";
import { useLogin } from "hooks/login";
import { Profile } from "element/profile";
export function LayoutPage() {
const navigate = useNavigate();
const login = useLogin();
async function doLogin() {
const pub = await EventPublisher.nip7();
if (pub) {
Login.loginWithPubkey(pub.pubKey);
}
}
function loggedIn() {
if (!login) return;
return <>
<button type="button" className="btn btn-primary">
New Stream
<Icon name="signal" />
</button>
<Profile pubkey={login.pubkey} options={{
showName: false
}} />
</>
}
function loggedOut() {
if (login) return;
return <>
<AsyncButton type="button" className="btn btn-border" onClick={doLogin}>
Login
<Icon name="login" />
</AsyncButton>
</>
}
return <>
<header>
<div onClick={() => navigate("/")}>
@ -14,14 +53,8 @@ export function LayoutPage() {
<Icon name="search" size={15} />
</div>
<div>
<button type="button" className="btn btn-primary">
New Stream
<Icon name="signal" />
</button>
<button type="button" className="btn btn-border">
Login
<Icon name="login" />
</button>
{loggedIn()}
{loggedOut()}
</div>
</header>
<Outlet />

View File

@ -50,10 +50,4 @@
.live-page .tags {
display: flex;
gap: 8px;
}
.live-page .btn-primary>span {
display: flex;
align-items: center;
gap: 8px;
}

View File

@ -81,7 +81,7 @@
"@jridgewell/trace-mapping" "^0.3.17"
jsesc "^2.5.1"
"@babel/helper-annotate-as-pure@^7.22.5":
"@babel/helper-annotate-as-pure@^7.18.6", "@babel/helper-annotate-as-pure@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882"
integrity sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==
@ -106,7 +106,7 @@
lru-cache "^5.1.1"
semver "^6.3.0"
"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.22.5":
"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.21.0", "@babel/helper-create-class-features-plugin@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.5.tgz#2192a1970ece4685fbff85b48da2c32fcb130b7c"
integrity sha512-xkb58MyOYIslxu3gKmVXmjTtUPvBU4odYzbiIQbWwLKIHCsx6UGZGX6F1IznMFVnDdirseUZopzN+ZRt8Xb33Q==
@ -366,6 +366,16 @@
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703"
integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==
"@babel/plugin-proposal-private-property-in-object@^7.21.11":
version "7.21.11"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz#69d597086b6760c4126525cfa154f34631ff272c"
integrity sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==
dependencies:
"@babel/helper-annotate-as-pure" "^7.18.6"
"@babel/helper-create-class-features-plugin" "^7.21.0"
"@babel/helper-plugin-utils" "^7.20.2"
"@babel/plugin-syntax-private-property-in-object" "^7.14.5"
"@babel/plugin-proposal-unicode-property-regex@^7.4.4":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz#af613d2cd5e643643b65cded64207b15c85cb78e"
@ -1771,19 +1781,19 @@
debug "^4.3.4"
dexie "^3.2.4"
"@snort/system-react@^1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@snort/system-react/-/system-react-1.0.2.tgz#980069bfeba1b6a0ea80cee7a9f4d5522b487191"
integrity sha512-K0tCf31SHOeIvmxBhNs1gl4IMZLb+jcKwbm39qv+MLHfYkKonMVjMXV5fTNJ0UFkVdnui9YXZjE/ORAaHHu/Qw==
"@snort/system-react@^1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@snort/system-react/-/system-react-1.0.3.tgz#a64ac4b96f084faab6b19264c4699d523d543927"
integrity sha512-7ZoYtmzThjOwJpM1I2UWBKfWRSdXfdZK+r59LSZFuqp5gTzAYKeqF97toQKG+PKbcNlNnqpifTIeNaZHnXkOhw==
dependencies:
"@snort/shared" "^1.0.1"
"@snort/system" "^1.0.7"
"@snort/system" "^1.0.8"
react "^18.2.0"
"@snort/system@^1.0.7":
version "1.0.7"
resolved "https://registry.yarnpkg.com/@snort/system/-/system-1.0.7.tgz#1542e17c3e880e41734e2937a819d460b832c320"
integrity sha512-PQqeR794pNlYAaDETO7Ab2mtFQz16bMeObb2eb/F09BtMalfwa3SymjZ0VqkEiT1TgGn+NkTu7bNfOEQOA+/Rg==
"@snort/system@^1.0.8":
version "1.0.8"
resolved "https://registry.yarnpkg.com/@snort/system/-/system-1.0.8.tgz#c4a7000320ebf65374298cc1ed5a0d42103a6557"
integrity sha512-IxiD4q3cnW8YEA43a42yxq/OmIeCbXR8Wd1kO59JqZA0xwhhASJQHXxWSuKPXNt6LgkEENPkmlTKY2BIkLvTfg==
dependencies:
"@noble/curves" "^1.0.0"
"@scure/base" "^1.1.1"