feat: add/remove emoji packs

This commit is contained in:
2023-07-30 10:40:57 +02:00
parent 0a5623e74f
commit 249de1d9be
10 changed files with 166 additions and 122 deletions

View File

@ -140,7 +140,14 @@ export function ChatMessage({
onClick={() => setShowZapDialog(true)} onClick={() => setShowZapDialog(true)}
> >
<Profile <Profile
icon={ev.pubkey === streamer && <Icon name="signal" size={16} />} icon={
ev.pubkey === streamer && <Icon name="signal" size={16} />
// <img
// className="badge-icon"
// src="https://nostr.build/i/nostr.build_4b0d4f7293eb0f2bacb5b232a8d2ef3fe7648192d636e152a3c18b9fc06142d7.png"
// alt="TODO"
// />
}
pubkey={ev.pubkey} pubkey={ev.pubkey}
profile={profile} profile={profile}
/> />

View File

@ -4,6 +4,10 @@
justify-content: space-between; justify-content: space-between;
} }
.emoji-pack-title .name {
margin: 0;
}
.emoji-pack-title a { .emoji-pack-title a {
font-size: 14px; font-size: 14px;
} }
@ -30,3 +34,7 @@
.emoji-pack h4 { .emoji-pack h4 {
margin: 0; margin: 0;
} }
.emoji-pack .btn {
font-size: 12px;
}

View File

