summaryrefslogtreecommitdiff
path: root/front/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'front/src/components')
-rw-r--r--front/src/components/Avatar.tsx53
-rw-r--r--front/src/components/Icon.tsx133
-rw-r--r--front/src/components/ProfileEditor.tsx280
-rw-r--r--front/src/components/Sigil.tsx20
-rw-r--r--front/src/components/composer/Composer.tsx9
-rw-r--r--front/src/components/layout/Sidebar.tsx46
-rw-r--r--front/src/components/modals/Modal.tsx2
-rw-r--r--front/src/components/modals/ShipModal.tsx15
-rw-r--r--front/src/components/post/Body.tsx4
-rw-r--r--front/src/components/post/Card.tsx7
-rw-r--r--front/src/components/post/External.tsx3
-rw-r--r--front/src/components/post/Footer.tsx24
-rw-r--r--front/src/components/post/Header.tsx2
-rw-r--r--front/src/components/post/Loader.tsx2
-rw-r--r--front/src/components/post/Post.tsx4
-rw-r--r--front/src/components/post/Reactions.tsx4
-rw-r--r--front/src/components/post/wrappers/Nostr.tsx2
-rw-r--r--front/src/components/post/wrappers/NostrIcon.tsx9
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>
);
}