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] Manage followers ( Contact List )
- [x] Relay management
- [ ] Profile management
- [x] Profile management
- [ ] Image upload
- [ ] Reactions
- [ ] 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
- 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 notifications service that keeps track of read notifications. (show unread count in sidenav)
- 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)
- Add note embeds
- Add "repost" button that mentions the note
- 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
- Save note drafts and let users manage them
- 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 clientFollowingService from "../services/client-following";
import { UserAvatar } from "./user-avatar";
import { UserDnsIdentityIcon } from "./user-dns-identity";
const FollowingListItem = ({ pubkey }: { pubkey: string }) => {
const metadata = useUserMetadata(pubkey);
@ -20,6 +21,7 @@ const FollowingListItem = ({ pubkey }: { pubkey: string }) => {
variant="outline"
to={`/u/${normalizeToBech32(pubkey, Bech32Prefix.Pubkey)}`}
justifyContent="flex-start"
rightIcon={<UserDnsIdentityIcon pubkey={pubkey} onlyIcon />}
>
{getUserDisplayName(metadata, pubkey)}
</Button>

View File

@ -134,7 +134,7 @@ export const ShareIcon = createIcon({
export const ReplyIcon = createIcon({
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,
});
@ -150,6 +150,12 @@ export const VerificationFailed = createIcon({
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({
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",

View File

@ -13,9 +13,9 @@ import { UserTipButton } from "../user-tip-button";
import { NoteRelays } from "./note-relays";
import { useIsMobile } from "../../hooks/use-is-mobile";
import { UserLink } from "../user-link";
import { ReplyIcon } from "../icons";
import { ReplyIcon, ShareIcon } from "../icons";
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 { convertTimestampToDate } from "../../helpers/date";
import { useCurrentAccount } from "../../hooks/use-current-account";
@ -33,6 +33,7 @@ export const Note = React.memo(({ event, maxHeight }: NoteProps) => {
const following = contacts?.contacts || [];
const reply = () => openModal(buildReply(event));
const share = () => openModal(buildShare(event));
return (
<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} />
</CardBody>
<CardFooter padding="2" display="flex" gap="2">
<UserTipButton pubkey={event.pubkey} eventId={event.id} size="xs" />
<IconButton
icon={<ReplyIcon />}
title="Reply"
@ -62,8 +64,15 @@ export const Note = React.memo(({ event, maxHeight }: NoteProps) => {
size="xs"
isDisabled={account.readonly}
/>
<IconButton
icon={<ShareIcon />}
onClick={share}
aria-label="Share Note"
title="Share Note"
size="xs"
isDisabled={account.readonly}
/>
<Box flexGrow={1} />
<UserTipButton pubkey={event.pubkey} size="xs" />
<NoteRelays event={event} size="xs" />
<NoteMenu event={event} />
</CardFooter>

View File

@ -15,7 +15,7 @@ import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip-19";
import { NostrEvent } from "../../types/nostr-event";
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";
export const NoteMenu = ({ event }: { event: NostrEvent }) => {
@ -26,10 +26,6 @@ export const NoteMenu = ({ event }: { event: NostrEvent }) => {
return (
<>
<MenuIconButton>
{/* TODO: should open the post modal and mention the note/relay */}
{/* <MenuItem icon={<ShareIcon />} onClick={() => console.log("Share Note", event)}>
Repost
</MenuItem> */}
<MenuItem
as="a"
icon={<Avatar src={IMAGE_ICONS.nostrGuruIcon} size="xs" />}

View File

@ -54,6 +54,10 @@ export const PostModal = ({ isOpen, onClose, initialDraft }: PostModalProps) =>
const handleSubmit = async () => {
setWaiting(true);
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);
setWaiting(false);
if (!event) return;

View File

@ -1,7 +1,7 @@
import { Spinner, Tooltip } from "@chakra-ui/react";
import { useDnsIdentity } from "../hooks/use-dns-identity";
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 }) => {
const metadata = useUserMetadata(pubkey);
@ -11,21 +11,22 @@ export const UserDnsIdentityIcon = ({ pubkey, onlyIcon }: { pubkey: string; only
return null;
}
let title = metadata.nip05;
const renderIcon = () => {
if (loading) {
return <Spinner size="xs" ml="1" />;
} else if (error) {
return <VerificationFailed color="yellow.500" />;
} else if (!identity) {
return <VerificationMissing color="red.500" />;
} else if (pubkey === identity.pubkey) {
return <VerifiedIcon color="purple.500" />;
} else {
const isValid = !!identity && pubkey === identity.pubkey;
return isValid ? <VerifiedIcon color="purple.500" /> : <VerificationFailed color="red.500" />;
return <VerificationFailed color="red.500" />;
}
};
if (onlyIcon) {
return <Tooltip label={title}>{renderIcon()}</Tooltip>;
return <Tooltip label={metadata.nip05}>{renderIcon()}</Tooltip>;
}
return (
<span>

View File

@ -2,6 +2,8 @@ import moment from "moment";
import { getEventRelays } from "../services/event-relays";
import { DraftNostrEvent, isETag, isPTag, NostrEvent, RTag } from "../types/nostr-event";
import { RelayConfig, RelayMode } from "../classes/relay";
import accountService from "../services/account";
import { Kind } from "nostr-tools";
export function isReply(event: NostrEvent | DraftNostrEvent) {
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 relay = getEventRelays(event.id).value?.[0] ?? "";
@ -70,14 +72,15 @@ export function buildReply(event: NostrEvent): DraftNostrEvent {
}
// add all ptags
// 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);
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]);
}
return {
kind: 1,
kind: Kind.Text,
// TODO: be smarter about picking relay
tags,
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 {
switch (tag[2]) {
case "write":

View File

@ -32,7 +32,6 @@ class UserRelaysService extends CachedPubkeyEventRequester {
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>());
requestRelays(pubkey: string, relays: string[], alwaysRequest = false) {
const sub = this.parsedSubjects.get(pubkey);