mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-27 12:07:43 +02:00
add basic share button
This commit is contained in:
@@ -32,7 +32,7 @@ I would recomend you use a browser extension like [Alby](https://getalby.com/) o
|
|||||||
- [x] User tipping
|
- [x] User tipping
|
||||||
- [x] Manage followers ( Contact List )
|
- [x] Manage followers ( Contact List )
|
||||||
- [x] Relay management
|
- [x] Relay management
|
||||||
- [ ] Profile management
|
- [x] Profile management
|
||||||
- [ ] Image upload
|
- [ ] Image upload
|
||||||
- [ ] Reactions
|
- [ ] Reactions
|
||||||
- [ ] Dynamically connect to relays (start with one relay then connect to others as required)
|
- [ ] Dynamically connect to relays (start with one relay then connect to others as required)
|
||||||
@@ -71,14 +71,13 @@ I would recomend you use a browser extension like [Alby](https://getalby.com/) o
|
|||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
|
- Show reactions and zaps on notes
|
||||||
- Create a "event posting" service that can show modals (for qr code scanning), warnings (signed by wrong pubkey), and results (what relays responded) when posting events.
|
- Create a "event posting" service that can show modals (for qr code scanning), warnings (signed by wrong pubkey), and results (what relays responded) when posting events.
|
||||||
- Create notifications service that keeps track of read notifications. (show unread count in sidenav)
|
- Create notifications service that keeps track of read notifications. (show unread count in sidenav)
|
||||||
- Rebuild relays view to show relay info and settings NIP-11
|
- Rebuild relays view to show relay info and settings NIP-11
|
||||||
- filter list of followers by users the user has blocked/reported (stops bots/spammers from showing up at followers)
|
- filter list of followers by users the user has blocked/reported (stops bots/spammers from showing up at followers)
|
||||||
- Add note embeds
|
|
||||||
- Add "repost" button that mentions the note
|
|
||||||
- Add preview tab to note modal
|
- Add preview tab to note modal
|
||||||
- Add mentions in posts (https://css-tricks.com/so-you-want-to-build-an-mention-autocomplete-feature/)
|
- Add mentions in notes (https://css-tricks.com/so-you-want-to-build-an-mention-autocomplete-feature/)
|
||||||
- add `client` tag to published events
|
- add `client` tag to published events
|
||||||
- Save note drafts and let users manage them
|
- Save note drafts and let users manage them
|
||||||
- make app a valid web share target https://developer.chrome.com/articles/web-share-target/
|
- make app a valid web share target https://developer.chrome.com/articles/web-share-target/
|
||||||
|
@@ -6,6 +6,7 @@ import useSubject from "../hooks/use-subject";
|
|||||||
import { useUserMetadata } from "../hooks/use-user-metadata";
|
import { useUserMetadata } from "../hooks/use-user-metadata";
|
||||||
import clientFollowingService from "../services/client-following";
|
import clientFollowingService from "../services/client-following";
|
||||||
import { UserAvatar } from "./user-avatar";
|
import { UserAvatar } from "./user-avatar";
|
||||||
|
import { UserDnsIdentityIcon } from "./user-dns-identity";
|
||||||
|
|
||||||
const FollowingListItem = ({ pubkey }: { pubkey: string }) => {
|
const FollowingListItem = ({ pubkey }: { pubkey: string }) => {
|
||||||
const metadata = useUserMetadata(pubkey);
|
const metadata = useUserMetadata(pubkey);
|
||||||
@@ -20,6 +21,7 @@ const FollowingListItem = ({ pubkey }: { pubkey: string }) => {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
to={`/u/${normalizeToBech32(pubkey, Bech32Prefix.Pubkey)}`}
|
to={`/u/${normalizeToBech32(pubkey, Bech32Prefix.Pubkey)}`}
|
||||||
justifyContent="flex-start"
|
justifyContent="flex-start"
|
||||||
|
rightIcon={<UserDnsIdentityIcon pubkey={pubkey} onlyIcon />}
|
||||||
>
|
>
|
||||||
{getUserDisplayName(metadata, pubkey)}
|
{getUserDisplayName(metadata, pubkey)}
|
||||||
</Button>
|
</Button>
|
||||||
|
@@ -134,7 +134,7 @@ export const ShareIcon = createIcon({
|
|||||||
|
|
||||||
export const ReplyIcon = createIcon({
|
export const ReplyIcon = createIcon({
|
||||||
displayName: "reply-icon",
|
displayName: "reply-icon",
|
||||||
d: "M11 20L1 12l10-8v5c5.523 0 10 4.477 10 10 0 .273-.01.543-.032.81-1.463-2.774-4.33-4.691-7.655-4.805L13 15h-2v5zm-2-7h4.034l.347.007c1.285.043 2.524.31 3.676.766C15.59 12.075 13.42 11 11 11H9V8.161L4.202 12 9 15.839V13z",
|
d: "M5.763 17H20V5H4v13.385L5.763 17zm.692 2L2 22.5V4a1 1 0 0 1 1-1h18a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H6.455z",
|
||||||
defaultProps,
|
defaultProps,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -150,6 +150,12 @@ export const VerificationFailed = createIcon({
|
|||||||
defaultProps,
|
defaultProps,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const VerificationMissing = createIcon({
|
||||||
|
displayName: "VerificationFailed",
|
||||||
|
d: "M12 19c.828 0 1.5.672 1.5 1.5S12.828 22 12 22s-1.5-.672-1.5-1.5.672-1.5 1.5-1.5zm0-17c3.314 0 6 2.686 6 6 0 2.165-.753 3.29-2.674 4.923C13.399 14.56 13 15.297 13 17h-2c0-2.474.787-3.695 3.031-5.601C15.548 10.11 16 9.434 16 8c0-2.21-1.79-4-4-4S8 5.79 8 8v1H6V8c0-3.314 2.686-6 6-6z",
|
||||||
|
defaultProps,
|
||||||
|
});
|
||||||
|
|
||||||
export const SpyIcon = createIcon({
|
export const SpyIcon = createIcon({
|
||||||
displayName: "SpyIcon",
|
displayName: "SpyIcon",
|
||||||
d: "M17 13a4 4 0 1 1-4 4h-2a4 4 0 1 1-.535-2h3.07A3.998 3.998 0 0 1 17 13zM7 15a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm10 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4zM16 3a4 4 0 0 1 4 4v3h2v2H2v-2h2V7a4 4 0 0 1 4-4h8zm0 2H8c-1.054 0-2 .95-2 2v3h12V7c0-1.054-.95-2-2-2z",
|
d: "M17 13a4 4 0 1 1-4 4h-2a4 4 0 1 1-.535-2h3.07A3.998 3.998 0 0 1 17 13zM7 15a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm10 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4zM16 3a4 4 0 0 1 4 4v3h2v2H2v-2h2V7a4 4 0 0 1 4-4h8zm0 2H8c-1.054 0-2 .95-2 2v3h12V7c0-1.054-.95-2-2-2z",
|
||||||
|
@@ -13,9 +13,9 @@ import { UserTipButton } from "../user-tip-button";
|
|||||||
import { NoteRelays } from "./note-relays";
|
import { NoteRelays } from "./note-relays";
|
||||||
import { useIsMobile } from "../../hooks/use-is-mobile";
|
import { useIsMobile } from "../../hooks/use-is-mobile";
|
||||||
import { UserLink } from "../user-link";
|
import { UserLink } from "../user-link";
|
||||||
import { ReplyIcon } from "../icons";
|
import { ReplyIcon, ShareIcon } from "../icons";
|
||||||
import { PostModalContext } from "../../providers/post-modal-provider";
|
import { PostModalContext } from "../../providers/post-modal-provider";
|
||||||
import { buildReply } from "../../helpers/nostr-event";
|
import { buildReply, buildShare } from "../../helpers/nostr-event";
|
||||||
import { UserDnsIdentityIcon } from "../user-dns-identity";
|
import { UserDnsIdentityIcon } from "../user-dns-identity";
|
||||||
import { convertTimestampToDate } from "../../helpers/date";
|
import { convertTimestampToDate } from "../../helpers/date";
|
||||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||||
@@ -33,6 +33,7 @@ export const Note = React.memo(({ event, maxHeight }: NoteProps) => {
|
|||||||
const following = contacts?.contacts || [];
|
const following = contacts?.contacts || [];
|
||||||
|
|
||||||
const reply = () => openModal(buildReply(event));
|
const reply = () => openModal(buildReply(event));
|
||||||
|
const share = () => openModal(buildShare(event));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card variant="outline">
|
<Card variant="outline">
|
||||||
@@ -54,6 +55,7 @@ export const Note = React.memo(({ event, maxHeight }: NoteProps) => {
|
|||||||
<NoteContents event={event} trusted={following.includes(event.pubkey)} maxHeight={maxHeight} />
|
<NoteContents event={event} trusted={following.includes(event.pubkey)} maxHeight={maxHeight} />
|
||||||
</CardBody>
|
</CardBody>
|
||||||
<CardFooter padding="2" display="flex" gap="2">
|
<CardFooter padding="2" display="flex" gap="2">
|
||||||
|
<UserTipButton pubkey={event.pubkey} eventId={event.id} size="xs" />
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<ReplyIcon />}
|
icon={<ReplyIcon />}
|
||||||
title="Reply"
|
title="Reply"
|
||||||
@@ -62,8 +64,15 @@ export const Note = React.memo(({ event, maxHeight }: NoteProps) => {
|
|||||||
size="xs"
|
size="xs"
|
||||||
isDisabled={account.readonly}
|
isDisabled={account.readonly}
|
||||||
/>
|
/>
|
||||||
|
<IconButton
|
||||||
|
icon={<ShareIcon />}
|
||||||
|
onClick={share}
|
||||||
|
aria-label="Share Note"
|
||||||
|
title="Share Note"
|
||||||
|
size="xs"
|
||||||
|
isDisabled={account.readonly}
|
||||||
|
/>
|
||||||
<Box flexGrow={1} />
|
<Box flexGrow={1} />
|
||||||
<UserTipButton pubkey={event.pubkey} size="xs" />
|
|
||||||
<NoteRelays event={event} size="xs" />
|
<NoteRelays event={event} size="xs" />
|
||||||
<NoteMenu event={event} />
|
<NoteMenu event={event} />
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
|
@@ -15,7 +15,7 @@ import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip-19";
|
|||||||
import { NostrEvent } from "../../types/nostr-event";
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
import { MenuIconButton } from "../menu-icon-button";
|
import { MenuIconButton } from "../menu-icon-button";
|
||||||
|
|
||||||
import { ClipboardIcon, CodeIcon, IMAGE_ICONS, ShareIcon } from "../icons";
|
import { ClipboardIcon, CodeIcon, IMAGE_ICONS } from "../icons";
|
||||||
import { getReferences } from "../../helpers/nostr-event";
|
import { getReferences } from "../../helpers/nostr-event";
|
||||||
|
|
||||||
export const NoteMenu = ({ event }: { event: NostrEvent }) => {
|
export const NoteMenu = ({ event }: { event: NostrEvent }) => {
|
||||||
@@ -26,10 +26,6 @@ export const NoteMenu = ({ event }: { event: NostrEvent }) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MenuIconButton>
|
<MenuIconButton>
|
||||||
{/* TODO: should open the post modal and mention the note/relay */}
|
|
||||||
{/* <MenuItem icon={<ShareIcon />} onClick={() => console.log("Share Note", event)}>
|
|
||||||
Repost
|
|
||||||
</MenuItem> */}
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
as="a"
|
as="a"
|
||||||
icon={<Avatar src={IMAGE_ICONS.nostrGuruIcon} size="xs" />}
|
icon={<Avatar src={IMAGE_ICONS.nostrGuruIcon} size="xs" />}
|
||||||
|
@@ -54,6 +54,10 @@ export const PostModal = ({ isOpen, onClose, initialDraft }: PostModalProps) =>
|
|||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
setWaiting(true);
|
setWaiting(true);
|
||||||
const updatedDraft: DraftNostrEvent = { ...draft, created_at: moment().unix() };
|
const updatedDraft: DraftNostrEvent = { ...draft, created_at: moment().unix() };
|
||||||
|
// add client tag, TODO: find a better place for this
|
||||||
|
if (!updatedDraft.tags.some((t) => t[0] === "client")) {
|
||||||
|
updatedDraft.tags.push(["client", "noStrudel"]);
|
||||||
|
}
|
||||||
const event = await requestSignature(updatedDraft);
|
const event = await requestSignature(updatedDraft);
|
||||||
setWaiting(false);
|
setWaiting(false);
|
||||||
if (!event) return;
|
if (!event) return;
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { Spinner, Tooltip } from "@chakra-ui/react";
|
import { Spinner, Tooltip } from "@chakra-ui/react";
|
||||||
import { useDnsIdentity } from "../hooks/use-dns-identity";
|
import { useDnsIdentity } from "../hooks/use-dns-identity";
|
||||||
import { useUserMetadata } from "../hooks/use-user-metadata";
|
import { useUserMetadata } from "../hooks/use-user-metadata";
|
||||||
import { VerificationFailed, VerifiedIcon } from "./icons";
|
import { VerificationFailed, VerificationMissing, VerifiedIcon } from "./icons";
|
||||||
|
|
||||||
export const UserDnsIdentityIcon = ({ pubkey, onlyIcon }: { pubkey: string; onlyIcon?: boolean }) => {
|
export const UserDnsIdentityIcon = ({ pubkey, onlyIcon }: { pubkey: string; onlyIcon?: boolean }) => {
|
||||||
const metadata = useUserMetadata(pubkey);
|
const metadata = useUserMetadata(pubkey);
|
||||||
@@ -11,21 +11,22 @@ export const UserDnsIdentityIcon = ({ pubkey, onlyIcon }: { pubkey: string; only
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let title = metadata.nip05;
|
|
||||||
|
|
||||||
const renderIcon = () => {
|
const renderIcon = () => {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <Spinner size="xs" ml="1" />;
|
return <Spinner size="xs" ml="1" />;
|
||||||
} else if (error) {
|
} else if (error) {
|
||||||
return <VerificationFailed color="yellow.500" />;
|
return <VerificationFailed color="yellow.500" />;
|
||||||
|
} else if (!identity) {
|
||||||
|
return <VerificationMissing color="red.500" />;
|
||||||
|
} else if (pubkey === identity.pubkey) {
|
||||||
|
return <VerifiedIcon color="purple.500" />;
|
||||||
} else {
|
} else {
|
||||||
const isValid = !!identity && pubkey === identity.pubkey;
|
return <VerificationFailed color="red.500" />;
|
||||||
return isValid ? <VerifiedIcon color="purple.500" /> : <VerificationFailed color="red.500" />;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (onlyIcon) {
|
if (onlyIcon) {
|
||||||
return <Tooltip label={title}>{renderIcon()}</Tooltip>;
|
return <Tooltip label={metadata.nip05}>{renderIcon()}</Tooltip>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
|
@@ -2,6 +2,8 @@ import moment from "moment";
|
|||||||
import { getEventRelays } from "../services/event-relays";
|
import { getEventRelays } from "../services/event-relays";
|
||||||
import { DraftNostrEvent, isETag, isPTag, NostrEvent, RTag } from "../types/nostr-event";
|
import { DraftNostrEvent, isETag, isPTag, NostrEvent, RTag } from "../types/nostr-event";
|
||||||
import { RelayConfig, RelayMode } from "../classes/relay";
|
import { RelayConfig, RelayMode } from "../classes/relay";
|
||||||
|
import accountService from "../services/account";
|
||||||
|
import { Kind } from "nostr-tools";
|
||||||
|
|
||||||
export function isReply(event: NostrEvent | DraftNostrEvent) {
|
export function isReply(event: NostrEvent | DraftNostrEvent) {
|
||||||
return !!event.tags.find((tag) => isETag(tag) && tag[3] !== "mention");
|
return !!event.tags.find((tag) => isETag(tag) && tag[3] !== "mention");
|
||||||
@@ -55,7 +57,7 @@ export function getReferences(event: NostrEvent | DraftNostrEvent) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildReply(event: NostrEvent): DraftNostrEvent {
|
export function buildReply(event: NostrEvent, account = accountService.current.value): DraftNostrEvent {
|
||||||
const refs = getReferences(event);
|
const refs = getReferences(event);
|
||||||
const relay = getEventRelays(event.id).value?.[0] ?? "";
|
const relay = getEventRelays(event.id).value?.[0] ?? "";
|
||||||
|
|
||||||
@@ -70,14 +72,15 @@ export function buildReply(event: NostrEvent): DraftNostrEvent {
|
|||||||
}
|
}
|
||||||
// add all ptags
|
// add all ptags
|
||||||
// TODO: omit my own pubkey
|
// TODO: omit my own pubkey
|
||||||
const ptags = event.tags.filter(isPTag);
|
const ptags = event.tags.filter(isPTag).filter((t) => !account || t[1] !== account.pubkey);
|
||||||
tags.push(...ptags);
|
tags.push(...ptags);
|
||||||
if (!ptags.find((t) => t[1] === event.pubkey)) {
|
// add the original authors pubkey if its not already there
|
||||||
|
if (!ptags.some((t) => t[1] === event.pubkey)) {
|
||||||
tags.push(["p", event.pubkey]);
|
tags.push(["p", event.pubkey]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
kind: 1,
|
kind: Kind.Text,
|
||||||
// TODO: be smarter about picking relay
|
// TODO: be smarter about picking relay
|
||||||
tags,
|
tags,
|
||||||
content: "",
|
content: "",
|
||||||
@@ -85,6 +88,22 @@ export function buildReply(event: NostrEvent): DraftNostrEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildShare(event: NostrEvent): DraftNostrEvent {
|
||||||
|
const relay = getEventRelays(event.id).value?.[0] ?? "";
|
||||||
|
|
||||||
|
const tags: NostrEvent["tags"] = [];
|
||||||
|
tags.push(["e", event.id, relay, "mention"]);
|
||||||
|
tags.push(["p", event.pubkey]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: Kind.Reaction,
|
||||||
|
// TODO: be smarter about picking relay
|
||||||
|
tags,
|
||||||
|
content: "#[0]",
|
||||||
|
created_at: moment().unix(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function parseRTag(tag: RTag): RelayConfig {
|
export function parseRTag(tag: RTag): RelayConfig {
|
||||||
switch (tag[2]) {
|
switch (tag[2]) {
|
||||||
case "write":
|
case "write":
|
||||||
|
@@ -32,7 +32,6 @@ class UserRelaysService extends CachedPubkeyEventRequester {
|
|||||||
return db.put("userRelays", event);
|
return db.put("userRelays", event);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: rxjs behavior subject dose not feel like the right thing to use here
|
|
||||||
private parsedSubjects = new SuperMap<string, Subject<UserRelays>>(() => new Subject<UserRelays>());
|
private parsedSubjects = new SuperMap<string, Subject<UserRelays>>(() => new Subject<UserRelays>());
|
||||||
requestRelays(pubkey: string, relays: string[], alwaysRequest = false) {
|
requestRelays(pubkey: string, relays: string[], alwaysRequest = false) {
|
||||||
const sub = this.parsedSubjects.get(pubkey);
|
const sub = this.parsedSubjects.get(pubkey);
|
||||||
|
Reference in New Issue
Block a user