@ -1,17 +1,60 @@
import "./emoji-pack.css"; import "./emoji-pack.css";
import { type NostrEvent } from "@snort/system"; import { type NostrEvent } from "@snort/system";
import { unixNow } from "@snort/shared";
import AsyncButton from "element/async-button";
import { useLogin } from "hooks/login";
import { toEmojiPack } from "hooks/emoji";
import { Mention } from "element/mention"; import { Mention } from "element/mention";
import { findTag } from "utils"; import { findTag } from "utils";
import { USER_EMOJIS } from "const";
import { Login, System } from "index";
export function EmojiPack({ ev }: { ev: NostrEvent }) { export function EmojiPack({ ev }: { ev: NostrEvent }) {
const login = useLogin();
const name = findTag(ev, "d"); const name = findTag(ev, "d");
const isUsed = login.emojis.find(
(e) => e.author === ev.pubkey && e.name === name,
);
const emoji = ev.tags.filter((e) => e.at(0) === "emoji"); const emoji = ev.tags.filter((e) => e.at(0) === "emoji");
async function toggleEmojiPack() {
let newPacks = [];
if (isUsed) {
newPacks = login.emojis.filter(
(e) => e.pubkey !== ev.pubkey && e.name !== name,
);
} else {
newPacks = [...login.emojis, toEmojiPack(ev)];
}
const pub = login?.publisher();
if (pub) {
const ev = await pub.generic((eb) => {
eb.kind(USER_EMOJIS).content("");
for (const e of newPacks) {
eb.tag(["a", e.address]);
}
return eb;
});
console.debug(ev);
System.BroadcastEvent(ev);
Login.setEmojis(newPacks, unixNow());
}
}
return ( return (
<div className="emoji-pack"> <div className="emoji-pack">
<div className="emoji-pack-title"> <div className="emoji-pack-title">
<h4>{name}</h4> <div>
<Mention pubkey={ev.pubkey} /> <h4>{name}</h4>
<Mention pubkey={ev.pubkey} />
</div>
<AsyncButton
className={`btn btn-primary ${isUsed ? "delete-button" : ""}`}
onClick={toggleEmojiPack}
>
{isUsed ? "Remove" : "Add"}
</AsyncButton>
</div> </div>
<div className="emoji-pack-emojis"> <div className="emoji-pack-emojis">
{emoji.map((e) => { {emoji.map((e) => {

View File

@ -1,50 +1,47 @@
import { EventKind } from "@snort/system"; import { EventKind } from "@snort/system";
import { unixNow } from "@snort/shared";
import { useLogin } from "hooks/login"; import { useLogin } from "hooks/login";
import AsyncButton from "element/async-button"; import AsyncButton from "element/async-button";
import { System } from "index"; import { Login, System } from "index";
export function LoggedInFollowButton({ export function LoggedInFollowButton({ pubkey }: { pubkey: string }) {
pubkey,
}: {
pubkey: string;
}) {
const login = useLogin(); const login = useLogin();
const tags = login?.follows.tags ?? [] const tags = login.follows.tags;
const relays = login?.relays
const follows = tags.filter((t) => t.at(0) === "p"); const follows = tags.filter((t) => t.at(0) === "p");
const isFollowing = follows.find((t) => t.at(1) === pubkey); const isFollowing = follows.find((t) => t.at(1) === pubkey);
async function unfollow() { async function unfollow() {
const pub = login?.publisher(); const pub = login?.publisher();
if (pub) { if (pub) {
const newFollows = tags.filter((t) => t.at(1) !== pubkey);
const ev = await pub.generic((eb) => { const ev = await pub.generic((eb) => {
eb.kind(EventKind.ContactList).content(JSON.stringify(relays)); eb.kind(EventKind.ContactList).content(JSON.stringify(login.relays));
for (const t of tags) { for (const t of newFollows) {
const isFollow = t.at(0) === "p" && t.at(1) === pubkey; eb.tag(t);
if (!isFollow) {
eb.tag(t);
}
} }
return eb; return eb;
}); });
console.debug(ev); console.debug(ev);
System.BroadcastEvent(ev); System.BroadcastEvent(ev);
Login.setFollows(newFollows, unixNow());
} }
} }
async function follow() { async function follow() {
const pub = login?.publisher(); const pub = login?.publisher();
if (pub) { if (pub) {
const newFollows = [...tags, ["p", pubkey]];
const ev = await pub.generic((eb) => { const ev = await pub.generic((eb) => {
eb.kind(EventKind.ContactList).content(JSON.stringify(relays)); eb.kind(EventKind.ContactList).content(JSON.stringify(login.relays));
for (const tag of tags) { for (const tag of newFollows) {
eb.tag(tag); eb.tag(tag);
} }
eb.tag(["p", pubkey]);
return eb; return eb;
}); });
console.debug(ev); console.debug(ev);
System.BroadcastEvent(ev); System.BroadcastEvent(ev);
Login.setFollows(newFollows, unixNow());
} }
} }

View File

@ -24,10 +24,10 @@
} }
.live-chat .header .popout-link { .live-chat .header .popout-link {
color: #FFFFFF80; color: #ffffff80;
} }
.live-chat>.messages { .live-chat > .messages {
display: flex; display: flex;
gap: 12px; gap: 12px;
flex-direction: column-reverse; flex-direction: column-reverse;
@ -37,12 +37,12 @@
} }
@media (min-width: 1020px) { @media (min-width: 1020px) {
.live-chat>.messages { .live-chat > .messages {
flex-grow: 1; flex-grow: 1;
} }
} }
.live-chat>.write-message { .live-chat > .write-message {
display: flex; display: flex;
gap: 8px; gap: 8px;
margin-top: auto; margin-top: auto;
@ -51,7 +51,7 @@
border-top: 1px solid var(--border, #171717); border-top: 1px solid var(--border, #171717);
} }
.live-chat>.write-message>div:nth-child(1) { .live-chat > .write-message > div:nth-child(1) {
height: 32px; height: 32px;
flex-grow: 1; flex-grow: 1;
} }
@ -77,15 +77,15 @@
} }
.live-chat .message .profile { .live-chat .message .profile {
color: #34D2FE; color: #34d2fe;
} }
.live-chat .message.streamer .profile { .live-chat .message.streamer .profile {
color: #F838D9; color: #f838d9;
} }
.live-chat .message a { .live-chat .message a {
color: #F838D9; color: #f838d9;
} }
.live-chat .profile img { .live-chat .profile img {
@ -93,7 +93,7 @@
height: 24px; height: 24px;
} }
.live-chat .message>span { .live-chat .message > span {
font-weight: 400; font-weight: 400;
font-size: 15px; font-size: 15px;
line-height: 24px; line-height: 24px;
@ -172,13 +172,13 @@
position: relative; position: relative;
border-radius: 12px; border-radius: 12px;
border: 1px solid transparent; border: 1px solid transparent;
background: #0A0A0A; background: #0a0a0a;
background-clip: padding-box; background-clip: padding-box;
padding: 8px 12px; padding: 8px 12px;
} }
.zap-container:before { .zap-container:before {
content: ''; content: "";
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
@ -186,20 +186,28 @@
left: 0; left: 0;
z-index: -1; z-index: -1;
margin: -1px; margin: -1px;
background: linear-gradient(to bottom right, #FF902B, #F83838); background: linear-gradient(to bottom right, #ff902b, #f83838);
border-radius: inherit; border-radius: inherit;
} }
.zap-container .profile { .zap-container .profile {
color: #FF8D2B; color: #ff8d2b;
} }
.zap-container .zap-amount { .zap-container .zap-amount {
color: #FF8D2B; color: #ff8d2b;
} }
.zap-container.big-zap:before { .zap-container.big-zap:before {
background: linear-gradient(60deg, #2BD9FF, #8C8DED, #F838D9, #F83838, #FF902B, #DDF838); background: linear-gradient(
60deg,
#2bd9ff,
#8c8ded,
#f838d9,
#f83838,
#ff902b,
#ddf838
);
animation: animatedgradient 3s ease alternate infinite; animation: animatedgradient 3s ease alternate infinite;
background-size: 300% 300%; background-size: 300% 300%;
} }
@ -224,7 +232,7 @@
.zap-pill { .zap-pill {
border-radius: 100px; border-radius: 100px;
background: rgba(255, 255, 255, 0.10); background: rgba(255, 255, 255, 0.1);
width: fit-content; width: fit-content;
display: flex; display: flex;
height: 24px; height: 24px;
@ -236,7 +244,7 @@
.zap-pill-icon { .zap-pill-icon {
width: 12px; width: 12px;
height: 12px; height: 12px;
color: #FF8D2B; color: #ff8d2b;
} }
.message-zap-container { .message-zap-container {
@ -252,7 +260,7 @@
margin-top: 4px; margin-top: 4px;
width: fit-content; width: fit-content;
z-index: 1; z-index: 1;
transition: opacity .3s ease-out; transition: opacity 0.3s ease-out;
} }
@media (min-width: 1020px) { @media (min-width: 1020px) {
@ -271,7 +279,7 @@
gap: 2px; gap: 2px;
border-radius: 100px; border-radius: 100px;
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.05);
color: #FFFFFF66; color: #ffffff66;
} }
.message-zap-button:hover { .message-zap-button:hover {
@ -299,7 +307,7 @@
align-items: center; align-items: center;
gap: 2px; gap: 2px;
border-radius: 100px; border-radius: 100px;
background: rgba(255, 255, 255, 0.10); background: rgba(255, 255, 255, 0.1);
} }
.message-reaction { .message-reaction {
@ -311,7 +319,7 @@
.zap-pill-amount { .zap-pill-amount {
text-transform: lowercase; text-transform: lowercase;
color: #FFF; color: #fff;
font-size: 12px; font-size: 12px;
font-family: Outfit; font-family: Outfit;
font-style: normal; font-style: normal;
@ -335,10 +343,17 @@
} }
.write-emoji-button { .write-emoji-button {
color: #FFFFFF80; color: #ffffff80;
cursor: pointer; cursor: pointer;
} }
.write-emoji-button:hover { .write-emoji-button:hover {
color: white; color: white;
} }
.message .profile .badge-icon {
background: transparent;
width: 18px;
height: 18px;
border-radius: unset;
}

View File

@ -26,7 +26,7 @@ import { ChatMessage } from "./chat-message";
import { Goal } from "./goal"; import { Goal } from "./goal";
import { NewGoalDialog } from "./new-goal"; import { NewGoalDialog } from "./new-goal";
import { WriteMessage } from "./write-message"; import { WriteMessage } from "./write-message";
import { findTag, getHost } from "utils"; import { findTag, getTagValues, getHost } from "utils";
export interface LiveChatOptions { export interface LiveChatOptions {
canWrite?: boolean; canWrite?: boolean;
@ -80,9 +80,7 @@ export function LiveChat({
}, [feed.zaps]); }, [feed.zaps]);
const mutedPubkeys = useMemo(() => { const mutedPubkeys = useMemo(() => {
return new Set( return new Set(getTagValues(login.muted.tags, "p"));
login.muted.tags.filter((t) => t.at(0) === "p").map((t) => t.at(1)),
);
}, [login.muted.tags]); }, [login.muted.tags]);
const userEmojiPacks = login?.emojis ?? []; const userEmojiPacks = login?.emojis ?? [];
const channelEmojiPacks = useEmoji(host); const channelEmojiPacks = useEmoji(host);

View File

@ -17,7 +17,7 @@ function cleanShortcode(shortcode?: string) {
return shortcode?.replace(/\s+/g, "_").replace(/_$/, ""); return shortcode?.replace(/\s+/g, "_").replace(/_$/, "");
} }
function toEmojiPack(ev: NostrEvent): EmojiPack { export function toEmojiPack(ev: NostrEvent): EmojiPack {
const d = findTag(ev, "d") || ""; const d = findTag(ev, "d") || "";
return { return {
address: `${ev.kind}:${ev.pubkey}:${d}`, address: `${ev.kind}:${ev.pubkey}:${d}`,
@ -78,7 +78,8 @@ export function useUserEmojiPacks(
}, [relatedData]); }, [relatedData]);
const emojis = useMemo(() => { const emojis = useMemo(() => {
return uniqBy(emojiPacks.map(toEmojiPack), packId); const packs = emojiPacks.map(toEmojiPack);
return uniqBy(packs, packId);
}, [emojiPacks]); }, [emojiPacks]);
return emojis; return emojis;

View File

@ -4,15 +4,9 @@ import { EventKind, NoteCollection, RequestBuilder } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react"; import { useRequestBuilder } from "@snort/system-react";
import { useUserEmojiPacks } from "hooks/emoji"; import { useUserEmojiPacks } from "hooks/emoji";
import { USER_EMOJIS } from "const"; import { MUTED, USER_EMOJIS } from "const";
import { System, Login } from "index"; import { System, Login } from "index";
import { import { getPublisher } from "login";
getPublisher,
setMuted,
setEmojis,
setFollows,
setRelays,
} from "login";
export function useLogin() { export function useLogin() {
const session = useSyncExternalStore( const session = useSyncExternalStore(
@ -52,12 +46,7 @@ export function useLoginEvents(pubkey?: string, leaveOpen = false) {
}) })
.withFilter() .withFilter()
.authors([pubkey]) .authors([pubkey])
.kinds([ .kinds([EventKind.ContactList, EventKind.Relays, MUTED, USER_EMOJIS]);
EventKind.ContactList,
EventKind.Relays,
10_000 as EventKind,
USER_EMOJIS,
]);
return b; return b;
}, [pubkey, leaveOpen]); }, [pubkey, leaveOpen]);
@ -71,30 +60,25 @@ export function useLoginEvents(pubkey?: string, leaveOpen = false) {
if (!data) { if (!data) {
return; return;
} }
if (!session) {
return;
}
for (const ev of data) { for (const ev of data) {
if (ev?.kind === USER_EMOJIS) { if (ev?.kind === USER_EMOJIS) {
setUserEmojis(ev.tags); setUserEmojis(ev.tags);
} }
if (ev?.kind === 10_000) { if (ev?.kind === MUTED) {
// todo: decrypt ev.content tags // todo: decrypt ev.content tags
setMuted(session, ev.tags, ev.created_at); Login.setMuted(ev.tags, ev.created_at);
} }
if (ev?.kind === EventKind.ContactList) { if (ev?.kind === EventKind.ContactList) {
setFollows(session, ev.tags, ev.created_at); Login.setFollows(ev.tags, ev.created_at);
} }
if (ev?.kind === EventKind.Relays) { if (ev?.kind === EventKind.Relays) {
setRelays(session, ev.tags, ev.created_at); Login.setRelays(ev.tags, ev.created_at);
} }
} }
}, [session, data]); }, [data]);
const emojis = useUserEmojiPacks(pubkey, { tags: userEmojis }); const emojis = useUserEmojiPacks(pubkey, { tags: userEmojis });
useEffect(() => { useEffect(() => {
if (session) { Login.setEmojis(emojis);
setEmojis(session, emojis); }, [emojis]);
}
}, [session, emojis]);
} }

View File

@ -68,13 +68,44 @@ export class LoginStore extends ExternalStore<LoginSession | undefined> {
this.#save(); this.#save();
} }
updateSession(s: LoginSession) { takeSnapshot() {
this.#session = s; return this.#session ? { ...this.#session } : undefined;
}
setFollows(follows: Array<string>, ts: number) {
if (this.#session.follows.timestamp >= ts) {
return;
}
this.#session.follows.tags = follows;
this.#session.follows.timestamp = ts;
this.#save(); this.#save();
} }
takeSnapshot() { setEmojis(emojis: Array<EmojiPack>) {
return this.#session ? { ...this.#session } : undefined; this.#session.emojis = emojis;
this.#save();
}
setMuted(muted: Array<string[]>, ts: number) {
if (this.#session.muted.timestamp >= ts) {
return;
}
this.#session.muted.tags = muted;
this.#session.muted.timestamp = ts;
this.#save();
}
setRelays(relays: Array<string>, ts: number) {
if (this.#session.relays.timestamp >= ts) {
return;
}
this.#session.relays = relays.reduce((acc, r) => {
const [, relay] = r;
const write = r.length === 2 || r.includes("write");
const read = r.length === 2 || r.includes("read");
return { ...acc, [relay]: { read, write } };
}, {});
this.#save();
} }
#save() { #save() {
@ -100,47 +131,3 @@ export function getPublisher(session: LoginSession) {
} }
} }
} }
export function setFollows(
state: LoginSession,
follows: Array<string>,
ts: number,
) {
if (state.follows.timestamp >= ts) {
return;
}
state.follows.tags = follows;
state.follows.timestamp = ts;
}
export function setEmojis(state: LoginSession, emojis: Array<EmojiPack>) {
state.emojis = emojis;
}
export function setMuted(
state: LoginSession,
muted: Array<string[]>,
ts: number,
) {
if (state.muted.timestamp >= ts) {
return;
}
state.muted.tags = muted;
state.muted.timestamp = ts;
}
export function setRelays(
state: LoginSession,
relays: Array<string>,
ts: number,
) {
if (state.relays.timestamp >= ts) {
return;
}
state.relays = relays.reduce((acc, r) => {
const [, relay] = r;
const write = r.length === 2 || r.includes("write");
const read = r.length === 2 || r.includes("read");
return { ...acc, [relay]: { read, write } };
}, {});
}

View File

@ -90,3 +90,7 @@ export async function openFile(): Promise<File | undefined> {
elm.click(); elm.click();
}); });
} }
export function getTagValues(tags: Array<string[]>, tag: string) {
return tags.filter((t) => t.at(0) === tag).map((t) => t.at(1));
}