feat: user preferences
This commit is contained in:
@ -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>
|
||||
|
Reference in New Issue
Block a user