mirror of
https://github.com/aljazceru/mutiny-web.git
synced 2025-12-19 07:14:22 +01:00
feat: nostr activity and tag support
This commit is contained in:
@@ -56,6 +56,7 @@ export function ContactViewer(props: {
|
|||||||
showToast(result.error);
|
showToast(result.error);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
|
result.value.privateTag = props.contact.name;
|
||||||
if (
|
if (
|
||||||
result.value?.address ||
|
result.value?.address ||
|
||||||
result.value?.invoice ||
|
result.value?.invoice ||
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import {
|
|||||||
Show,
|
Show,
|
||||||
Switch
|
Switch
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
|
import { Dynamic } from "solid-js/web";
|
||||||
|
|
||||||
import rightArrow from "~/assets/icons/right-arrow.svg";
|
import rightArrow from "~/assets/icons/right-arrow.svg";
|
||||||
import { AmountSats, VStack } from "~/components";
|
import { AmountSats, TinyText, VStack } from "~/components";
|
||||||
|
import { useI18n } from "~/i18n/context";
|
||||||
import { useMegaStore } from "~/state/megaStore";
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
import { fetchZaps, getHexpubFromNpub } from "~/utils";
|
import { fetchZaps, getHexpubFromNpub } from "~/utils";
|
||||||
import { timeAgo } from "~/utils/prettyPrintTime";
|
import { timeAgo } from "~/utils/prettyPrintTime";
|
||||||
@@ -31,6 +33,7 @@ function formatProfileLink(hexpub: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function NostrActivity() {
|
export function NostrActivity() {
|
||||||
|
const i18n = useI18n();
|
||||||
const [state, _actions] = useMegaStore();
|
const [state, _actions] = useMegaStore();
|
||||||
|
|
||||||
const [data, { refetch }] = createResource(state.npub, fetchZaps);
|
const [data, { refetch }] = createResource(state.npub, fetchZaps);
|
||||||
@@ -93,10 +96,10 @@ export function NostrActivity() {
|
|||||||
</a>
|
</a>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={zap.kind === "private"}>
|
<Match when={zap.kind === "private"}>
|
||||||
Private
|
{i18n.t("activity.private")}
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={zap.kind === "anonymous"}>
|
<Match when={zap.kind === "anonymous"}>
|
||||||
Anonymous
|
{i18n.t("activity.anonymous")}
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</span>
|
</span>
|
||||||
@@ -151,10 +154,36 @@ export function NostrActivity() {
|
|||||||
</div>
|
</div>
|
||||||
<Show when={zap.content}>
|
<Show when={zap.content}>
|
||||||
<hr class="my-2 border-m-grey-750" />
|
<hr class="my-2 border-m-grey-750" />
|
||||||
<p
|
<TinyText>
|
||||||
class="truncate text-center text-sm font-light text-neutral-200"
|
<Dynamic
|
||||||
textContent={zap.content}
|
component={
|
||||||
/>
|
zap.content?.includes("From:") ||
|
||||||
|
zap.content?.includes("://")
|
||||||
|
? "a"
|
||||||
|
: "p"
|
||||||
|
}
|
||||||
|
href={
|
||||||
|
zap.content?.split("nostr:")[1]
|
||||||
|
? formatProfileLink(
|
||||||
|
getHexpubFromNpub(
|
||||||
|
zap.content?.split(
|
||||||
|
"nostr:"
|
||||||
|
)[1]
|
||||||
|
) ?? ""
|
||||||
|
)
|
||||||
|
: zap.content
|
||||||
|
}
|
||||||
|
class="block truncate text-center text-sm font-light text-neutral-200"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{zap.content?.includes("From:")
|
||||||
|
? `${i18n.t(
|
||||||
|
"activity.from"
|
||||||
|
)} ${zap.content?.split("nostr:")[1]}`
|
||||||
|
: zap.content}
|
||||||
|
</Dynamic>
|
||||||
|
</TinyText>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export function TagEditor(props: {
|
|||||||
selectedValues: Partial<MutinyTagItem>[];
|
selectedValues: Partial<MutinyTagItem>[];
|
||||||
setSelectedValues: (value: Partial<MutinyTagItem>[]) => void;
|
setSelectedValues: (value: Partial<MutinyTagItem>[]) => void;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
|
autoFillTag?: string | undefined;
|
||||||
}) {
|
}) {
|
||||||
const [_state, actions] = useMegaStore();
|
const [_state, actions] = useMegaStore();
|
||||||
const [availableTags, setAvailableTags] = createSignal<MutinyTagItem[]>([]);
|
const [availableTags, setAvailableTags] = createSignal<MutinyTagItem[]>([]);
|
||||||
@@ -28,12 +29,24 @@ export function TagEditor(props: {
|
|||||||
.filter((tag) => tag.kind === "Contact")
|
.filter((tag) => tag.kind === "Contact")
|
||||||
.sort(sortByLastUsed)
|
.sort(sortByLastUsed)
|
||||||
);
|
);
|
||||||
|
if (props.autoFillTag && availableTags()) {
|
||||||
|
const tagToAutoSelect = availableTags().find(
|
||||||
|
(tag) => tag.name === props.autoFillTag
|
||||||
|
);
|
||||||
|
if (tagToAutoSelect) {
|
||||||
|
props.setSelectedValues([
|
||||||
|
...props.selectedValues,
|
||||||
|
tagToAutoSelect
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const selectProps = createMemo(() => {
|
const selectProps = createMemo(() => {
|
||||||
return createOptions(availableTags() || [], {
|
return createOptions(availableTags() || [], {
|
||||||
key: "name",
|
key: "name",
|
||||||
|
disable: (value) => props.selectedValues.includes(value),
|
||||||
filterable: true, // Default
|
filterable: true, // Default
|
||||||
createable: createLabelValue
|
createable: createLabelValue
|
||||||
});
|
});
|
||||||
@@ -42,8 +55,6 @@ export function TagEditor(props: {
|
|||||||
const onChange = (selected: MutinyTagItem[]) => {
|
const onChange = (selected: MutinyTagItem[]) => {
|
||||||
props.setSelectedValues(selected);
|
props.setSelectedValues(selected);
|
||||||
|
|
||||||
console.log(selected);
|
|
||||||
|
|
||||||
const lastValue = selected[selected.length - 1];
|
const lastValue = selected[selected.length - 1];
|
||||||
if (
|
if (
|
||||||
lastValue &&
|
lastValue &&
|
||||||
@@ -54,7 +65,6 @@ export function TagEditor(props: {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// FIXME: eslint is mad about reactivity
|
|
||||||
const onTagTap = (tag: MutinyTagItem) => {
|
const onTagTap = (tag: MutinyTagItem) => {
|
||||||
props.setSelectedValues([...props.selectedValues!, tag]);
|
props.setSelectedValues([...props.selectedValues!, tag]);
|
||||||
};
|
};
|
||||||
@@ -70,10 +80,16 @@ export function TagEditor(props: {
|
|||||||
/>
|
/>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<Show when={availableTags() && availableTags()!.length > 0}>
|
<Show when={availableTags() && availableTags()!.length > 0}>
|
||||||
<For each={availableTags()!.slice(0, 3)}>
|
<For
|
||||||
|
each={availableTags()!.slice(0, 3).sort(sortByLastUsed)}
|
||||||
|
>
|
||||||
{(tag) => (
|
{(tag) => (
|
||||||
|
<TinyButton
|
||||||
|
hidden={props.selectedValues.includes(tag)}
|
||||||
|
tag={tag}
|
||||||
// eslint-disable-next-line solid/reactivity
|
// eslint-disable-next-line solid/reactivity
|
||||||
<TinyButton tag={tag} onClick={() => onTagTap(tag)}>
|
onClick={() => onTagTap(tag)}
|
||||||
|
>
|
||||||
{tag.name}
|
{tag.name}
|
||||||
</TinyButton>
|
</TinyButton>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -258,6 +258,7 @@ export const TinyText: ParentComponent = (props) => {
|
|||||||
export const TinyButton: ParentComponent<{
|
export const TinyButton: ParentComponent<{
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
tag?: MutinyTagItem;
|
tag?: MutinyTagItem;
|
||||||
|
hidden?: boolean;
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
// TODO: don't need to run this if it's not a contact
|
// TODO: don't need to run this if it's not a contact
|
||||||
const [gradient] = createResource(async () => {
|
const [gradient] = createResource(async () => {
|
||||||
@@ -272,6 +273,7 @@ export const TinyButton: ParentComponent<{
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
class="rounded-lg bg-white/10 px-2 py-1"
|
class="rounded-lg bg-white/10 px-2 py-1"
|
||||||
|
classList={{ hidden: props.hidden }}
|
||||||
onClick={() => props.onClick()}
|
onClick={() => props.onClick()}
|
||||||
style={{ background: bg() }}
|
style={{ background: bg() }}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -151,7 +151,10 @@ export default {
|
|||||||
unknown: "Unknown",
|
unknown: "Unknown",
|
||||||
import_contacts:
|
import_contacts:
|
||||||
"Import your contacts from nostr to see who they're zapping.",
|
"Import your contacts from nostr to see who they're zapping.",
|
||||||
coming_soon: "Coming soon"
|
coming_soon: "Coming soon",
|
||||||
|
private: "Private",
|
||||||
|
anonymous: "Anonymous",
|
||||||
|
from: "From:"
|
||||||
},
|
},
|
||||||
redshift: {
|
redshift: {
|
||||||
title: "Redshift",
|
title: "Redshift",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export type ParsedParams = {
|
|||||||
amount_sats?: bigint;
|
amount_sats?: bigint;
|
||||||
network?: string;
|
network?: string;
|
||||||
memo?: string;
|
memo?: string;
|
||||||
|
privateTag?: string;
|
||||||
node_pubkey?: string;
|
node_pubkey?: string;
|
||||||
lnurl?: string;
|
lnurl?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -685,6 +685,9 @@ export default function Send() {
|
|||||||
{i18n.t("common.private_tags")}
|
{i18n.t("common.private_tags")}
|
||||||
</SmallHeader>
|
</SmallHeader>
|
||||||
<TagEditor
|
<TagEditor
|
||||||
|
autoFillTag={
|
||||||
|
destination()?.privateTag
|
||||||
|
}
|
||||||
selectedValues={selectedContacts()}
|
selectedValues={selectedContacts()}
|
||||||
setSelectedValues={
|
setSelectedValues={
|
||||||
setSelectedContacts
|
setSelectedContacts
|
||||||
|
|||||||
Reference in New Issue
Block a user