diff options
Diffstat (limited to 'front/src/components')
-rw-r--r-- | front/src/components/Avatar.tsx | 53 | ||||
-rw-r--r-- | front/src/components/Icon.tsx | 133 | ||||
-rw-r--r-- | front/src/components/ProfileEditor.tsx | 280 | ||||
-rw-r--r-- | front/src/components/Sigil.tsx | 20 | ||||
-rw-r--r-- | front/src/components/composer/Composer.tsx | 9 | ||||
-rw-r--r-- | front/src/components/layout/Sidebar.tsx | 46 | ||||
-rw-r--r-- | front/src/components/modals/Modal.tsx | 2 | ||||
-rw-r--r-- | front/src/components/modals/ShipModal.tsx | 15 | ||||
-rw-r--r-- | front/src/components/post/Body.tsx | 4 | ||||
-rw-r--r-- | front/src/components/post/Card.tsx | 7 | ||||
-rw-r--r-- | front/src/components/post/External.tsx | 3 | ||||
-rw-r--r-- | front/src/components/post/Footer.tsx | 24 | ||||
-rw-r--r-- | front/src/components/post/Header.tsx | 2 | ||||
-rw-r--r-- | front/src/components/post/Loader.tsx | 2 | ||||
-rw-r--r-- | front/src/components/post/Post.tsx | 4 | ||||
-rw-r--r-- | front/src/components/post/Reactions.tsx | 4 | ||||
-rw-r--r-- | front/src/components/post/wrappers/Nostr.tsx | 2 | ||||
-rw-r--r-- | front/src/components/post/wrappers/NostrIcon.tsx | 9 |
18 files changed, 511 insertions, 108 deletions
diff --git a/front/src/components/Avatar.tsx b/front/src/components/Avatar.tsx index 35b4386..0f3dc90 100644 --- a/front/src/components/Avatar.tsx +++ b/front/src/components/Avatar.tsx @@ -2,25 +2,41 @@ import useLocalState from "@/state/state"; import type { Ship } from "@/types/urbit"; import Sigil from "./Sigil"; import ShipModal from "./modals/ShipModal"; +import { isValidPatp } from "urbit-ob"; +import type { UserProfile } from "@/types/nostrill"; +import Icon from "@/components/Icon"; export default function ({ p, size, color, noClickOnName, + profile, + picOnly = false, }: { p: Ship; size: number; color?: string; noClickOnName?: boolean; + profile?: UserProfile; + picOnly?: boolean; }) { - const { setModal } = useLocalState(); + const { setModal } = useLocalState((s) => ({ setModal: s.setModal })); // TODO revisit this when %whom updates + const avatarInner = profile ? ( + <img src={profile.picture} /> + ) : isValidPatp(p) ? ( + <Sigil patp={p} size={size} bg={color} /> + ) : ( + <Icon name="comet" /> + ); const avatar = ( - <div className="avatar-w sigil cp" role="link" onClick={openModal}> - <Sigil patp={p} size={size} color={color} /> + <div className="avatar cp" onClick={openModal}> + {avatarInner} </div> ); + if (picOnly) return avatar; + const tooLong = (s: string) => (s.length > 15 ? " too-long" : ""); function openModal(e: React.MouseEvent) { if (noClickOnName) return; @@ -29,31 +45,12 @@ export default function ({ } const name = ( <div className="name cp" role="link" onMouseUp={openModal}> - <p className={"p-only" + tooLong(p)}>{p.length > 28 ? "Anon" : p}</p> - </div> - ); - return ( - <div className="ship-avatar"> - {avatar} - {name} - </div> - ); -} - -export function SigilOnly({ p, size, color }: any) { - const { setModal } = useLocalState(); - function openModal(e: React.MouseEvent) { - e.stopPropagation(); - setModal(<ShipModal ship={p} />); - } - return ( - <div - className="avatar-w sigil cp" - role="link" - onClick={openModal} - onMouseUp={openModal} - > - <Sigil patp={p} size={size} color={color} /> + {profile ? ( + <p>{profile.name}</p> + ) : ( + <p className={"p-only" + tooLong(p)}>{p.length > 28 ? "Anon" : p}</p> + )} </div> ); + return <div className="ship-avatar">{name}</div>; } diff --git a/front/src/components/Icon.tsx b/front/src/components/Icon.tsx new file mode 100644 index 0000000..a316e08 --- /dev/null +++ b/front/src/components/Icon.tsx @@ -0,0 +1,133 @@ +import { useTheme } from "@/styles/ThemeProvider"; + +import bellSvg from "@/assets/icons/bell.svg"; +import cometSvg from "@/assets/icons/comet.svg"; +import copySvg from "@/assets/icons/copy.svg"; +import crowSvg from "@/assets/icons/crow.svg"; +import emojiSvg from "@/assets/icons/emoji.svg"; +import homeSvg from "@/assets/icons/home.svg"; +import keySvg from "@/assets/icons/key.svg"; +import messagesSvg from "@/assets/icons/messages.svg"; +import nostrSvg from "@/assets/icons/nostr.svg"; +import palsSvg from "@/assets/icons/pals.svg"; +import profileSvg from "@/assets/icons/profile.svg"; +import quoteSvg from "@/assets/icons/quote.svg"; +import radioSvg from "@/assets/icons/radio.svg"; +import replySvg from "@/assets/icons/reply.svg"; +import repostSvg from "@/assets/icons/rt.svg"; +import rumorsSvg from "@/assets/icons/rumors.svg"; +import settingsSvg from "@/assets/icons/settings.svg"; +import youtubeSvg from "@/assets/icons/youtube.svg"; + +export type IconName = + | "bell" + | "comet" + | "copy" + | "crow" + | "emoji" + | "home" + | "key" + | "messages" + | "nostr" + | "pals" + | "profile" + | "quote" + | "radio" + | "reply" + | "repost" + | "rumors" + | "settings" + | "youtube"; + +const iconMap: Record<IconName, string> = { + bell: bellSvg, + comet: cometSvg, + copy: copySvg, + crow: crowSvg, + emoji: emojiSvg, + home: homeSvg, + key: keySvg, + messages: messagesSvg, + nostr: nostrSvg, + pals: palsSvg, + profile: profileSvg, + quote: quoteSvg, + radio: radioSvg, + reply: replySvg, + repost: repostSvg, + rumors: rumorsSvg, + settings: settingsSvg, + youtube: youtubeSvg, +}; + +interface IconProps { + name: IconName; + size?: number; + className?: string; + title?: string; + onClick?: (e?: React.MouseEvent) => void; + color?: "primary" | "text" | "textSecondary" | "textMuted" | "custom"; + customColor?: string; +} + +const Icon: React.FC<IconProps> = ({ + name, + size = 20, + className = "", + title, + onClick, + color = "text", + customColor, +}) => { + const { theme } = useTheme(); + + // Simple filter based on theme - icons should match text + const getFilter = () => { + // For dark themes, invert the black SVGs to white + if (theme.name === "dark" || theme.name === "noir" || theme.name === "gruvbox") { + return "invert(1)"; + } + // For light themes with dark text, keep as is + if (theme.name === "light") { + return "none"; + } + // For colored themes, adjust brightness/contrast + if (theme.name === "sepia") { + return "sepia(1) saturate(2) hue-rotate(20deg) brightness(0.8)"; + } + if (theme.name === "ocean") { + return "brightness(0) saturate(100%) invert(13%) sepia(95%) saturate(3207%) hue-rotate(195deg) brightness(94%) contrast(106%)"; + } + if (theme.name === "forest") { + return "brightness(0) saturate(100%) invert(24%) sepia(95%) saturate(1352%) hue-rotate(87deg) brightness(92%) contrast(96%)"; + } + return "none"; + }; + + const iconUrl = iconMap[name]; + + if (!iconUrl) { + console.error(`Icon "${name}" not found`); + return null; + } + + return ( + <img + src={iconUrl} + className={`icon ${className}`} + onClick={onClick} + title={title} + alt={title || name} + style={{ + width: size, + height: size, + display: "inline-block", + cursor: onClick ? "pointer" : "default", + filter: getFilter(), + transition: "filter 0.2s ease", + }} + /> + ); +}; + +export default Icon;
\ No newline at end of file diff --git a/front/src/components/ProfileEditor.tsx b/front/src/components/ProfileEditor.tsx new file mode 100644 index 0000000..9a7493f --- /dev/null +++ b/front/src/components/ProfileEditor.tsx @@ -0,0 +1,280 @@ +import { useState, useEffect } from "react"; +import type { UserProfile } from "@/types/nostrill"; +import useLocalState from "@/state/state"; +import Icon from "@/components/Icon"; +import toast from "react-hot-toast"; +import Avatar from "./Avatar"; + +interface ProfileEditorProps { + ship: string; + onSave?: () => void; +} + +const ProfileEditor: React.FC<ProfileEditorProps> = ({ ship, onSave }) => { + const { api, profiles } = useLocalState((s) => ({ + api: s.api, + profiles: s.profiles, + })); + const isOwnProfile = ship === api?.airlock.our; + + // Initialize state with existing profile or defaults + const existingProfile = profiles.get(ship); + const [name, setName] = useState(existingProfile?.name || ""); + const [picture, setPicture] = useState(existingProfile?.picture || ""); + const [about, setAbout] = useState(existingProfile?.about || ""); + const [customFields, setCustomFields] = useState< + Array<{ key: string; value: string }> + >( + Object.entries(existingProfile?.other || {}).map(([key, value]) => ({ + key, + value, + })), + ); + const [isEditing, setIsEditing] = useState(false); + const [isSaving, setIsSaving] = useState(false); + + useEffect(() => { + const profile = profiles.get(ship); + if (profile) { + setName(profile.name || ""); + setPicture(profile.picture || ""); + setAbout(profile.about || ""); + setCustomFields( + Object.entries(profile.other || {}).map(([key, value]) => ({ + key, + value, + })), + ); + } + }, [ship, profiles]); + + const handleAddCustomField = () => { + setCustomFields([...customFields, { key: "", value: "" }]); + }; + + const handleUpdateCustomField = ( + index: number, + field: "key" | "value", + newValue: string, + ) => { + const updated = [...customFields]; + updated[index][field] = newValue; + setCustomFields(updated); + }; + + const handleRemoveCustomField = (index: number) => { + setCustomFields(customFields.filter((_, i) => i !== index)); + }; + + const handleSave = async () => { + setIsSaving(true); + try { + // Convert custom fields array to object + const other: Record<string, string> = {}; + customFields.forEach(({ key, value }) => { + if (key.trim()) { + other[key.trim()] = value; + } + }); + + const profile: UserProfile = { + name, + picture, + about, + other, + }; + + // Call API to save profile + if (api && typeof api.createProfile === "function") { + await api.createProfile(profile); + } else { + throw new Error("Profile update API not available"); + } + + toast.success("Profile updated successfully"); + setIsEditing(false); + onSave?.(); + } catch (error) { + toast.error("Failed to update profile"); + console.error("Failed to save profile:", error); + } finally { + setIsSaving(false); + } + }; + + const handleCancel = () => { + // Reset to original values + const profile = profiles.get(ship); + if (profile) { + setName(profile.name || ""); + setPicture(profile.picture || ""); + setAbout(profile.about || ""); + setCustomFields( + Object.entries(profile.other || {}).map(([key, value]) => ({ + key, + value, + })), + ); + } + setIsEditing(false); + }; + + if (!isOwnProfile) { + // View-only mode for other users' profiles - no editing allowed + return ( + <div className="profile-editor view-mode"> + <div className="profile-picture"> + <Avatar p={ship} size={120} picOnly={true} /> + </div> + <div className="profile-info"> + <h2>{name || ship}</h2> + {about && <p className="profile-about">{about}</p>} + + {customFields.length > 0 && ( + <div className="profile-custom-fields"> + <h4>Additional Info</h4> + {customFields.map(({ key, value }, index) => ( + <div key={index} className="custom-field-view"> + <span className="field-key">{key}:</span> + <span className="field-value">{value}</span> + </div> + ))} + </div> + )} + </div> + </div> + ); + } + + return ( + <div className="profile-editor"> + <div className="profile-header"> + <h2>Edit Profile</h2> + {isOwnProfile && !isEditing && ( + <button onClick={() => setIsEditing(true)} className="edit-btn"> + <Icon name="settings" size={16} /> + Edit + </button> + )} + </div> + + {isEditing ? ( + <div className="profile-form"> + <div className="form-group"> + <label htmlFor="name">Display Name</label> + <input + id="name" + type="text" + value={name} + onChange={(e) => setName(e.target.value)} + placeholder="Your display name" + /> + </div> + + <div className="form-group"> + <label htmlFor="picture">Profile Picture URL</label> + <input + id="picture" + type="url" + value={picture} + onChange={(e) => setPicture(e.target.value)} + placeholder="https://example.com/avatar.jpg" + /> + <div className="picture-preview"> + <Avatar p={ship} size={54} picOnly={true} /> + </div> + </div> + + <div className="form-group"> + <label htmlFor="about">About</label> + <textarea + id="about" + value={about} + onChange={(e) => setAbout(e.target.value)} + placeholder="Tell us about yourself..." + rows={4} + /> + </div> + + <div className="form-group custom-fields"> + <label>Custom Fields</label> + {customFields.map((field, index) => ( + <div key={index} className="custom-field-row"> + <input + type="text" + value={field.key} + onChange={(e) => + handleUpdateCustomField(index, "key", e.target.value) + } + placeholder="Field name" + className="field-key-input" + /> + <input + type="text" + value={field.value} + onChange={(e) => + handleUpdateCustomField(index, "value", e.target.value) + } + placeholder="Field value" + className="field-value-input" + /> + <button + onClick={() => handleRemoveCustomField(index)} + className="remove-field-btn" + title="Remove field" + > + × + </button> + </div> + ))} + <button onClick={handleAddCustomField} className="add-field-btn"> + + Add Custom Field + </button> + </div> + + <div className="form-actions"> + <button + onClick={handleSave} + disabled={isSaving} + className="save-btn" + > + {isSaving ? "Saving..." : "Save Profile"} + </button> + <button + onClick={handleCancel} + disabled={isSaving} + className="cancel-btn" + > + Cancel + </button> + </div> + </div> + ) : ( + <div className="profile-view"> + <div className="profile-picture"> + <Avatar p={ship} size={120} picOnly={true} /> + </div> + + <div className="profile-info"> + <h3>{name || ship}</h3> + {about && <p className="profile-about">{about}</p>} + + {customFields.length > 0 && ( + <div className="profile-custom-fields"> + <h4>Additional Info</h4> + {customFields.map(({ key, value }, index) => ( + <div key={index} className="custom-field-view"> + <span className="field-key">{key}:</span> + <span className="field-value">{value}</span> + </div> + ))} + </div> + )} + </div> + </div> + )} + </div> + ); +}; + +export default ProfileEditor; diff --git a/front/src/components/Sigil.tsx b/front/src/components/Sigil.tsx index 4978a72..cbc2e57 100644 --- a/front/src/components/Sigil.tsx +++ b/front/src/components/Sigil.tsx @@ -1,4 +1,4 @@ -import comet from "@/assets/icons/comet.svg"; +import Icon from "@/components/Icon"; import { auraToHex } from "@/logic/utils"; import { isValidPatp } from "urbit-ob"; import { sigil } from "urbit-sigils"; @@ -7,19 +7,19 @@ import { reactRenderer } from "urbit-sigils"; interface SigilProps { patp: string; size: number; - color?: string; + bg?: string; + fg?: string; } const Sigil = (props: SigilProps) => { - const color = props.color ? auraToHex(props.color) : "black"; - if (!isValidPatp(props.patp)) return <div className="sigil bad-sigil">X</div>; - else if (props.patp.length > 28) + const bg = props.bg ? auraToHex(props.bg) : "var(--color-background)"; + const fg = props.fg ? auraToHex(props.fg) : "var(--color-primary)"; + if (props.patp.length > 28) return ( - <img + <Icon + name="comet" + size={props.size} className="comet-icon" - src={comet} - alt="" - style={{ width: `${props.size}px`, height: `${props.size}px` }} /> ); else if (props.patp.length > 15) @@ -41,7 +41,7 @@ const Sigil = (props: SigilProps) => { patp: props.patp, renderer: reactRenderer, size: props.size, - colors: [color, "white"], + colors: [bg, fg], })} </> ); diff --git a/front/src/components/composer/Composer.tsx b/front/src/components/composer/Composer.tsx index 795188e..daa5af6 100644 --- a/front/src/components/composer/Composer.tsx +++ b/front/src/components/composer/Composer.tsx @@ -15,7 +15,10 @@ function Composer({ replying?: Poast; }) { const [loc, navigate] = useLocation(); - const { api, composerData } = useLocalState(); + const { api, composerData } = useLocalState((s) => ({ + api: s.api, + composerData: s.composerData, + })); const our = api!.airlock.our!; const [input, setInput] = useState(replying ? `${replying}: ` : ""); async function poast(e: FormEvent<HTMLFormElement>) { @@ -44,8 +47,8 @@ function Composer({ const placeHolder = isAnon ? "> be me" : "What's going on in Urbit"; return ( <form id="composer" onSubmit={poast}> - <div className="sigil"> - <Sigil patp={our} size={48} /> + <div className="sigil avatar"> + <Sigil patp={our} size={46} /> </div> {composerData && composerData.type === "reply" && ( diff --git a/front/src/components/layout/Sidebar.tsx b/front/src/components/layout/Sidebar.tsx index 4055454..d237fb5 100644 --- a/front/src/components/layout/Sidebar.tsx +++ b/front/src/components/layout/Sidebar.tsx @@ -1,20 +1,13 @@ import { RADIO, versionNum } from "@/logic/constants"; import { useLocation } from "wouter"; import useLocalState from "@/state/state"; -import key from "@/assets/icons/key.svg"; import logo from "@/assets/icons/logo.png"; -import home from "@/assets/icons/home.svg"; -import bell from "@/assets/icons/bell.svg"; -import settings from "@/assets/icons/settings.svg"; -import messages from "@/assets/icons/messages.svg"; -import profile from "@/assets/icons/profile.svg"; -import pals from "@/assets/icons/pals.svg"; -import rumors from "@/assets/icons/rumors.svg"; +import Icon from "@/components/Icon"; import { ThemeSwitcher } from "@/styles/ThemeSwitcher"; function SlidingMenu() { const [_, navigate] = useLocation(); - const { api } = useLocalState(); + const { api } = useLocalState((s) => ({ api: s.api })); function goto(to: string) { navigate(to); } @@ -26,21 +19,25 @@ function SlidingMenu() { </div> <h3>Feeds</h3> <div className="opt" role="link" onClick={() => goto(`/feed/global`)}> - <img src={home} alt="" /> + <Icon name="home" size={20} /> <div>Home</div> </div> <div className="opt" role="link" onClick={() => goto(`/hark`)}> - <img src={bell} alt="" /> + <Icon name="bell" size={20} /> <div>Activity</div> </div> <hr /> - <div className="opt" role="link" onClick={() => goto("/chat")}> - <img src={messages} alt="" /> + <div + className="opt tbd" + role="link" + // onClick={() => goto("/chat")} + > + <Icon name="messages" size={20} /> <div>Messages</div> </div> <div className="opt" role="link" onClick={() => goto("/pals")}> - <img src={pals} alt="" /> + <Icon name="pals" size={20} /> <div>Pals</div> </div> <hr /> @@ -49,29 +46,12 @@ function SlidingMenu() { role="link" onClick={() => goto(`/feed/${api!.airlock.our}`)} > - <img src={profile} alt="" /> + <Icon name="profile" size={20} /> <div>Profile</div> </div> - <div className="opt" role="link" onClick={() => goto("/feed/anon")}> - <img src={rumors} alt="" /> - <div>Rumors</div> - </div> - <hr /> - <div className="opt" role="link" onClick={() => goto("/radio")}> - <div className="img">{RADIO}</div> - <div>Radio</div> - </div> <hr /> - <div - className="opt" - role="link" - onClick={() => (window.location.href = "/cookies")} - > - <img src={key} alt="" /> - <div>Logins</div> - </div> <div className="opt" role="link" onClick={() => goto("/sets")}> - <img src={settings} alt="" /> + <Icon name="settings" size={20} /> <div>Settings</div> </div> <ThemeSwitcher /> diff --git a/front/src/components/modals/Modal.tsx b/front/src/components/modals/Modal.tsx index 7dd688c..e7bae78 100644 --- a/front/src/components/modals/Modal.tsx +++ b/front/src/components/modals/Modal.tsx @@ -2,7 +2,7 @@ import useLocalState from "@/state/state"; import { useEffect, useRef, useState } from "react"; function Modal({ children }: any) { - const { setModal } = useLocalState(); + const { setModal } = useLocalState((s) => ({ setModal: s.setModal })); function onKey(event: any) { if (event.key === "Escape") setModal(null); } diff --git a/front/src/components/modals/ShipModal.tsx b/front/src/components/modals/ShipModal.tsx index 86bffbb..e823a3a 100644 --- a/front/src/components/modals/ShipModal.tsx +++ b/front/src/components/modals/ShipModal.tsx @@ -1,13 +1,16 @@ import type { Ship } from "@/types/urbit"; import Modal from "./Modal"; import Avatar from "../Avatar"; -import copyIcon from "@/assets/icons/copy.svg"; +import Icon from "@/components/Icon"; import useLocalState from "@/state/state"; import { useLocation } from "wouter"; import toast from "react-hot-toast"; export default function ({ ship }: { ship: Ship }) { - const { setModal, api } = useLocalState(); + const { setModal, api } = useLocalState((s) => ({ + setModal: s.setModal, + api: s.api, + })); const [_, navigate] = useLocation(); function close() { setModal(null); @@ -22,12 +25,12 @@ export default function ({ ship }: { ship: Ship }) { <div id="ship-modal"> <div className="flex"> <Avatar p={ship} size={60} /> - <img + <Icon + name="copy" + size={20} className="copy-icon cp" - role="link" onClick={copy} - src={copyIcon} - alt="" + title="Copy ship name" /> </div> <div className="buttons f1"> diff --git a/front/src/components/post/Body.tsx b/front/src/components/post/Body.tsx index 2e4e2f8..e8b659c 100644 --- a/front/src/components/post/Body.tsx +++ b/front/src/components/post/Body.tsx @@ -6,7 +6,7 @@ import type { Media as MediaType, ExternalContent, } from "@/types/trill"; -import crow from "@/assets/icons/crow.svg"; +import Icon from "@/components/Icon"; import type { PostProps } from "./Post"; import Media from "./Media"; import JSONContent, { YoutubeSnippet } from "./External"; @@ -168,7 +168,7 @@ function Ref({ r, nest }: { r: Reference; nest: number }) { nest: nest + 1, className: "quote-in-post", })(Quote); - return <Card logo={crow}>{comp}</Card>; + return <Card logo="crow">{comp}</Card>; } return <></>; } diff --git a/front/src/components/post/Card.tsx b/front/src/components/post/Card.tsx index 37f4911..9309423 100644 --- a/front/src/components/post/Card.tsx +++ b/front/src/components/post/Card.tsx @@ -1,8 +1,11 @@ -export default function ({ children, logo, cn}: { cn?: string; logo: string; children: any }) { +import Icon from "@/components/Icon"; +import type { IconName } from "@/components/Icon"; + +export default function ({ children, logo, cn}: { cn?: string; logo: IconName; children: any }) { const className = "trill-post-card" + (cn ? ` ${cn}`: "") return ( <div className={className}> - <img src={logo} alt="" className="trill-post-card-logo" /> + <Icon name={logo} size={20} className="trill-post-card-logo" /> {children} </div> ); diff --git a/front/src/components/post/External.tsx b/front/src/components/post/External.tsx index 0ea1500..d52aec7 100644 --- a/front/src/components/post/External.tsx +++ b/front/src/components/post/External.tsx @@ -1,5 +1,4 @@ import type { ExternalContent } from "@/types/trill"; -import youtube from "@/assets/icons/youtube.svg"; import Card from "./Card"; interface JSONProps { @@ -32,7 +31,7 @@ export function YoutubeSnippet({ href, id }: { href: string; id: string }) { const thumbnail = `https://i.ytimg.com/vi/${id}/hqdefault.jpg`; // todo styiling return ( - <Card logo={youtube} cn="youtube-thumbnail"> + <Card logo="youtube" cn="youtube-thumbnail"> <a href={href}> <img src={thumbnail} alt="" /> </a> diff --git a/front/src/components/post/Footer.tsx b/front/src/components/post/Footer.tsx index 3b48241..3e4bbdc 100644 --- a/front/src/components/post/Footer.tsx +++ b/front/src/components/post/Footer.tsx @@ -1,7 +1,5 @@ import type { PostProps } from "./Post"; -import reply from "@/assets/icons/reply.svg"; -import quote from "@/assets/icons/quote.svg"; -import repost from "@/assets/icons/rt.svg"; +import Icon from "@/components/Icon"; import { useState } from "react"; import useLocalState from "@/state/state"; import { useLocation } from "wouter"; @@ -15,7 +13,11 @@ function Footer({ poast, refetch }: PostProps) { const [_showMenu, setShowMenu] = useState(false); const [location, navigate] = useLocation(); const [reposting, _setReposting] = useState(false); - const { api, setComposerData, setModal } = useLocalState(); + const { api, setComposerData, setModal } = useLocalState((s) => ({ + api: s.api, + setComposerData: s.setComposerData, + setModal: s.setModal, + })); const our = api!.airlock.our!; function doReply(e: React.MouseEvent) { e.stopPropagation(); @@ -126,13 +128,13 @@ function Footer({ poast, refetch }: PostProps) { <span role="link" onMouseUp={showReplyCount} className="reply-count"> {displayCount(childrenCount)} </span> - <img role="link" onMouseUp={doReply} src={reply} alt="" /> + <Icon name="reply" size={20} onClick={doReply} /> </div> <div className="icon"> <span role="link" onMouseUp={showQuoteCount} className="quote-count"> {displayCount(poast.engagement.quoted.length)} </span> - <img role="link" onMouseUp={doQuote} src={quote} alt="" /> + <Icon name="quote" size={20} onClick={doQuote} /> </div> <div className="icon"> <span @@ -145,15 +147,15 @@ function Footer({ poast, refetch }: PostProps) { {reposting ? ( <p>...</p> ) : myRP ? ( - <img - role="link" + <Icon + name="repost" + size={20} className="my-rp" - onMouseUp={cancelRP} - src={repost} + onClick={cancelRP} title="cancel repost" /> ) : ( - <img role="link" onMouseUp={sendRP} src={repost} title="repost" /> + <Icon name="repost" size={20} onClick={sendRP} title="repost" /> )} </div> <div className="icon" role="link" onMouseUp={doReact}> diff --git a/front/src/components/post/Header.tsx b/front/src/components/post/Header.tsx index e541fa5..4e72fe8 100644 --- a/front/src/components/post/Header.tsx +++ b/front/src/components/post/Header.tsx @@ -4,7 +4,7 @@ import { useLocation } from "wouter"; import useLocalState from "@/state/state"; function Header(props: PostProps) { const [_, navigate] = useLocation(); - const { profiles } = useLocalState(); + const profiles = useLocalState((s) => s.profiles); const profile = profiles.get(props.poast.author); // console.log("profile", profile); // console.log(props.poast.author.length, "length"); diff --git a/front/src/components/post/Loader.tsx b/front/src/components/post/Loader.tsx index f3c4715..a23bea1 100644 --- a/front/src/components/post/Loader.tsx +++ b/front/src/components/post/Loader.tsx @@ -14,7 +14,7 @@ function PostData(props: { nest?: number; // nested quotes className?: string; }) { - const { api } = useLocalState(); + const { api } = useLocalState((s) => ({ api: s.api })); const { host, id, nest } = props; const [enest, setEnest] = useState(nest); useEffect(() => { diff --git a/front/src/components/post/Post.tsx b/front/src/components/post/Post.tsx index e61efb0..277c119 100644 --- a/front/src/components/post/Post.tsx +++ b/front/src/components/post/Post.tsx @@ -47,7 +47,7 @@ export default Post; function TrillPost(props: PostProps) { const { poast, profile, fake } = props; - const { setModal } = useLocalState(); + const setModal = useLocalState((s) => s.setModal); const [_, navigate] = useLocation(); function openThread(_e: React.MouseEvent) { const sel = window.getSelection()?.toString(); @@ -64,7 +64,7 @@ function TrillPost(props: PostProps) { </div> ) : ( <div className="avatar sigil cp" role="link" onMouseUp={openModal}> - <Sigil patp={poast.author} size={42} /> + <Sigil patp={poast.author} size={46} /> </div> ); return ( diff --git a/front/src/components/post/Reactions.tsx b/front/src/components/post/Reactions.tsx index 58662cd..aabab61 100644 --- a/front/src/components/post/Reactions.tsx +++ b/front/src/components/post/Reactions.tsx @@ -14,7 +14,7 @@ import soy from "@/assets/reacts/soy.png"; import chad from "@/assets/reacts/chad.png"; import pika from "@/assets/reacts/pika.png"; import facepalm from "@/assets/reacts/facepalm.png"; -import emoji from "@/assets/icons/emoji.svg"; +import Icon from "@/components/Icon"; import emojis from "@/logic/emojis.json"; import Modal from "../modals/Modal"; import useLocalState from "@/state/state"; @@ -93,7 +93,7 @@ export function stringToReact(s: string) { if (s === "pepesad") return <img className="react-img" src={pepesad} alt="" />; if (s === "") - return <img className="react-img no-react" src={emoji} alt="" />; + return <Icon name="emoji" size={20} className="react-img no-react" />; if (s === "cringe") return <img className="react-img" src={cringe} alt="" />; if (s === "cry") return <img className="react-img" src={cry} alt="" />; if (s === "crywojak") return <img className="react-img" src={cry} alt="" />; diff --git a/front/src/components/post/wrappers/Nostr.tsx b/front/src/components/post/wrappers/Nostr.tsx index bdc5ba9..2782fb8 100644 --- a/front/src/components/post/wrappers/Nostr.tsx +++ b/front/src/components/post/wrappers/Nostr.tsx @@ -4,7 +4,7 @@ import useLocalState from "@/state/state"; export default NostrPost; function NostrPost({ data }: { data: NostrPost }) { - const { profiles } = useLocalState(); + const { profiles } = useLocalState((s) => ({ profiles: s.profiles })); const profile = profiles.get(data.event.pubkey); return <Post poast={data.post} profile={profile} />; diff --git a/front/src/components/post/wrappers/NostrIcon.tsx b/front/src/components/post/wrappers/NostrIcon.tsx index 0c368fb..30fbfe9 100644 --- a/front/src/components/post/wrappers/NostrIcon.tsx +++ b/front/src/components/post/wrappers/NostrIcon.tsx @@ -1,9 +1,12 @@ -import nostrIcon from "@/assets/icons/nostr.svg"; +import Icon from "@/components/Icon"; import useLocalState from "@/state/state"; import toast from "react-hot-toast"; import type { Poast } from "@/types/trill"; export default function ({ poast }: { poast: Poast }) { - const { relays, api, keys } = useLocalState(); + const { relays, api } = useLocalState((s) => ({ + relays: s.relays, + api: s.api, + })); async function sendToRelay(e: React.MouseEvent) { e.stopPropagation(); @@ -16,7 +19,7 @@ export default function ({ poast }: { poast: Poast }) { return ( <div className="icon" role="link" onMouseUp={sendToRelay}> - <img role="link" src={nostrIcon} title="repost" /> + <Icon name="nostr" size={20} title="relay to nostr" /> </div> ); } |