Add profile zap and QR codes

This commit is contained in:
Bojan Mojsilovic 2024-02-02 16:31:42 +01:00
parent ae05fea8eb
commit 390d488b13
15 changed files with 588 additions and 27 deletions

22
package-lock.json generated
View File

@ -1,17 +1,17 @@
{
"name": "primal-web-app",
"version": "0.97.3",
"version": "0.101.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "primal-web-app",
"version": "0.97.3",
"version": "0.101.2",
"license": "MIT",
"dependencies": {
"@cookbook/solid-intl": "0.1.2",
"@jukben/emoji-search": "3.0.0",
"@kobalte/core": "^0.11.0",
"@kobalte/core": "0.11.0",
"@picocss/pico": "1.5.10",
"@scure/base": "1.1.3",
"@solidjs/router": "0.8.3",
@ -20,7 +20,8 @@
"dompurify": "3.0.5",
"medium-zoom": "1.0.8",
"nostr-tools": "1.15.0",
"photoswipe": "^5.4.3",
"photoswipe": "5.4.3",
"qr-code-styling": "^1.6.0-rc.1",
"sass": "1.67.0",
"solid-js": "1.7.11"
},
@ -1881,6 +1882,19 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/qr-code-styling": {
"version": "1.6.0-rc.1",
"resolved": "https://registry.npmjs.org/qr-code-styling/-/qr-code-styling-1.6.0-rc.1.tgz",
"integrity": "sha512-ModRIiW6oUnsP18QzrRYZSc/CFKFKIdj7pUs57AEVH20ajlglRpN3HukjHk0UbNMTlKGuaYl7Gt6/O5Gg2NU2Q==",
"dependencies": {
"qrcode-generator": "^1.4.3"
}
},
"node_modules/qrcode-generator": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/qrcode-generator/-/qrcode-generator-1.4.4.tgz",
"integrity": "sha512-HM7yY8O2ilqhmULxGMpcHSF1EhJJ9yBj8gvDEuZ6M+KGJ0YY2hKpnXvRD+hZPLrDVck3ExIGhmPtSdcjC+guuw=="
},
"node_modules/readdirp": {
"version": "3.6.0",
"license": "MIT",

View File

@ -33,6 +33,7 @@
"medium-zoom": "1.0.8",
"nostr-tools": "1.15.0",
"photoswipe": "5.4.3",
"qr-code-styling": "^1.6.0-rc.1",
"sass": "1.67.0",
"solid-js": "1.7.11"
}

View File

