User preferences #104
@ -13,148 +13,150 @@ import { default as NEvent } from "Nostr/Event";
|
||||
import { RootState } from "State/Store";
|
||||
import { HexKey, TaggedRawEvent } from "Nostr";
|
||||
import EventKind from "Nostr/EventKind";
|
||||
import { UserPreferences } from "State/Login";
|
||||
|
||||
export interface NoteFooterProps {
|
||||
related: TaggedRawEvent[],
|
||||
ev: NEvent
|
||||
related: TaggedRawEvent[],
|
||||
ev: NEvent
|
||||
}
|
||||
|
||||
export default function NoteFooter(props: NoteFooterProps) {
|
||||
const { related, ev } = props;
|
||||
const { related, ev } = props;
|
||||
|
||||
const login = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
|
||||
const author = useProfile(ev.RootPubKey)?.get(ev.RootPubKey);
|
||||
const publisher = useEventPublisher();
|
||||
const [reply, setReply] = useState(false);
|
||||
const [tip, setTip] = useState(false);
|
||||
const isMine = ev.RootPubKey === login;
|
||||
const reactions = useMemo(() => getReactions(related, ev.Id, EventKind.Reaction), [related]);
|
||||
const reposts = useMemo(() => getReactions(related, ev.Id, EventKind.Repost), [related]);
|
||||
const groupReactions = useMemo(() => {
|
||||
return reactions?.reduce((acc, { content }) => {
|
||||
let r = normalizeReaction(content);
|
||||
const amount = acc[r] || 0
|
||||
return { ...acc, [r]: amount + 1 }
|
||||
}, {
|
||||
[Reaction.Positive]: 0,
|
||||
[Reaction.Negative]: 0
|
||||
});
|
||||
}, [reactions]);
|
||||
const login = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
|
||||
const prefs = useSelector<RootState, UserPreferences>(s => s.login.preferences);
|
||||
const author = useProfile(ev.RootPubKey)?.get(ev.RootPubKey);
|
||||
const publisher = useEventPublisher();
|
||||
const [reply, setReply] = useState(false);
|
||||
const [tip, setTip] = useState(false);
|
||||
const isMine = ev.RootPubKey === login;
|
||||
const reactions = useMemo(() => getReactions(related, ev.Id, EventKind.Reaction), [related]);
|
||||
const reposts = useMemo(() => getReactions(related, ev.Id, EventKind.Repost), [related]);
|
||||
const groupReactions = useMemo(() => {
|
||||
return reactions?.reduce((acc, { content }) => {
|
||||
let r = normalizeReaction(content);
|
||||
const amount = acc[r] || 0
|
||||
return { ...acc, [r]: amount + 1 }
|
||||
}, {
|
||||
[Reaction.Positive]: 0,
|
||||
[Reaction.Negative]: 0
|
||||
});
|
||||
}, [reactions]);
|
||||
|
||||
function hasReacted(emoji: string) {
|
||||
return reactions?.some(({ pubkey, content }) => normalizeReaction(content) === emoji && pubkey === login)
|
||||
function hasReacted(emoji: string) {
|
||||
return reactions?.some(({ pubkey, content }) => normalizeReaction(content) === emoji && pubkey === login)
|
||||
}
|
||||
|
||||
function hasReposted() {
|
||||
return reposts.some(a => a.pubkey === login);
|
||||
}
|
||||
|
||||
async function react(content: string) {
|
||||
if (!hasReacted(content)) {
|
||||
let evLike = await publisher.react(ev, content);
|
||||
publisher.broadcast(evLike);
|
||||
}
|
||||
}
|
||||
|
||||
function hasReposted() {
|
||||
return reposts.some(a => a.pubkey === login);
|
||||
async function deleteEvent() {
|
||||
if (window.confirm(`Are you sure you want to delete ${ev.Id.substring(0, 8)}?`)) {
|
||||
let evDelete = await publisher.delete(ev.Id);
|
||||
publisher.broadcast(evDelete);
|
||||
}
|
||||
}
|
||||
|
||||
async function react(content: string) {
|
||||
if (!hasReacted(content)) {
|
||||
let evLike = await publisher.react(ev, content);
|
||||
publisher.broadcast(evLike);
|
||||
async function repost() {
|
||||
if (!hasReposted()) {
|
||||
if (!prefs.confirmReposts || window.confirm(`Are you sure you want to repost: ${ev.Id}`)) {
|
||||
let evRepost = await publisher.repost(ev);
|
||||
publisher.broadcast(evRepost);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteEvent() {
|
||||
if (window.confirm(`Are you sure you want to delete ${ev.Id.substring(0, 8)}?`)) {
|
||||
let evDelete = await publisher.delete(ev.Id);
|
||||
publisher.broadcast(evDelete);
|
||||
}
|
||||
}
|
||||
|
||||
async function repost() {
|
||||
if (!hasReposted()) {
|
||||
let evRepost = await publisher.repost(ev);
|
||||
publisher.broadcast(evRepost);
|
||||
}
|
||||
}
|
||||
|
||||
function tipButton() {
|
||||
let service = author?.lud16 || author?.lud06;
|
||||
if (service) {
|
||||
return (
|
||||
<>
|
||||
<div className="reaction-pill" onClick={(e) => setTip(true)}>
|
||||
<div className="reaction-pill-icon">
|
||||
<FontAwesomeIcon icon={faBolt} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function reactionIcon(content: string, reacted: boolean) {
|
||||
switch (content) {
|
||||
case Reaction.Positive: {
|
||||
return <FontAwesomeIcon icon={faHeart} />;
|
||||
}
|
||||
case Reaction.Negative: {
|
||||
return <FontAwesomeIcon icon={faThumbsDown} />;
|
||||
}
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
function repostIcon() {
|
||||
function tipButton() {
|
||||
let service = author?.lud16 || author?.lud06;
|
||||
if (service) {
|
||||
return (
|
||||
<div className={`reaction-pill ${hasReposted() ? 'reacted' : ''}`} onClick={() => repost()}>
|
||||
<>
|
||||
<div className="reaction-pill" onClick={(e) => setTip(true)}>
|
||||
<div className="reaction-pill-icon">
|
||||
<FontAwesomeIcon icon={faRepeat} />
|
||||
<FontAwesomeIcon icon={faBolt} />
|
||||
</div>
|
||||
{reposts.length > 0 && (
|
||||
<div className="reaction-pill-number">
|
||||
{formatShort(reposts.length)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
function repostIcon() {
|
||||
return (
|
||||
<>
|
||||
<div className="footer">
|
||||
<div className={`reaction-pill ${reply ? 'reacted' : ''}`} onClick={(e) => setReply(s => !s)}>
|
||||
<div className="reaction-pill-icon">
|
||||
<FontAwesomeIcon icon={faReply} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={`reaction-pill ${hasReacted('+') ? 'reacted' : ''} `} onClick={(e) => react("+")}>
|
||||
<div className="reaction-pill-icon">
|
||||
<FontAwesomeIcon icon={faHeart} />
|
||||
</div>
|
||||
<div className="reaction-pill-number">
|
||||
{formatShort(groupReactions[Reaction.Positive])}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`reaction-pill ${hasReacted('-') ? 'reacted' : ''}`} onClick={(e) => react("-")}>
|
||||
<div className="reaction-pill-icon">
|
||||
<FontAwesomeIcon icon={faThumbsDown} />
|
||||
</div>
|
||||
<div className="reaction-pill-number">
|
||||
{formatShort(groupReactions[Reaction.Negative])}
|
||||
</div>
|
||||
</div>
|
||||
{repostIcon()}
|
||||
{tipButton()}
|
||||
{isMine && (
|
||||
<div className="reaction-pill trash-icon">
|
||||
<div className="reaction-pill-icon">
|
||||
<FontAwesomeIcon icon={faTrash} onClick={(e) => deleteEvent()} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<NoteCreator
|
||||
autoFocus={true}
|
||||
replyTo={ev}
|
||||
onSend={() => setReply(false)}
|
||||
show={reply}
|
||||
/>
|
||||
<LNURLTip svc={author?.lud16 || author?.lud06} onClose={() => setTip(false)} show={tip} />
|
||||
</>
|
||||
<div className={`reaction-pill ${hasReposted() ? 'reacted' : ''}`} onClick={() => repost()}>
|
||||
<div className="reaction-pill-icon">
|
||||
<FontAwesomeIcon icon={faRepeat} />
|
||||
</div>
|
||||
{reposts.length > 0 && (
|
||||
<div className="reaction-pill-number">
|
||||
{formatShort(reposts.length)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function reactionIcons() {
|
||||
if (!prefs.enableReactions) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className={`reaction-pill ${hasReacted('+') ? 'reacted' : ''} `} onClick={(e) => react("+")}>
|
||||
<div className="reaction-pill-icon">
|
||||
<FontAwesomeIcon icon={faHeart} />
|
||||
</div>
|
||||
<div className="reaction-pill-number">
|
||||
{formatShort(groupReactions[Reaction.Positive])}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`reaction-pill ${hasReacted('-') ? 'reacted' : ''}`} onClick={(e) => react("-")}>
|
||||
<div className="reaction-pill-icon">
|
||||
<FontAwesomeIcon icon={faThumbsDown} />
|
||||
</div>
|
||||
<div className="reaction-pill-number">
|
||||
{formatShort(groupReactions[Reaction.Negative])}
|
||||
</div>
|
||||
</div>
|
||||
{repostIcon()}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="footer">
|
||||
<div className={`reaction-pill ${reply ? 'reacted' : ''}`} onClick={(e) => setReply(s => !s)}>
|
||||
<div className="reaction-pill-icon">
|
||||
<FontAwesomeIcon icon={faReply} />
|
||||
</div>
|
||||
</div>
|
||||
{reactionIcons()}
|
||||
{tipButton()}
|
||||
{isMine && (
|
||||
<div className="reaction-pill trash-icon">
|
||||
<div className="reaction-pill-icon">
|
||||
<FontAwesomeIcon icon={faTrash} onClick={(e) => deleteEvent()} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<NoteCreator
|
||||
autoFocus={true}
|
||||
replyTo={ev}
|
||||
onSend={() => setReply(false)}
|
||||
show={reply}
|
||||
/>
|
||||
<LNURLTip svc={author?.lud16 || author?.lud06} onClose={() => setTip(false)} show={tip} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -13,9 +13,15 @@ import Tag from "Nostr/Tag";
|
||||
import { MetadataCache } from "Db/User";
|
||||
import Mention from "Element/Mention";
|
||||
import TidalEmbed from "Element/TidalEmbed";
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState } from 'State/Store';
|
||||
import { UserPreferences } from 'State/Login';
|
||||
|
||||
function transformHttpLink(a: string) {
|
||||
function transformHttpLink(a: string, pref: UserPreferences) {
|
||||
try {
|
||||
if (!pref.autoLoadMedia) {
|
||||
return <a href={a} onClick={(e) => e.stopPropagation()} target="_blank" rel="noreferrer" className="ext">{a}</a>
|
||||
}
|
||||
const url = new URL(a);
|
||||
const youtubeId = YoutubeUrlRegex.test(a) && RegExp.$1;
|
||||
const tweetId = TweetUrlRegex.test(a) && RegExp.$2;
|
||||
@ -73,12 +79,12 @@ function transformHttpLink(a: string) {
|
||||
return <a href={a} onClick={(e) => e.stopPropagation()} target="_blank" rel="noreferrer" className="ext">{a}</a>
|
||||
}
|
||||
|
||||
function extractLinks(fragments: Fragment[]) {
|
||||
function extractLinks(fragments: Fragment[], pref: UserPreferences) {
|
||||
return fragments.map(f => {
|
||||
if (typeof f === "string") {
|
||||
return f.split(UrlRegex).map(a => {
|
||||
if (a.startsWith("http")) {
|
||||
return transformHttpLink(a)
|
||||
return transformHttpLink(a, pref)
|
||||
}
|
||||
return a;
|
||||
});
|
||||
@ -87,14 +93,14 @@ function extractLinks(fragments: Fragment[]) {
|
||||
}).flat();
|
||||
}
|
||||
|
||||
function extractMentions(fragments: Fragment[], tags: Tag[], users: Map<string, MetadataCache>) {
|
||||
return fragments.map(f => {
|
||||
function extractMentions(frag: TextFragment) {
|
||||
return frag.body.map(f => {
|
||||
if (typeof f === "string") {
|
||||
return f.split(MentionRegex).map((match) => {
|
||||
let matchTag = match.match(/#\[(\d+)\]/);
|
||||
if (matchTag && matchTag.length === 2) {
|
||||
let idx = parseInt(matchTag[1]);
|
||||
let ref = tags?.find(a => a.Index === idx);
|
||||
let ref = frag.tags?.find(a => a.Index === idx);
|
||||
if (ref) {
|
||||
switch (ref.Key) {
|
||||
case "p": {
|
||||
@ -149,25 +155,25 @@ function extractHashtags(fragments: Fragment[]) {
|
||||
}).flat();
|
||||
}
|
||||
|
||||
function transformLi({ body, tags, users }: TextFragment) {
|
||||
let fragments = transformText({ body, tags, users })
|
||||
function transformLi(frag: TextFragment) {
|
||||
let fragments = transformText(frag)
|
||||
return <li>{fragments}</li>
|
||||
}
|
||||
|
||||
function transformParagraph({ body, tags, users }: TextFragment) {
|
||||
const fragments = transformText({ body, tags, users })
|
||||
function transformParagraph(frag: TextFragment) {
|
||||
const fragments = transformText(frag)
|
||||
if (fragments.every(f => typeof f === 'string')) {
|
||||
return <p>{fragments}</p>
|
||||
}
|
||||
return <>{fragments}</>
|
||||
}
|
||||
|
||||
function transformText({ body, tags, users }: TextFragment) {
|
||||
if (body === undefined) {
|
||||
function transformText(frag: TextFragment) {
|
||||
if (frag.body === undefined) {
|
||||
debugger;
|
||||
}
|
||||
let fragments = extractMentions(body, tags, users);
|
||||
fragments = extractLinks(fragments);
|
||||
let fragments = extractMentions(frag);
|
||||
fragments = extractLinks(fragments, frag.pref);
|
||||
fragments = extractInvoices(fragments);
|
||||
fragments = extractHashtags(fragments);
|
||||
return fragments;
|
||||
@ -178,7 +184,8 @@ export type Fragment = string | JSX.Element;
|
||||
export interface TextFragment {
|
||||
body: Fragment[],
|
||||
tags: Tag[],
|
||||
users: Map<string, MetadataCache>
|
||||
users: Map<string, MetadataCache>,
|
||||
pref: UserPreferences
|
||||
}
|
||||
|
||||
export interface TextProps {
|
||||
@ -188,11 +195,12 @@ export interface TextProps {
|
||||
}
|
||||
|
||||
export default function Text({ content, tags, users }: TextProps) {
|
||||
const pref = useSelector<RootState, UserPreferences>(s => s.login.preferences);
|
||||
const components = useMemo(() => {
|
||||
return {
|
||||
p: (x: any) => transformParagraph({ body: x.children ?? [], tags, users }),
|
||||
a: (x: any) => transformHttpLink(x.href),
|
||||
li: (x: any) => transformLi({ body: x.children ?? [], tags, users }),
|
||||
p: (x: any) => transformParagraph({ body: x.children ?? [], tags, users, pref }),
|
||||
a: (x: any) => transformHttpLink(x.href, pref),
|
||||
li: (x: any) => transformLi({ body: x.children ?? [], tags, users, pref }),
|
||||
};
|
||||
}, [content]);
|
||||
return <ReactMarkdown className="text" components={components}>{content}</ReactMarkdown>
|
||||
|
@ -3,9 +3,13 @@ import { u256 } from "Nostr";
|
||||
import EventKind from "Nostr/EventKind";
|
||||
import { Subscriptions } from "Nostr/Subscriptions";
|
||||
import useSubscription from "Feed/Subscription";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "State/Store";
|
||||
import { UserPreferences } from "State/Login";
|
||||
|
||||
export default function useThreadFeed(id: u256) {
|
||||
const [trackingEvents, setTrackingEvent] = useState<u256[]>([id]);
|
||||
const pref = useSelector<RootState, UserPreferences>(s => s.login.preferences);
|
||||
|
||||
function addId(id: u256[]) {
|
||||
setTrackingEvent((s) => {
|
||||
@ -21,7 +25,7 @@ export default function useThreadFeed(id: u256) {
|
||||
|
||||
// get replies to this event
|
||||
const subRelated = new Subscriptions();
|
||||
subRelated.Kinds = new Set([EventKind.Reaction, EventKind.TextNote, EventKind.Deletion, EventKind.Repost]);
|
||||
subRelated.Kinds = new Set(pref.enableReactions ? [EventKind.Reaction, EventKind.TextNote, EventKind.Deletion, EventKind.Repost] : [EventKind.TextNote]);
|
||||
subRelated.ETags = thisSub.Ids;
|
||||
thisSub.AddSubscription(subRelated);
|
||||
|
||||
|
@ -4,6 +4,9 @@ import EventKind from "Nostr/EventKind";
|
||||
import { Subscriptions } from "Nostr/Subscriptions";
|
||||
import { unixNow } from "Util";
|
||||
import useSubscription from "Feed/Subscription";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "State/Store";
|
||||
import { UserPreferences } from "State/Login";
|
||||
|
||||
export interface TimelineFeedOptions {
|
||||
method: "TIME_RANGE" | "LIMIT_UNTIL"
|
||||
@ -20,6 +23,7 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
|
||||
const [until, setUntil] = useState<number>(now);
|
||||
const [since, setSince] = useState<number>(now - window);
|
||||
const [trackingEvents, setTrackingEvent] = useState<u256[]>([]);
|
||||
const pref = useSelector<RootState, UserPreferences>(s => s.login.preferences);
|
||||
|
||||
const sub = useMemo(() => {
|
||||
if (subject.type !== "global" && subject.items.length == 0) {
|
||||
@ -56,7 +60,7 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
|
||||
const main = useSubscription(sub, { leaveOpen: true });
|
||||
|
||||
const subNext = useMemo(() => {
|
||||
if (trackingEvents.length > 0) {
|
||||
if (trackingEvents.length > 0 && pref.enableReactions) {
|
||||
let sub = new Subscriptions();
|
||||
sub.Id = `timeline-related:${subject.type}`;
|
||||
sub.Kinds = new Set([EventKind.Reaction, EventKind.Deletion, EventKind.Repost]);
|
||||
|
@ -1,9 +1,15 @@
|
||||
import ProfilePreview from "Element/ProfilePreview";
|
||||
import ZapButton from "Element/ZapButton";
|
||||
import { bech32ToHex } from "Util";
|
||||
|
||||
const Developers = [
|
||||
"63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed", // kieran
|
||||
"7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194" // verbiricha
|
||||
bech32ToHex("npub1v0lxxxxutpvrelsksy8cdhgfux9l6a42hsj2qzquu2zk7vc9qnkszrqj49"), // kieran
|
||||
bech32ToHex("npub107jk7htfv243u0x5ynn43scq9wrxtaasmrwwa8lfu2ydwag6cx2quqncxg") // verbiricha
|
||||
];
|
||||
|
||||
const Contributors = [
|
||||
bech32ToHex("npub10djxr5pvdu97rjkde7tgcsjxzpdzmdguwacfjwlchvj7t88dl7nsdl54nf"), // ivan
|
||||
bech32ToHex("npub148jmlutaa49y5wl5mcll003ftj59v79vf7wuv3apcwpf75hx22vs7kk9ay"), // liran cohen
|
||||
];
|
||||
|
||||
const DonatePage = () => {
|
||||
@ -19,8 +25,10 @@ const DonatePage = () => {
|
||||
<p>
|
||||
Check out the code here: <a className="highlight" href="https://github.com/v0l/snort" rel="noreferrer" target="_blank">https://github.com/v0l/snort</a>
|
||||
</p>
|
||||
<h3>Developers</h3>
|
||||
<h3>Primary Developers</h3>
|
||||
{Developers.map(a => <ProfilePreview pubkey={a} key={a} actions={<ZapButton pubkey={a} />} />)}
|
||||
<h4>Contributors</h4>
|
||||
{Contributors.map(a => <ProfilePreview pubkey={a} key={a} actions={<ZapButton pubkey={a} />} />)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import { faBell, faMessage } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { RootState } from "State/Store";
|
||||
import { init } from "State/Login";
|
||||
import { init, UserPreferences } from "State/Login";
|
||||
import { HexKey, RawEvent, TaggedRawEvent } from "Nostr";
|
||||
import { RelaySettings } from "Nostr/Connection";
|
||||
import { System } from "Nostr/System"
|
||||
@ -23,6 +23,7 @@ export default function Layout() {
|
||||
const notifications = useSelector<RootState, TaggedRawEvent[]>(s => s.login.notifications);
|
||||
const readNotifications = useSelector<RootState, number>(s => s.login.readNotifications);
|
||||
const dms = useSelector<RootState, RawEvent[]>(s => s.login.dms);
|
||||
const prefs = useSelector<RootState, UserPreferences>(s => s.login.preferences);
|
||||
useLoginFeed();
|
||||
|
||||
useEffect(() => {
|
||||
@ -38,6 +39,15 @@ export default function Layout() {
|
||||
}
|
||||
}, [relays]);
|
||||
|
||||
useEffect(() => {
|
||||
const elm = document.documentElement;
|
||||
if (prefs.theme === "light") {
|
||||
elm.classList.add("light");
|
||||
} else {
|
||||
elm.classList.remove("light");
|
||||
}
|
||||
}, [prefs]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(init());
|
||||
}, []);
|
||||
|
@ -1,240 +1,35 @@
|
||||
import "./SettingsPage.css";
|
||||
import Nostrich from "nostrich.jpg";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faShop } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { RootState } from "State/Store";
|
||||
import { logout, setRelays } from "State/Login";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import useProfile from "Feed/ProfileFeed";
|
||||
import VoidUpload from "Feed/VoidUpload";
|
||||
import { hexToBech32, openFile } from "Util";
|
||||
import Relay from "Element/Relay";
|
||||
import Copy from "Element/Copy";
|
||||
import { HexKey, UserMetadata } from "Nostr";
|
||||
import { RelaySettings } from "Nostr/Connection";
|
||||
import { MetadataCache } from "Db/User";
|
||||
import { Outlet, RouteObject, useNavigate } from "react-router-dom";
|
||||
import SettingsIndex from "Pages/settings/Index";
|
||||
import Profile from "Pages/settings/Profile";
|
||||
import Relay from "Pages/settings/Relays";
|
||||
import Preferences from "Pages/settings/Preferences";
|
||||
|
||||
export default function SettingsPage() {
|
||||
const navigate = useNavigate();
|
||||
const id = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
|
||||
const privKey = useSelector<RootState, HexKey | undefined>(s => s.login.privateKey);
|
||||
const relays = useSelector<RootState, Record<string, RelaySettings>>(s => s.login.relays);
|
||||
const dispatch = useDispatch();
|
||||
const user = useProfile(id)?.get(id || "");
|
||||
const publisher = useEventPublisher();
|
||||
|
||||
const [name, setName] = useState<string>();
|
||||
const [displayName, setDisplayName] = useState<string>();
|
||||
const [picture, setPicture] = useState<string>();
|
||||
const [banner, setBanner] = useState<string>();
|
||||
const [about, setAbout] = useState<string>();
|
||||
const [website, setWebsite] = useState<string>();
|
||||
const [nip05, setNip05] = useState<string>();
|
||||
const [lud06, setLud06] = useState<string>();
|
||||
const [lud16, setLud16] = useState<string>();
|
||||
const [newRelay, setNewRelay] = useState<string>();
|
||||
|
||||
const avatarPicture = (picture?.length ?? 0) === 0 ? Nostrich : picture
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setName(user.name);
|
||||
setDisplayName(user.display_name)
|
||||
setPicture(user.picture);
|
||||
setBanner(user.banner);
|
||||
setAbout(user.about);
|
||||
setWebsite(user.website);
|
||||
setNip05(user.nip05);
|
||||
setLud06(user.lud06);
|
||||
setLud16(user.lud16);
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
async function saveProfile() {
|
||||
// copy user object and delete internal fields
|
||||
let userCopy = {
|
||||
...user,
|
||||
name,
|
||||
display_name: displayName,
|
||||
about,
|
||||
picture,
|
||||
banner,
|
||||
website,
|
||||
nip05,
|
||||
lud16
|
||||
};
|
||||
delete userCopy["loaded"];
|
||||
delete userCopy["created"];
|
||||
delete userCopy["pubkey"];
|
||||
console.debug(userCopy);
|
||||
|
||||
let ev = await publisher.metadata(userCopy);
|
||||
console.debug(ev);
|
||||
publisher.broadcast(ev);
|
||||
}
|
||||
|
||||
async function uploadFile() {
|
||||
let file = await openFile();
|
||||
if (file) {
|
||||
console.log(file);
|
||||
let rsp = await VoidUpload(file, file.name);
|
||||
if (!rsp?.ok) {
|
||||
throw "Upload failed, please try again later";
|
||||
}
|
||||
return rsp.file;
|
||||
}
|
||||
}
|
||||
|
||||
async function setNewAvatar() {
|
||||
const rsp = await uploadFile();
|
||||
if (rsp) {
|
||||
setPicture(rsp.meta?.url ?? `https://void.cat/d/${rsp.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function setNewBanner() {
|
||||
const rsp = await uploadFile();
|
||||
if (rsp) {
|
||||
setBanner(rsp.meta?.url ?? `https://void.cat/d/${rsp.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveRelays() {
|
||||
let ev = await publisher.saveRelays();
|
||||
publisher.broadcast(ev);
|
||||
}
|
||||
|
||||
function editor() {
|
||||
return (
|
||||
<div className="editor">
|
||||
<div className="form-group">
|
||||
<div>Name:</div>
|
||||
<div>
|
||||
<input type="text" value={name} onChange={(e) => setName(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<div>Display name:</div>
|
||||
<div>
|
||||
<input type="text" value={displayName} onChange={(e) => setDisplayName(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group f-col">
|
||||
<div>About:</div>
|
||||
<div className="w-max">
|
||||
<textarea className="w-max" onChange={(e) => setAbout(e.target.value)} value={about}></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<div>Website:</div>
|
||||
<div>
|
||||
<input type="text" value={website} onChange={(e) => setWebsite(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<div>NIP-05:</div>
|
||||
<div>
|
||||
<input type="text" className="mr10" value={nip05} onChange={(e) => setNip05(e.target.value)} />
|
||||
<div className="btn" onClick={() => navigate("/verification")}>
|
||||
<FontAwesomeIcon icon={faShop} />
|
||||
|
||||
Buy
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<div>LN Address:</div>
|
||||
<div>
|
||||
<input type="text" value={lud16} onChange={(e) => setLud16(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<div>
|
||||
<div className="btn" onClick={() => { dispatch(logout()); navigate("/"); }}>Logout</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="btn" onClick={() => saveProfile()}>Save</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function addNewRelay() {
|
||||
if ((newRelay?.length ?? 0) > 0) {
|
||||
const parsed = new URL(newRelay!);
|
||||
const payload = {
|
||||
relays: {
|
||||
...relays,
|
||||
[parsed.toString()]: { read: false, write: false }
|
||||
},
|
||||
createdAt: Math.floor(new Date().getTime() / 1000)
|
||||
};
|
||||
dispatch(setRelays(payload))
|
||||
}
|
||||
}
|
||||
|
||||
function addRelay() {
|
||||
return (
|
||||
<>
|
||||
<h4>Add Relays</h4>
|
||||
<div className="flex mb10">
|
||||
<input type="text" className="f-grow" placeholder="wss://my-relay.com" value={newRelay} onChange={(e) => setNewRelay(e.target.value)} />
|
||||
</div>
|
||||
<div className="btn mb10" onClick={() => addNewRelay()}>Add</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function settings() {
|
||||
if (!id) return null;
|
||||
return (
|
||||
<>
|
||||
<h1>Settings</h1>
|
||||
<div className="flex f-center image-settings">
|
||||
<div>
|
||||
<h2>Avatar</h2>
|
||||
<div style={{ backgroundImage: `url(${avatarPicture})` }} className="avatar">
|
||||
<div className="edit" onClick={() => setNewAvatar()}>Edit</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Header</h2>
|
||||
<div style={{ backgroundImage: `url(${(banner?.length ?? 0) === 0 ? Nostrich : banner})` }} className="banner">
|
||||
<div className="edit" onClick={() => setNewBanner()}>Edit</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{editor()}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="settings">
|
||||
{settings()}
|
||||
{privKey && (<div className="flex f-col bg-grey">
|
||||
<div>
|
||||
<h4>Private Key:</h4>
|
||||
</div>
|
||||
<div>
|
||||
<Copy text={hexToBech32("nsec", privKey)} />
|
||||
</div>
|
||||
</div>)}
|
||||
<h4>Relays</h4>
|
||||
<div className="flex f-col">
|
||||
{Object.keys(relays || {}).map(a => <Relay addr={a} key={a} />)}
|
||||
</div>
|
||||
<div className="flex actions">
|
||||
<div className="f-grow"></div>
|
||||
<div className="btn" onClick={() => saveRelays()}>Save</div>
|
||||
</div>
|
||||
{addRelay()}
|
||||
<div className="settings-page">
|
||||
<h2 onClick={() => navigate("/settings")}>Settings</h2>
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const SettingsRoutes: RouteObject[] = [
|
||||
{
|
||||
path: "",
|
||||
element: <SettingsIndex />
|
||||
},
|
||||
{
|
||||
path: "profile",
|
||||
element: <Profile />
|
||||
},
|
||||
{
|
||||
path: "relays",
|
||||
element: <Relay />
|
||||
},
|
||||
{
|
||||
path: "preferences",
|
||||
element: <Preferences />
|
||||
}
|
||||
]
|
||||
|
3
src/Pages/settings/Index.css
Normal file
@ -0,0 +1,3 @@
|
||||
verbiricha
commented
Review
```suggestion
background-color: var(--note-bg);
cursor: pointer;
```
verbiricha
commented
Review
```suggestion
background-color: var(--note-bg);
cursor: pointer;
```
|
||||
.settings-nav h3 {
|
||||
```suggestion
background-color: var(--note-bg);
cursor: pointer;
```
|
||||
background-color: var(--note-bg);
|
||||
```suggestion
background-color: var(--note-bg);
cursor: pointer;
```
|
||||
}
|
||||
```suggestion
background-color: var(--note-bg);
cursor: pointer;
```
|
31
src/Pages/settings/Index.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { faCircleDollarToSlot, faGear, faPlug, faUser } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import "./Index.css";
|
||||
|
||||
const SettingsIndex = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="settings-nav">
|
||||
<div className="card" onClick={() => navigate("profile")}>
|
||||
<FontAwesomeIcon icon={faUser} size="xl" className="mr10" />
|
||||
Profile
|
||||
</div>
|
||||
<div className="card" onClick={() => navigate("relays")}>
|
||||
<FontAwesomeIcon icon={faPlug} size="xl" className="mr10" />
|
||||
Relays
|
||||
</div>
|
||||
<div className="card" onClick={() => navigate("preferences")}>
|
||||
<FontAwesomeIcon icon={faGear} size="xl" className="mr10" />
|
||||
Preferences
|
||||
</div>
|
||||
<div className="card" onClick={() => navigate("/donate")}>
|
||||
<FontAwesomeIcon icon={faCircleDollarToSlot} size="xl" className="mr10" />
|
||||
Donate
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SettingsIndex;
|
8
src/Pages/settings/Preferences.css
Normal file
@ -0,0 +1,8 @@
|
||||
.preferences small {
|
||||
margin-top: 0.5em;
|
||||
color: var(--font-secondary-color);
|
||||
}
|
||||
|
||||
.preferences select {
|
||||
min-width: 100px;
|
||||
}
|
55
src/Pages/settings/Preferences.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
import { setPreferences, UserPreferences } from "State/Login";
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
import { RootState } from "State/Store";
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
import "./Preferences.css";
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
const PreferencesPage = () => {
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
const dispatch = useDispatch();
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
const perf = useSelector<RootState, UserPreferences>(s => s.login.preferences);
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
return (
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
<div className="preferences">
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
<h3>Preferences</h3>
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
<div className="card flex">
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
<div className="flex f-col f-grow">
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
<div>Theme</div>
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
</div>
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
<div>
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
<select value={perf.theme} onChange={e => dispatch(setPreferences({ ...perf, theme: "light" === e.target.value ? "light" : "dark" }))}>
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
<option value="light">Light</option>
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
<option value="dark">Dark</option>
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
</select>
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
</div>
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
</div>
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
<div className="card flex">
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
<div className="flex f-col f-grow">
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
<div>Automatically load media</div>
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
<small>Media in posts will automatically be shown, if disabled only the link will show</small>
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
</div>
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
<div>
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
<input type="checkbox" checked={perf.autoLoadMedia} onChange={e => dispatch(setPreferences({ ...perf, autoLoadMedia: e.target.checked }))} />
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
</div>
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
</div>
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
<div className="card flex">
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
<div className="flex f-col f-grow">
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
<div>Enable reactions</div>
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
<small>Reactions will be shown on every page, if disabled no reactions will be shown</small>
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
</div>
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
<div>
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
<input type="checkbox" checked={perf.enableReactions} onChange={e => dispatch(setPreferences({ ...perf, enableReactions: e.target.checked }))} />
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
</div>
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
</div>
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
<div className="card flex">
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
<div className="flex f-col f-grow">
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
<div>Confirm reposts</div>
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
<small>Reposts need to be manually confirmed</small>
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
</div>
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
<div>
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
<input type="checkbox" checked={perf.confirmReposts} onChange={e => dispatch(setPreferences({ ...perf, confirmReposts: e.target.checked }))} />
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
</div>
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
</div>
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
</div>
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
)
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
}
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
||||
export default PreferencesPage;
|
||||
Wdyt about keeping current default (use This would be similar to other preferences, keeping the current default but offering customization. Wdyt about keeping current default (use `system` theme)? If user explicitly sets a theme we can override it but if not use system theme. Have my devices configured to use dark/light depending on time of the day, don't want to be manually changing it from preferences.
This would be similar to other preferences, keeping the current default but offering customization.
|
@ -1,4 +1,4 @@
|
||||
import "./SettingsPage.css";
|
||||
import "./Profile.css";
|
||||
import Nostrich from "nostrich.jpg";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
@ -10,20 +10,16 @@ import { faShop } from "@fortawesome/free-solid-svg-icons";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import useProfile from "Feed/ProfileFeed";
|
||||
import VoidUpload from "Feed/VoidUpload";
|
||||
import { logout, setRelays } from "State/Login";
|
||||
import { logout } from "State/Login";
|
||||
import { hexToBech32, openFile } from "Util";
|
||||
import Relay from "Element/Relay";
|
||||
import Copy from "Element/Copy";
|
||||
import { RootState } from "State/Store";
|
||||
import { HexKey, UserMetadata } from "Nostr";
|
||||
import { RelaySettings } from "Nostr/Connection";
|
||||
import { MetadataCache } from "Db/User";
|
||||
import { HexKey } from "Nostr";
|
||||
|
||||
export default function SettingsPage() {
|
||||
export default function ProfileSettings() {
|
||||
const navigate = useNavigate();
|
||||
const id = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
|
||||
const privKey = useSelector<RootState, HexKey | undefined>(s => s.login.privateKey);
|
||||
const relays = useSelector<RootState, Record<string, RelaySettings>>(s => s.login.relays);
|
||||
const dispatch = useDispatch();
|
||||
const user = useProfile(id)?.get(id || "");
|
||||
const publisher = useEventPublisher();
|
||||
@ -37,7 +33,6 @@ export default function SettingsPage() {
|
||||
const [nip05, setNip05] = useState<string>();
|
||||
const [lud06, setLud06] = useState<string>();
|
||||
const [lud16, setLud16] = useState<string>();
|
||||
const [newRelay, setNewRelay] = useState<string>();
|
||||
|
||||
const avatarPicture = (picture?.length ?? 0) === 0 ? Nostrich : picture
|
||||
|
||||
@ -104,11 +99,6 @@ export default function SettingsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveRelays() {
|
||||
let ev = await publisher.saveRelays();
|
||||
publisher.broadcast(ev);
|
||||
}
|
||||
|
||||
function editor() {
|
||||
return (
|
||||
<div className="editor">
|
||||
@ -165,37 +155,10 @@ export default function SettingsPage() {
|
||||
)
|
||||
}
|
||||
|
||||
function addNewRelay() {
|
||||
if ((newRelay?.length ?? 0) > 0) {
|
||||
const parsed = new URL(newRelay!);
|
||||
const payload = {
|
||||
relays: {
|
||||
...relays,
|
||||
[parsed.toString()]: { read: false, write: false }
|
||||
},
|
||||
createdAt: Math.floor(new Date().getTime() / 1000)
|
||||
};
|
||||
dispatch(setRelays(payload))
|
||||
}
|
||||
}
|
||||
|
||||
function addRelay() {
|
||||
return (
|
||||
<>
|
||||
<h4>Add Relays</h4>
|
||||
<div className="flex mb10">
|
||||
<input type="text" className="f-grow" placeholder="wss://my-relay.com" value={newRelay} onChange={(e) => setNewRelay(e.target.value)} />
|
||||
</div>
|
||||
<div className="btn mb10" onClick={() => addNewRelay()}>Add</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function settings() {
|
||||
if (!id) return null;
|
||||
return (
|
||||
<>
|
||||
<h1>Settings</h1>
|
||||
<div className="flex f-center image-settings">
|
||||
<div>
|
||||
<h2>Avatar</h2>
|
||||
@ -217,6 +180,7 @@ export default function SettingsPage() {
|
||||
|
||||
return (
|
||||
<div className="settings">
|
||||
<h3>Profile</h3>
|
||||
{settings()}
|
||||
{privKey && (<div className="flex f-col bg-grey">
|
||||
<div>
|
||||
@ -226,15 +190,6 @@ export default function SettingsPage() {
|
||||
<Copy text={hexToBech32("nsec", privKey)} />
|
||||
</div>
|
||||
</div>)}
|
||||
<h4>Relays</h4>
|
||||
<div className="flex f-col">
|
||||
{Object.keys(relays || {}).map(a => <Relay addr={a} key={a} />)}
|
||||
</div>
|
||||
<div className="flex actions">
|
||||
<div className="f-grow"></div>
|
||||
<div className="btn" onClick={() => saveRelays()}>Save</div>
|
||||
</div>
|
||||
{addRelay()}
|
||||
</div>
|
||||
);
|
||||
}
|
63
src/Pages/settings/Relays.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import { useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
import Relay from "Element/Relay";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import { RootState } from "State/Store";
|
||||
import { RelaySettings } from "Nostr/Connection";
|
||||
import { setRelays } from "State/Login";
|
||||
|
||||
const RelaySettingsPage = () => {
|
||||
const dispatch = useDispatch();
|
||||
const publisher = useEventPublisher();
|
||||
const relays = useSelector<RootState, Record<string, RelaySettings>>(s => s.login.relays);
|
||||
const [newRelay, setNewRelay] = useState<string>();
|
||||
|
||||
async function saveRelays() {
|
||||
let ev = await publisher.saveRelays();
|
||||
publisher.broadcast(ev);
|
||||
}
|
||||
|
||||
|
||||
function addRelay() {
|
||||
return (
|
||||
<>
|
||||
<h4>Add Relays</h4>
|
||||
<div className="flex mb10">
|
||||
<input type="text" className="f-grow" placeholder="wss://my-relay.com" value={newRelay} onChange={(e) => setNewRelay(e.target.value)} />
|
||||
</div>
|
||||
<div className="btn mb10" onClick={() => addNewRelay()}>Add</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function addNewRelay() {
|
||||
if ((newRelay?.length ?? 0) > 0) {
|
||||
const parsed = new URL(newRelay!);
|
||||
const payload = {
|
||||
relays: {
|
||||
...relays,
|
||||
[parsed.toString()]: { read: false, write: false }
|
||||
},
|
||||
createdAt: Math.floor(new Date().getTime() / 1000)
|
||||
};
|
||||
dispatch(setRelays(payload))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3>Relays</h3>
|
||||
<div className="flex f-col">
|
||||
{Object.keys(relays || {}).map(a => <Relay addr={a} key={a} />)}
|
||||
</div>
|
||||
<div className="flex actions">
|
||||
<div className="f-grow"></div>
|
||||
<div className="btn" onClick={() => saveRelays()}>Save</div>
|
||||
</div>
|
||||
{addRelay()}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default RelaySettingsPage;
|
@ -3,12 +3,36 @@ import * as secp from '@noble/secp256k1';
|
||||
import { DefaultRelays } from 'Const';
|
||||
import { HexKey, RawEvent, TaggedRawEvent } from 'Nostr';
|
||||
import { RelaySettings } from 'Nostr/Connection';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
const PrivateKeyItem = "secret";
|
||||
const PublicKeyItem = "pubkey";
|
||||
const NotificationsReadItem = "notifications-read";
|
||||
const UserPreferencesKey = "preferences";
|
||||
|
||||
interface LoginStore {
|
||||
export interface UserPreferences {
|
||||
/**
|
||||
* Enable reactions / reposts / zaps
|
||||
*/
|
||||
enableReactions: boolean,
|
||||
|
||||
/**
|
||||
* Automatically load media (show link only) (bandwidth/privacy)
|
||||
*/
|
||||
autoLoadMedia: boolean,
|
||||
|
||||
/**
|
||||
* Select between light/dark theme
|
||||
*/
|
||||
theme: "light" | "dark",
|
||||
|
||||
/**
|
||||
* Ask for confirmation when reposting notes
|
||||
*/
|
||||
confirmReposts: boolean
|
||||
}
|
||||
|
||||
export interface LoginStore {
|
||||
/**
|
||||
* If there is no login
|
||||
*/
|
||||
@ -57,7 +81,12 @@ interface LoginStore {
|
||||
/**
|
||||
* Counter to trigger refresh of unread dms
|
||||
*/
|
||||
dmInteraction: 0
|
||||
dmInteraction: 0,
|
||||
|
||||
/**
|
||||
* Users cusom preferences
|
||||
*/
|
||||
preferences: UserPreferences
|
||||
};
|
||||
|
||||
const InitState = {
|
||||
@ -70,7 +99,13 @@ const InitState = {
|
||||
notifications: [],
|
||||
readNotifications: new Date().getTime(),
|
||||
dms: [],
|
||||
dmInteraction: 0
|
||||
dmInteraction: 0,
|
||||
preferences: {
|
||||
enableReactions: true,
|
||||
autoLoadMedia: true,
|
||||
theme: "dark",
|
||||
confirmReposts: false
|
||||
}
|
||||
} as LoginStore;
|
||||
|
||||
export interface SetRelaysPayload {
|
||||
@ -106,6 +141,16 @@ const LoginSlice = createSlice({
|
||||
if (!isNaN(readNotif)) {
|
||||
state.readNotifications = readNotif;
|
||||
}
|
||||
|
||||
// preferences
|
||||
let pref = window.localStorage.getItem(UserPreferencesKey);
|
||||
if (pref) {
|
||||
state.preferences = JSON.parse(pref);
|
||||
} else {
|
||||
// get os defaults
|
||||
const osTheme = window.matchMedia("(prefers-color-scheme: light)");
|
||||
state.preferences.theme = osTheme.matches ? "light" : "dark";
|
||||
}
|
||||
},
|
||||
setPrivateKey: (state, action: PayloadAction<HexKey>) => {
|
||||
state.loggedOut = false;
|
||||
@ -205,6 +250,10 @@ const LoginSlice = createSlice({
|
||||
markNotificationsRead: (state) => {
|
||||
state.readNotifications = new Date().getTime();
|
||||
window.localStorage.setItem(NotificationsReadItem, state.readNotifications.toString());
|
||||
},
|
||||
setPreferences: (state, action: PayloadAction<UserPreferences>) => {
|
||||
state.preferences = action.payload;
|
||||
window.localStorage.setItem(UserPreferencesKey, JSON.stringify(state.preferences));
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -220,6 +269,7 @@ export const {
|
||||
addDirectMessage,
|
||||
incDmInteraction,
|
||||
logout,
|
||||
markNotificationsRead
|
||||
markNotificationsRead,
|
||||
setPreferences
|
||||
} = LoginSlice.actions;
|
||||
export const reducer = LoginSlice.reducer;
|
553
src/index.css
@ -1,53 +1,51 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700&display=swap');
|
||||
|
||||
:root {
|
||||
--bg-color: #000;
|
||||
--font-color: #FFF;
|
||||
--font-secondary-color: #555;
|
||||
--font-tertiary-color: #666;
|
||||
--font-size: 16px;
|
||||
--font-size-small: 14px;
|
||||
--font-size-tiny: 12px;
|
||||
--modal-bg-color: rgba(0,0,0, 0.8);
|
||||
--note-bg: #111;
|
||||
--highlight-light: #ffd342;
|
||||
--highlight: #ffc400;
|
||||
--highlight-dark: #dba800;
|
||||
--error: #FF6053;
|
||||
--success: #2AD544;
|
||||
--bg-color: #000;
|
||||
--font-color: #FFF;
|
||||
--font-secondary-color: #555;
|
||||
--font-tertiary-color: #666;
|
||||
--font-size: 16px;
|
||||
--font-size-small: 14px;
|
||||
--font-size-tiny: 12px;
|
||||
--modal-bg-color: rgba(0, 0, 0, 0.8);
|
||||
--note-bg: #111;
|
||||
--highlight-light: #ffd342;
|
||||
--highlight: #ffc400;
|
||||
--highlight-dark: #dba800;
|
||||
--error: #FF6053;
|
||||
--success: #2AD544;
|
||||
|
||||
--gray-superlight: #EEE;
|
||||
--gray-light: #999;
|
||||
--gray-medium: #666;
|
||||
--gray: #333;
|
||||
--gray-secondary: #222;
|
||||
--gray-tertiary: #444;
|
||||
--gray-gradient: linear-gradient(to bottom right, var(--gray-superlight), var(--gray), var(--gray-light));
|
||||
--snort-gradient: linear-gradient(to bottom right, var(--highlight-light), var(--highlight), var(--highlight-dark));
|
||||
--nostrplebs-gradient: linear-gradient(to bottom right, #ff3cac, #2b86c5);
|
||||
--strike-army-gradient: linear-gradient(to bottom right, #CCFF00, #a1c900);
|
||||
--gray-superlight: #EEE;
|
||||
--gray-light: #999;
|
||||
--gray-medium: #666;
|
||||
--gray: #333;
|
||||
--gray-secondary: #222;
|
||||
--gray-tertiary: #444;
|
||||
--gray-gradient: linear-gradient(to bottom right, var(--gray-superlight), var(--gray), var(--gray-light));
|
||||
--snort-gradient: linear-gradient(to bottom right, var(--highlight-light), var(--highlight), var(--highlight-dark));
|
||||
--nostrplebs-gradient: linear-gradient(to bottom right, #ff3cac, #2b86c5);
|
||||
--strike-army-gradient: linear-gradient(to bottom right, #CCFF00, #a1c900);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
--bg-color: #F1F1F1;
|
||||
--font-color: #57534E;
|
||||
--font-secondary-color: #B9B9B9;
|
||||
--font-tertiary-color: #F3F3F3;
|
||||
html.light {
|
||||
--bg-color: #F1F1F1;
|
||||
--font-color: #57534E;
|
||||
--font-secondary-color: #B9B9B9;
|
||||
--font-tertiary-color: #F3F3F3;
|
||||
|
||||
--highlight-light: #16AAC1;
|
||||
--highlight: #0284C7;
|
||||
--highlight-dark: #0A52B5;
|
||||
--modal-bg-color: rgba(240, 240, 240, 0.8);
|
||||
--highlight-light: #16AAC1;
|
||||
--highlight: #0284C7;
|
||||
--highlight-dark: #0A52B5;
|
||||
--modal-bg-color: rgba(240, 240, 240, 0.8);
|
||||
|
||||
--note-bg: white;
|
||||
--note-bg: white;
|
||||
|
||||
--gray: #CCC;
|
||||
--gray-secondary: #DDD;
|
||||
--gray-tertiary: #EEE;
|
||||
--gray-superlight: #333;
|
||||
--gray-light: #555;
|
||||
}
|
||||
--gray: #CCC;
|
||||
--gray-secondary: #DDD;
|
||||
--gray-tertiary: #EEE;
|
||||
--gray-superlight: #333;
|
||||
--gray-light: #555;
|
||||
}
|
||||
|
||||
body {
|
||||
@ -70,18 +68,18 @@ code {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.page > .header {
|
||||
.page>.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.page > .header > div:nth-child(1) {
|
||||
.page>.header>div:nth-child(1) {
|
||||
font-size: x-large;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.page > .header > div:nth-child(2) {
|
||||
.page>.header>div:nth-child(2) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
@ -92,299 +90,318 @@ code {
|
||||
background-color: var(--note-bg);
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
.card { margin-bottom: 24px; padding: 12px 24px; }
|
||||
<<<<<<< HEAD .card {
|
||||
margin-bottom: 24px;
|
||||
padding: 12px 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
.card {
|
||||
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.05);
|
||||
|
||||
=======.card {
|
||||
margin-bottom: 24px;
|
||||
padding: 24px;
|
||||
>>>>>>>3e41614 (feat: user preferences)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card .header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.card .header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.card > .footer {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.card>.footer {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--font-color);
|
||||
border: 1px solid;
|
||||
display: inline-block;
|
||||
}
|
||||
.btn {
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--font-color);
|
||||
border: 1px solid;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.btn-warn {
|
||||
border-color: var(--error);
|
||||
}
|
||||
.btn-warn {
|
||||
border-color: var(--error);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
border-color: var(--success);
|
||||
}
|
||||
.btn-success {
|
||||
border-color: var(--success);
|
||||
}
|
||||
|
||||
.btn.active {
|
||||
border: 2px solid;
|
||||
background-color: var(--gray-secondary);
|
||||
color: var(--font-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
.btn.active {
|
||||
border: 2px solid;
|
||||
background-color: var(--gray-secondary);
|
||||
color: var(--font-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.btn.disabled {
|
||||
color: var(--gray-light);
|
||||
}
|
||||
.btn.disabled {
|
||||
color: var(--gray-light);
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: var(--gray);
|
||||
}
|
||||
.btn:hover {
|
||||
background-color: var(--gray);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 5px;
|
||||
}
|
||||
.btn-sm {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.btn-rnd {
|
||||
border-radius: 100%;
|
||||
}
|
||||
.btn-rnd {
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
textarea {
|
||||
font: inherit;
|
||||
}
|
||||
textarea {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
input[type="text"], input[type="password"], input[type="number"], textarea, select {
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
border: 0;
|
||||
background-color: var(--gray);
|
||||
color: var(--font-color);
|
||||
}
|
||||
input[type="text"], input[type="password"], input[type="number"], textarea, select {
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
border: 0;
|
||||
background-color: var(--gray);
|
||||
color: var(--font-color);
|
||||
}
|
||||
|
||||
input:disabled {
|
||||
color: var(--gray-medium);
|
||||
cursor:not-allowed;
|
||||
}
|
||||
input[type="checkbox"] {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
textarea:placeholder {
|
||||
color: var(--gray-superlight);
|
||||
}
|
||||
input:disabled {
|
||||
color: var(--gray-medium);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
textarea:placeholder {
|
||||
color: var(--gray-superlight);
|
||||
}
|
||||
|
||||
.f-center {
|
||||
justify-content: center;
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.f-1 {
|
||||
flex: 1;
|
||||
}
|
||||
.f-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.f-2 {
|
||||
flex: 2;
|
||||
}
|
||||
.f-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.f-grow {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.f-2 {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.f-shrink {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
.f-grow {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.f-ellipsis {
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
.f-shrink {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.f-col {
|
||||
flex-direction: column;
|
||||
align-items: flex-start !important;
|
||||
}
|
||||
.f-ellipsis {
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.w-max {
|
||||
width: 100%;
|
||||
width: -moz-available;
|
||||
width: -webkit-fill-available;
|
||||
width: fill-available;
|
||||
}
|
||||
.f-col {
|
||||
flex-direction: column;
|
||||
align-items: flex-start !important;
|
||||
}
|
||||
|
||||
.w-max-w {
|
||||
max-width: 100%;
|
||||
max-width: -moz-available;
|
||||
max-width: -webkit-fill-available;
|
||||
max-width: fill-available;
|
||||
}
|
||||
.w-max {
|
||||
width: 100%;
|
||||
width: -moz-available;
|
||||
width: -webkit-fill-available;
|
||||
width: fill-available;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
line-height: 1.3em;
|
||||
}
|
||||
.w-max-w {
|
||||
max-width: 100%;
|
||||
max-width: -moz-available;
|
||||
max-width: -webkit-fill-available;
|
||||
max-width: fill-available;
|
||||
}
|
||||
|
||||
a.ext {
|
||||
word-break: break-all;
|
||||
white-space: initial;
|
||||
}
|
||||
a {
|
||||
color: inherit;
|
||||
line-height: 1.3em;
|
||||
}
|
||||
|
||||
div.form-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
a.ext {
|
||||
word-break: break-all;
|
||||
white-space: initial;
|
||||
}
|
||||
|
||||
div.form-group > div {
|
||||
padding: 3px 5px;
|
||||
word-break: break-word;
|
||||
}
|
||||
div.form-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
div.form-group > div:nth-child(1) {
|
||||
min-width: 100px;
|
||||
}
|
||||
div.form-group>div {
|
||||
padding: 3px 5px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
div.form-group > div:nth-child(2) {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
justify-content: end;
|
||||
}
|
||||
div.form-group>div:nth-child(1) {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
div.form-group > div:nth-child(2) input {
|
||||
flex-grow: 1;
|
||||
}
|
||||
div.form-group>div:nth-child(2) {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: absolute;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: rgba(0,0,0,0.8);
|
||||
}
|
||||
div.form-group>div:nth-child(2) input {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.modal .modal-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.modal {
|
||||
position: absolute;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.modal .modal-content > div {
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
background-color: var(--gray);
|
||||
margin-top: 5vh;
|
||||
}
|
||||
.modal .modal-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
body.scroll-lock {
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
}
|
||||
.modal .modal-content>div {
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
background-color: var(--gray);
|
||||
margin-top: 5vh;
|
||||
}
|
||||
|
||||
.m5 {
|
||||
margin: 5px;
|
||||
}
|
||||
body.scroll-lock {
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.m10 {
|
||||
margin: 10px;
|
||||
}
|
||||
.m5 {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.mr10 {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.m10 {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.mr5 {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.mr10 {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.ml5 {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.mr5 {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.mb10 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.ml5 {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
align-content: center;
|
||||
text-align: center;
|
||||
margin: 10px 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.mb10 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.tabs > div {
|
||||
margin-right: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tabs {
|
||||
display: flex;
|
||||
align-content: center;
|
||||
text-align: center;
|
||||
margin: 10px 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.tabs > div:last-child {
|
||||
margin: 0;
|
||||
}
|
||||
.tabs>div {
|
||||
margin-right: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tabs .active {
|
||||
font-weight: bold;
|
||||
}
|
||||
.tabs>div:last-child {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--error);
|
||||
}
|
||||
.tabs .active {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.bg-error {
|
||||
background-color: var(--error);
|
||||
}
|
||||
.error {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.bg-success {
|
||||
background-color: var(--success);
|
||||
}
|
||||
.bg-error {
|
||||
background-color: var(--error);
|
||||
}
|
||||
|
||||
.root-tabs {
|
||||
.bg-success {
|
||||
background-color: var(--success);
|
||||
}
|
||||
|
||||
.root-tabs {
|
||||
padding: 0;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.root-tab {
|
||||
.root-tab {
|
||||
border-bottom: 3px solid var(--gray-secondary);
|
||||
}
|
||||
.root-tab.active {
|
||||
border-bottom: 3px solid var(--highlight);
|
||||
}
|
||||
}
|
||||
|
||||
.tweet {
|
||||
.root-tab.active {
|
||||
border-bottom: 3px solid var(--highlight);
|
||||
}
|
||||
|
||||
.tweet {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.tweet div {
|
||||
.tweet div {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.tweet div .twitter-tweet {
|
||||
.tweet div .twitter-tweet {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.tweet div .twitter-tweet > iframe {
|
||||
.tweet div .twitter-tweet>iframe {
|
||||
max-height: unset;
|
||||
}
|
||||
|
||||
@media(max-width: 720px) {
|
||||
.page {
|
||||
width: calc(100vw - 8px);
|
||||
}
|
||||
div.form-group {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.highlight { color: var(--highlight); }
|
||||
@media(max-width: 720px) {
|
||||
.page {
|
||||
width: calc(100vw - 8px);
|
||||
}
|
||||
|
||||
div.form-group {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: var(--highlight);
|
||||
}
|
@ -19,7 +19,7 @@ import ProfilePage from 'Pages/ProfilePage';
|
||||
import RootPage from 'Pages/Root';
|
||||
import NotificationsPage from 'Pages/Notifications';
|
||||
import NewUserPage from 'Pages/NewUserPage';
|
||||
import SettingsPage from 'Pages/SettingsPage';
|
||||
import SettingsPage, { SettingsRoutes } from 'Pages/SettingsPage';
|
||||
import ErrorPage from 'Pages/ErrorPage';
|
||||
import VerificationPage from 'Pages/Verification';
|
||||
import MessagesPage from 'Pages/MessagesPage';
|
||||
@ -65,7 +65,8 @@ const router = createBrowserRouter([
|
||||
},
|
||||
{
|
||||
path: "/settings",
|
||||
element: <SettingsPage />
|
||||
element: <SettingsPage />,
|
||||
children: SettingsRoutes
|
||||
},
|
||||
{
|
||||
path: "/verification",
|
||||
|