add basic share button

This commit is contained in:
hzrd149
2023-02-20 12:17:41 -06:00
parent ff94abe0da
commit c9ba9193e5
9 changed files with 59 additions and 24 deletions

View File

@@ -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/

View File

@@ -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>

View File

@@ -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",

View File

@@ -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>

View File

@@ -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" />}

View File

@@ -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;

View File

@@ -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>

View File

@@ -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":

View File

@@ -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);