@ -0,0 +1,13 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.18182 3.68176C3.18182 3.40562 3.40568 3.18176 3.68182 3.18176H5.4091C5.68524 3.18176 5.9091 3.40562 5.9091 3.68176V5.40904C5.9091 5.68518 5.68524 5.90904 5.4091 5.90904H3.68182C3.40568 5.90904 3.18182 5.68518 3.18182 5.40904V3.68176Z" fill="#AAAAAA"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.81818 0C0.814028 0 0 0.814028 0 1.81818V7.27273C0 8.27688 0.814028 9.09091 1.81818 9.09091H7.27273C8.27688 9.09091 9.09091 8.27688 9.09091 7.27273V1.81818C9.09091 0.814028 8.27688 0 7.27273 0H1.81818ZM7.27273 1.36364H1.81818C1.56714 1.36364 1.36364 1.56714 1.36364 1.81818V7.27273C1.36364 7.52377 1.56714 7.72727 1.81818 7.72727H7.27273C7.52377 7.72727 7.72727 7.52377 7.72727 7.27273V1.81818C7.72727 1.56714 7.52377 1.36364 7.27273 1.36364Z" fill="#AAAAAA"/>
<path d="M14.0909 3.68176C14.0909 3.40562 14.3148 3.18176 14.5909 3.18176H16.3182C16.5943 3.18176 16.8182 3.40562 16.8182 3.68176V5.40904C16.8182 5.68518 16.5943 5.90904 16.3182 5.90904H14.5909C14.3148 5.90904 14.0909 5.68518 14.0909 5.40904V3.68176Z" fill="#AAAAAA"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.9091 1.81818C10.9091 0.814028 11.7231 0 12.7273 0H18.1818C19.186 0 20 0.814028 20 1.81818V7.27273C20 8.27688 19.186 9.09091 18.1818 9.09091H12.7273C11.7231 9.09091 10.9091 8.27688 10.9091 7.27273V1.81818ZM12.7273 1.36364H18.1818C18.4329 1.36364 18.6364 1.56714 18.6364 1.81818V7.27273C18.6364 7.52377 18.4329 7.72727 18.1818 7.72727H12.7273C12.4762 7.72727 12.2727 7.52377 12.2727 7.27273V1.81818C12.2727 1.56714 12.4762 1.36364 12.7273 1.36364Z" fill="#AAAAAA"/>
<path d="M3.18182 14.5909C3.18182 14.3148 3.40568 14.0909 3.68182 14.0909H5.4091C5.68524 14.0909 5.9091 14.3148 5.9091 14.5909V16.3182C5.9091 16.5944 5.68524 16.8182 5.4091 16.8182H3.68182C3.40568 16.8182 3.18182 16.5944 3.18182 16.3182V14.5909Z" fill="#AAAAAA"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 12.7272C0 11.7231 0.814028 10.9091 1.81818 10.9091H7.27273C8.27688 10.9091 9.09091 11.7231 9.09091 12.7272V18.1818C9.09091 19.1859 8.27688 20 7.27273 20H1.81818C0.814028 20 0 19.1859 0 18.1818V12.7272ZM1.81818 12.2727H7.27273C7.52377 12.2727 7.72727 12.4762 7.72727 12.7272V18.1818C7.72727 18.4328 7.52377 18.6363 7.27273 18.6363H1.81818C1.56714 18.6363 1.36364 18.4328 1.36364 18.1818V12.7272C1.36364 12.4762 1.56714 12.2727 1.81818 12.2727Z" fill="#AAAAAA"/>
<path d="M11.4091 10.9091C11.1329 10.9091 10.9091 11.1329 10.9091 11.4091V13.1363C10.9091 13.4125 11.1329 13.6363 11.4091 13.6363H13.1364C13.4125 13.6363 13.6364 13.4125 13.6364 13.1363V11.4091C13.6364 11.1329 13.4125 10.9091 13.1364 10.9091H11.4091Z" fill="#AAAAAA"/>
<path d="M14.0909 14.5909C14.0909 14.3148 14.3148 14.0909 14.5909 14.0909H16.3182C16.5943 14.0909 16.8182 14.3148 16.8182 14.5909V16.3182C16.8182 16.5944 16.5943 16.8182 16.3182 16.8182H14.5909C14.3148 16.8182 14.0909 16.5944 14.0909 16.3182V14.5909Z" fill="#AAAAAA"/>
<path d="M17.7727 17.2727C17.4966 17.2727 17.2727 17.4966 17.2727 17.7727V19.5C17.2727 19.7761 17.4966 20 17.7727 20H19.5C19.7761 20 20 19.7761 20 19.5V17.7727C20 17.4966 19.7761 17.2727 19.5 17.2727H17.7727Z" fill="#AAAAAA"/>
<path d="M10.9091 17.7727C10.9091 17.4966 11.1329 17.2727 11.4091 17.2727H13.1364C13.4125 17.2727 13.6364 17.4966 13.6364 17.7727V19.5C13.6364 19.7761 13.4125 20 13.1364 20H11.4091C11.1329 20 10.9091 19.7761 10.9091 19.5V17.7727Z" fill="#AAAAAA"/>
<path d="M17.7727 10.9091C17.4966 10.9091 17.2727 11.1329 17.2727 11.4091V13.1363C17.2727 13.4125 17.4966 13.6363 17.7727 13.6363H19.5C19.7761 13.6363 20 13.4125 20 13.1363V11.4091C20 11.1329 19.7761 10.9091 19.5 10.9091H17.7727Z" fill="#AAAAAA"/>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -119,8 +119,8 @@
.smallAvatar {
@include avatar;
width: 48px;
height: 48px;
width: 44px;
height: 44px;
.missingBack {
width: 44px;

View File

@ -10,6 +10,7 @@ const ButtonCopy: Component<{
disabled?: boolean,
label?: string,
labelBeforeIcon?: boolean,
light?: boolean,
}> = (props) => {
const [copying, setCopying] = createSignal(false);
@ -24,7 +25,7 @@ const ButtonCopy: Component<{
return (
<Button.Root
id={props.id}
class={styles.copy}
class={`${styles.copy} ${props.light ? styles.light : ''}`}
onClick={doCopy}
disabled={props.disabled}
>
@ -34,9 +35,9 @@ const ButtonCopy: Component<{
<Show
when={copying()}
fallback={<div class={styles.copyIcon}></div>}
fallback={<div class={`${styles.copyIcon} ${props.labelBeforeIcon ? styles.right : styles.left}`}></div>}
>
<div class={styles.checkIcon}></div>
<div class={`${styles.checkIcon} ${props.labelBeforeIcon ? styles.right : styles.left}`}></div>
</Show>
<Show when={!props.labelBeforeIcon}>

View File

@ -46,10 +46,16 @@
.copyIcon {
width: 16px;
height: 16px;
margin-inline: 8px;
background-color: var(--text-tertiary-2);
-webkit-mask: url(../../assets/icons/copy.svg) no-repeat 0 / 100%;
mask: url(../../assets/icons/copy.svg) no-repeat 0 / 100%;
&.left {
margin-right: 8px;
}
&.right {
margin-left: 8px;
}
}
@ -57,10 +63,16 @@
width: 16px;
height: 16px;
display: inline-block;
margin-inline: 8px;
background-color: var(--success-color);
-webkit-mask: url(../../assets/icons/check.svg) no-repeat 0 / 80%;
mask: url(../../assets/icons/check.svg) no-repeat 0 / 80%;
&.left {
margin-right: 8px;
}
&.right {
margin-left: 8px;
}
}
color: var(--text-tertiary-2);
@ -76,6 +88,9 @@
display: flex;
align-items: center;
&.light {
color: var(--text-secondary);
}
&:focus {
box-shadow: none;

View File

@ -4,10 +4,10 @@ import { defaultZap, defaultZapOptions } from '../../constants';
import { useAccountContext } from '../../contexts/AccountContext';
import { useSettingsContext } from '../../contexts/SettingsContext';
import { hookForDev } from '../../lib/devTools';
import { zapNote } from '../../lib/zap';
import { zapNote, zapProfile } from '../../lib/zap';
import { userName } from '../../stores/profile';
import { toastZapFail, zapCustomOption, actions as tActions, placeholders as tPlaceholders, zapCustomAmount } from '../../translations';
import { PrimalNote, ZapOption } from '../../types/primal';
import { PrimalNote, PrimalUser, ZapOption } from '../../types/primal';
import { debounce } from '../../utils';
import ButtonPrimary from '../Buttons/ButtonPrimary';
import Modal from '../Modal/Modal';
@ -19,10 +19,12 @@ import styles from './CustomZap.module.scss';
const CustomZap: Component<{
id?: string,
open?: boolean,
note: PrimalNote,
note?: PrimalNote,
profile?: PrimalUser,
onConfirm: (zapOption?: ZapOption) => void,
onSuccess: (zapOption?: ZapOption) => void,
onFail: (zapOption?: ZapOption) => void
onFail: (zapOption?: ZapOption) => void,
onCancel: (zapOption?: ZapOption) => void
}> = (props) => {
const toast = useToastContext();
@ -89,13 +91,27 @@ const CustomZap: Component<{
const submit = async () => {
if (account?.hasPublicKey()) {
props.onConfirm(selectedValue());
const success = await zapNote(
let success = false;
if (props.note) {
success = await zapNote(
props.note,
account.publicKey,
selectedValue().amount || 0,
comment(),
account.relays,
);
}
else if (props.profile) {
success = await zapProfile(
props.profile,
account.publicKey,
selectedValue().amount || 0,
comment(),
account.relays,
);
}
if (success) {
props.onSuccess(selectedValue());
@ -111,7 +127,7 @@ const CustomZap: Component<{
};
return (
<Modal open={props.open} onClose={() => props.onFail({ amount: 0, message: '' })}>
<Modal open={props.open} onClose={() => props.onCancel({ amount: 0, message: '' })}>
<div id={props.id} class={styles.customZap}>
<div class={styles.header}>
<div class={styles.title}>
@ -119,13 +135,13 @@ const CustomZap: Component<{
{intl.formatMessage(tActions.zap)}
</div>
</div>
<button class={styles.close} onClick={() => props.onFail({ amount: 0, message: '' })}>
<button class={styles.close} onClick={() => props.onCancel({ amount: 0, message: '' })}>
</button>
</div>
<div class={styles.description}>
{intl.formatMessage(zapCustomOption,{
user: userName(props.note.user),
user: userName(props.note?.user || props.profile),
})}
<span class={styles.amount}>

View File

@ -437,6 +437,15 @@ const NoteFooter: Component<{ note: PrimalNote, wide?: boolean, id?: string }> =
setHideZapIcon(false);
setZapped(props.note.post.noteActions.zapped);
}}
onCancel={(zapOption: ZapOption) => {
setZappedAmount(() => -(zapOption.amount || 0));
setZappedNow(true);
setIsCustomZap(false);
setIsZapping(false);
setShowZapAnim(false);
setHideZapIcon(false);
setZapped(props.note.post.noteActions.zapped);
}}
/>
</div>

View File

@ -0,0 +1,195 @@
.ProfileQrCodeModal {
position: fixed;
min-width: 472px;
color: var(--text-primary);
background-color: var(--background-input);
border-radius: 8px;
display: flex;
flex-direction: column;
padding: 20px 24px 28px 24px;
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-bottom: 24px;
.userInfo {
display: flex;
justify-content: flex-start;
align-items: flex-start;
.avatar {
display: flex;
align-items: center;
justify-content: center;
}
.details {
display: flex;
flex-direction: column;
margin-left: 8px;
justify-content: center;
height: 44px;
.name {
display: flex;
flex-direction: row;
align-items: center;
color: var(--text-primary);
font-size: 20px;
font-weight: 700;
line-height: 20px;
}
.verification {
color: var(--text-tertiary);
font-size: 16px;
font-weight: 400;
line-height: 16px;
}
}
}
.close {
border: none;
outline: none;
padding: 0;
margin: 0;
box-shadow: none;
width: 20px;
height: 20px;
display: inline-block;
margin: 0px 0px;
background-color: var(--text-secondary);
-webkit-mask: url(../../assets/icons/close.svg) no-repeat center;
mask: url(../../assets/icons/close.svg) no-repeat center;
&:hover {
background-color: var(--text-primary);
}
}
}
.tabs {
position: relative;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
padding-inline: 8px;
width: 100%;
border-radius: 0;
padding-top: 22px;
border-bottom: 1px solid var(--devider);
border-top: none;
margin-bottom: 20px;
}
.tab {
position: relative;
display: inline-block;
padding-inline: 14px;
padding-block: 2px;
border: none;
background: none;
width: fit-content;
height: 28px;
margin: 0;
margin-bottom: 12px;
color: var(--text-primary);
font-size: 16px;
font-weight: 600;
line-height: 14px;
&:focus {
outline: none;
box-shadow: none;
}
}
.tabIndicator {
position: absolute;
height: 4px;
top: 54px;
left: 0;
border-radius: 2px 2px 0px 0px;
background: var(--accent);
transition: all 250ms;
}
.tabContent {
@keyframes fadeIn {
from {
opacity:0;
}
to {
opacity:1;
}
}
animation: fadeIn 1s;
}
.keys {
margin-top: 34px;
padding-top: 24px;
border-top: 1px solid var(--subtile-devider);
display: flex;
flex-direction: column;
gap: 16px;
.keyEntry {
display: flex;
justify-content: space-between;
align-items: center;
color: var(--text-secondary);
font-size: 16px;
font-weight: 400;
line-height: 16px;
padding-inline: 12px;
}
}
}
.zapIcon {
width: 22px;
height: 22px;
display: inline-block;
margin-right: 9px;
background: var(--sidebar-section-icon-gradient);
-webkit-mask: url(../../assets/icons/explore/zaps_hollow.svg) no-repeat 2px 0 / 19px 22px;
mask: url(../../assets/icons/explore/zaps_hollow.svg) no-repeat 2px 0 / 19px 22px;
}
.customAmount {
width: 392px;
display: flex;
flex-flow: row-reverse;
align-items: center;
justify-content: space-between;
padding-right: 8px;
padding-bottom: 20px;
border-bottom: 1px solid var(--subtile-devider);
margin-bottom: 20px;
label {
min-width: 120px;
margin-right: 12px;
margin-top: 4px;
color: var(--text-tertiary);
font-size: 16px;
font-weight: 400;
line-height: 20px;
}
}

View File

@ -0,0 +1,135 @@
import { useIntl } from '@cookbook/solid-intl';
import { Tabs } from '@kobalte/core';
import { Component, createEffect, createSignal, For, Show } from 'solid-js';
import { defaultZap, defaultZapOptions } from '../../constants';
import { useAccountContext } from '../../contexts/AccountContext';
import { useSettingsContext } from '../../contexts/SettingsContext';
import { hookForDev } from '../../lib/devTools';
import { truncateNumber } from '../../lib/notifications';
import { zapNote, zapProfile } from '../../lib/zap';
import { authorName, nip05Verification, truncateNpub, userName } from '../../stores/profile';
import { toastZapFail, zapCustomOption, actions as tActions, placeholders as tPlaceholders, zapCustomAmount } from '../../translations';
import { PrimalNote, PrimalUser, ZapOption } from '../../types/primal';
import { debounce } from '../../utils';
import Avatar from '../Avatar/Avatar';
import ButtonCopy from '../Buttons/ButtonCopy';
import ButtonPrimary from '../Buttons/ButtonPrimary';
import Modal from '../Modal/Modal';
import QrCode from '../QrCode/QrCode';
import TextInput from '../TextInput/TextInput';
import { useToastContext } from '../Toaster/Toaster';
import VerificationCheck from '../VerificationCheck/VerificationCheck';
import styles from './ProfileQrCodeModal.module.scss';
const ProfileQrCodeModal: Component<{
id?: string,
open?: boolean,
profile: PrimalUser,
onClose?: () => void,
}> = (props) => {
const toast = useToastContext();
const account = useAccountContext();
const intl = useIntl();
const settings = useSettingsContext();
const profileData = () => Object.entries({
pubkey: {
title: 'Public key',
data: props.profile.npub || props.profile.pubkey,
},
lnAddress: {
title: 'Lightning address',
data: props.profile.lud16 || props.profile.lud06,
}
});
return (
<Modal open={props.open} onClose={props.onClose}>
<div id={props.id} class={styles.ProfileQrCodeModal}>
<div class={styles.header}>
<div class={styles.userInfo}>
<div class={styles.avatar}>
<Avatar
size="sm"
user={props.profile}
/>
</div>
<div class={styles.details}>
<div class={styles.name}>
{authorName(props.profile)}
<VerificationCheck user={props.profile} />
</div>
<div class={styles.verification} title={props.profile?.nip05}>
<Show when={props.profile?.nip05}>
<span
class={styles.verifiedBy}
title={props.profile?.nip05}
>
{nip05Verification(props.profile)}
</span>
</Show>
</div>
</div>
</div>
<button class={styles.close} onClick={props.onClose}>
</button>
</div>
<div class={styles.qrCode}>
<Tabs.Root>
<Tabs.List class={styles.tabs}>
<For each={profileData()}>
{([key, info]) =>
<Show when={info.data}>
<Tabs.Trigger class={styles.tab} value={key} >
{info.title}
</Tabs.Trigger>
</Show>
}
</For>
<Tabs.Indicator class={styles.tabIndicator} />
</Tabs.List>
<For each={profileData()}>
{([key, info]) =>
<Show when={info.data}>
<Tabs.Content class={styles.tabContent} value={key}>
<QrCode data={info.data} />
</Tabs.Content>
</Show>
}
</For>
</Tabs.Root>
</div>
<div class={styles.keys}>
<For each={profileData()}>
{([key, info]) =>
<Show when={info.data}>
<div class={styles.keyEntry}>
<div class={styles.label}>
{info.title}:
</div>
<div class={styles.value}>
<ButtonCopy
light={true}
copyValue={info.data}
labelBeforeIcon={true}
label={truncateNpub(info.data)}
/>
</div>
</div>
</Show>
}
</For>
</div>
</div>
</Modal>
);
}
export default hookForDev(ProfileQrCodeModal);

View File

@ -0,0 +1,12 @@
.container {
display: flex;
justify-content: center;
width: 100%;
.frame {
width: 280px;
height: 280px;
border-radius: 24px;
overflow: hidden;
}
}

View File

@ -0,0 +1,65 @@
import QRCodeStyling from 'qr-code-styling';
import { Component, createEffect, onMount } from 'solid-js';
import primalLogoFire from '../../assets/icons/logo_fire.svg'
import primalLogoIce from '../../assets/icons/logo_ice.svg'
import { useSettingsContext } from '../../contexts/SettingsContext';
import styles from './QrCode.module.scss';
const QrCode: Component<{ data: string }> = (props) => {
let qrSlot: HTMLDivElement | undefined;
const settings = useSettingsContext();
const isIce = () => ['midnight', 'ice'].includes(settings?.theme || '');
createEffect(() => {
const qrCode = new QRCodeStyling({
width: 280,
height: 280,
type: "svg",
data: props.data,
margin: 6,
image: isIce() ? primalLogoIce : primalLogoFire,
qrOptions: {
typeNumber: 0,
mode: "Byte",
errorCorrectionLevel :"Q",
},
imageOptions: {
hideBackgroundDots: true,
imageSize:0.2,
margin:0,
},
dotsOptions:{
type: "square",
color: 'black',
},
cornersSquareOptions: {
type: "square",
color: 'black',
},
cornersDotOptions: {
type: "square",
color: 'black',
},
backgroundOptions: {
color: 'white',
},
});
qrCode.append(qrSlot);
});
return (
<div class={styles.container}>
<div class={styles.frame}>
<div id="qrSlot" ref={qrSlot}></div>
</div>
</div>
);
}
export default QrCode;

View File

@ -43,6 +43,44 @@ export const zapNote = async (note: PrimalNote, sender: string | undefined, amou
}
}
export const zapProfile = async (profile: PrimalUser, sender: string | undefined, amount: number, comment = '', relays: Relay[]) => {
if (!sender || !profile) {
return false;
}
const callback = await getZapEndpoint(profile);
if (!callback) {
return false;
}
const sats = Math.round(amount * 1000);
const zapReq = nip57.makeZapRequest({
profile: profile.pubkey,
amount: sats,
comment,
relays: relays.map(r => r.url)
});
try {
const signedEvent = await signEvent(zapReq);
const event = encodeURI(JSON.stringify(signedEvent));
const r2 = await (await fetch(`${callback}?amount=${sats}&nostr=${event}`)).json();
const pr = r2.pr;
await enableWebLn();
await sendPayment(pr);
return true;
} catch (reason) {
console.error('Failed to zap: ', reason);
return false;
}
}
export const getZapEndpoint = async (user: PrimalUser): Promise<string | null> => {
try {
let lnurl: string = ''

View File

@ -170,6 +170,16 @@
mask: url(../assets/icons/feed_zap.svg) no-repeat 0px / 20px;
}
.qrIcon {
width: 20px;
height: 20px;
display: inline-block;
margin: 0px;
background-color: var(--text-primary);
-webkit-mask: url(../assets/icons/qr_code.svg) no-repeat 0px / 20px;
mask: url(../assets/icons/qr_code.svg) no-repeat 0px / 20px;
}
.contextIcon {
width: 20px;
height: 20px;

View File

@ -25,7 +25,7 @@ import { shortDate } from '../lib/dates';
import styles from './Profile.module.scss';
import StickySidebar from '../components/StickySidebar/StickySidebar';
import ProfileSidebar from '../components/ProfileSidebar/ProfileSidebar';
import { MenuItem, PrimalUser, VanityProfiles } from '../types/primal';
import { MenuItem, PrimalUser, VanityProfiles, ZapOption } from '../types/primal';
import PageTitle from '../components/PageTitle/PageTitle';
import FollowButton from '../components/FollowButton/FollowButton';
import Search from '../components/Search/Search';
@ -43,6 +43,9 @@ import VerificationCheck from '../components/VerificationCheck/VerificationCheck
import PhotoSwipeLightbox from 'photoswipe/lightbox';
import NoteImage from '../components/NoteImage/NoteImage';
import { createStore } from 'solid-js/store';
import CustomZap from '../components/CustomZap/CustomZap';
import Modal from '../components/Modal/Modal';
import ProfileQrCodeModal from '../components/ProfileQrCodeModal/ProfileQrCodeModal';
const Profile: Component = () => {
@ -61,6 +64,8 @@ const Profile: Component = () => {
const [showContext, setContext] = createSignal(false);
const [confirmReportUser, setConfirmReportUser] = createSignal(false);
const [confirmMuteUser, setConfirmMuteUser] = createSignal(false);
const [isCustomZap, setIsCustomZap] = createSignal(false);
const [openQr, setOpenQr] = createSignal(false);
const lightbox = new PhotoSwipeLightbox({
gallery: '#central_header',
@ -537,14 +542,46 @@ const Profile: Component = () => {
hidden={!showContext()}
/>
</div>
<ButtonSecondary
onClick={() => setOpenQr(true)}
shrink={true}
>
<div class={styles.qrIcon}></div>
</ButtonSecondary>
<ProfileQrCodeModal
open={openQr()}
onClose={() => setOpenQr(false)}
profile={profile?.userProfile}
/>
<Show when={!isCurrentUser()}>
<ButtonSecondary
onClick={onNotImplemented}
onClick={() => setIsCustomZap(true)}
shrink={true}
>
<div class={styles.zapIcon}></div>
</ButtonSecondary>
<CustomZap
open={isCustomZap()}
profile={profile?.userProfile}
onConfirm={(zapOption: ZapOption) => {
setIsCustomZap(false);
}}
onSuccess={(zapOption: ZapOption) => {
setIsCustomZap(false);
toaster?.sendSuccess("Profile successfully zapped")
}}
onFail={(zapOption: ZapOption) => {
setIsCustomZap(false);
toaster?.sendWarning("Zaping failed")
}}
onCancel={(zapOption: ZapOption) => {
setIsCustomZap(false);
toaster?.sendWarning("Zaping canceled")
}}
/>
</Show>
<Show when={account?.publicKey}>