New spotlight media
This commit is contained in:
parent
cdf9a00fcb
commit
2adce0ead1
@ -282,6 +282,11 @@
|
|||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.49466 2.78774C7.73973 1.25408 5.14439 0.940234 3.12891 2.6623C0.948817 4.52502 0.63207 7.66213 2.35603 9.88052C3.01043 10.7226 4.28767 11.9877 5.51513 13.1462C6.75696 14.3184 7.99593 15.426 8.60692 15.9671C8.61074 15.9705 8.61463 15.9739 8.61859 15.9774C8.67603 16.0283 8.74753 16.0917 8.81608 16.1433C8.89816 16.2052 9.01599 16.2819 9.17334 16.3288C9.38253 16.3912 9.60738 16.3912 9.81656 16.3288C9.97391 16.2819 10.0917 16.2052 10.1738 16.1433C10.2424 16.0917 10.3139 16.0283 10.3713 15.9774C10.3753 15.9739 10.3792 15.9705 10.383 15.9671C10.994 15.426 12.2329 14.3184 13.4748 13.1462C14.7022 11.9877 15.9795 10.7226 16.6339 9.88052C18.3512 7.67065 18.0834 4.50935 15.8532 2.65572C13.8153 0.961905 11.2476 1.25349 9.49466 2.78774Z" fill="currentColor"/>
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.49466 2.78774C7.73973 1.25408 5.14439 0.940234 3.12891 2.6623C0.948817 4.52502 0.63207 7.66213 2.35603 9.88052C3.01043 10.7226 4.28767 11.9877 5.51513 13.1462C6.75696 14.3184 7.99593 15.426 8.60692 15.9671C8.61074 15.9705 8.61463 15.9739 8.61859 15.9774C8.67603 16.0283 8.74753 16.0917 8.81608 16.1433C8.89816 16.2052 9.01599 16.2819 9.17334 16.3288C9.38253 16.3912 9.60738 16.3912 9.81656 16.3288C9.97391 16.2819 10.0917 16.2052 10.1738 16.1433C10.2424 16.0917 10.3139 16.0283 10.3713 15.9774C10.3753 15.9739 10.3792 15.9705 10.383 15.9671C10.994 15.426 12.2329 14.3184 13.4748 13.1462C14.7022 11.9877 15.9795 10.7226 16.6339 9.88052C18.3512 7.67065 18.0834 4.50935 15.8532 2.65572C13.8153 0.961905 11.2476 1.25349 9.49466 2.78774Z" fill="currentColor"/>
|
||||||
</g>
|
</g>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
<symbol id="x-close" viewBox="0 0 24 24" fill="none">
|
||||||
|
<g>
|
||||||
|
<path d="M18 6L6 18M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 73 KiB |
@ -1,15 +0,0 @@
|
|||||||
.modal.spotlight .modal-body {
|
|
||||||
max-width: 100vw;
|
|
||||||
width: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal.spotlight img,
|
|
||||||
.modal.spotlight video {
|
|
||||||
max-width: 90vw;
|
|
||||||
max-height: 90vh;
|
|
||||||
aspect-ratio: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal.spotlight .close {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
@ -1,18 +1,5 @@
|
|||||||
import { ProxyImg } from "Element/ProxyImg";
|
import { ProxyImg } from "Element/ProxyImg";
|
||||||
import React, { MouseEvent, useEffect, useState } from "react";
|
import React from "react";
|
||||||
import { FormattedMessage, FormattedNumber } from "react-intl";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import { decodeInvoice, InvoiceDetails } from "@snort/shared";
|
|
||||||
|
|
||||||
import "./MediaElement.css";
|
|
||||||
import Modal from "Element/Modal";
|
|
||||||
import Icon from "Icons/Icon";
|
|
||||||
import { kvToObject } from "SnortUtils";
|
|
||||||
import AsyncButton from "Element/AsyncButton";
|
|
||||||
import { useWallet } from "Wallet";
|
|
||||||
import { PaymentsCache } from "Cache";
|
|
||||||
import { Payment } from "Db";
|
|
||||||
import PageSpinner from "Element/PageSpinner";
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
[
|
[
|
||||||
@ -28,162 +15,16 @@ interface MediaElementProps {
|
|||||||
magnet?: string;
|
magnet?: string;
|
||||||
sha256?: string;
|
sha256?: string;
|
||||||
blurHash?: string;
|
blurHash?: string;
|
||||||
disableSpotlight?: boolean;
|
onMediaClick?: (e: React.MouseEvent<HTMLImageElement>) => void;
|
||||||
}
|
|
||||||
|
|
||||||
interface L402Object {
|
|
||||||
macaroon: string;
|
|
||||||
invoice: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MediaElement(props: MediaElementProps) {
|
export function MediaElement(props: MediaElementProps) {
|
||||||
const [invoice, setInvoice] = useState<InvoiceDetails>();
|
|
||||||
const [l402, setL402] = useState<L402Object>();
|
|
||||||
const [auth, setAuth] = useState<Payment>();
|
|
||||||
const [error, setError] = useState("");
|
|
||||||
const [url, setUrl] = useState(props.url);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const wallet = useWallet();
|
|
||||||
|
|
||||||
async function probeFor402() {
|
|
||||||
const cached = await PaymentsCache.get(props.url);
|
|
||||||
if (cached) {
|
|
||||||
setAuth(cached);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const req = new Request(props.url, {
|
|
||||||
method: "OPTIONS",
|
|
||||||
headers: {
|
|
||||||
accept: "L402",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const rsp = await fetch(req);
|
|
||||||
if (rsp.status === 402) {
|
|
||||||
const auth = rsp.headers.get("www-authenticate");
|
|
||||||
if (auth?.startsWith("L402")) {
|
|
||||||
const vals = kvToObject<L402Object>(auth.substring(5));
|
|
||||||
console.debug(vals);
|
|
||||||
setL402(vals);
|
|
||||||
|
|
||||||
if (vals.invoice) {
|
|
||||||
const decoded = decodeInvoice(vals.invoice);
|
|
||||||
setInvoice(decoded);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function payInvoice() {
|
|
||||||
if (wallet.wallet && l402) {
|
|
||||||
try {
|
|
||||||
const res = await wallet.wallet.payInvoice(l402.invoice);
|
|
||||||
console.debug(res);
|
|
||||||
if (res.preimage) {
|
|
||||||
const pmt = {
|
|
||||||
pr: l402.invoice,
|
|
||||||
url: props.url,
|
|
||||||
macaroon: l402.macaroon,
|
|
||||||
preimage: res.preimage,
|
|
||||||
};
|
|
||||||
await PaymentsCache.set(pmt);
|
|
||||||
setAuth(pmt);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Error) {
|
|
||||||
setError(e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadMedia() {
|
|
||||||
if (!auth) return;
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const mediaReq = new Request(props.url, {
|
|
||||||
headers: {
|
|
||||||
Authorization: `L402 ${auth.macaroon}:${auth.preimage}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const rsp = await fetch(mediaReq);
|
|
||||||
if (rsp.ok) {
|
|
||||||
const buf = await rsp.blob();
|
|
||||||
setUrl(URL.createObjectURL(buf));
|
|
||||||
}
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (auth) {
|
|
||||||
loadMedia().catch(console.error);
|
|
||||||
}
|
|
||||||
}, [auth]);
|
|
||||||
|
|
||||||
if (auth && loading) {
|
|
||||||
return <PageSpinner />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (invoice) {
|
|
||||||
return (
|
|
||||||
<div className="note-invoice">
|
|
||||||
<h3>
|
|
||||||
<FormattedMessage defaultMessage="Payment Required" />
|
|
||||||
</h3>
|
|
||||||
<div className="flex f-row">
|
|
||||||
<div className="f-grow">
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="You must pay {n} sats to access this file."
|
|
||||||
values={{
|
|
||||||
n: <FormattedNumber value={(invoice.amount ?? 0) / 1000} />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{wallet.wallet && (
|
|
||||||
<AsyncButton onClick={() => payInvoice()}>
|
|
||||||
<FormattedMessage defaultMessage="Pay Now" />
|
|
||||||
</AsyncButton>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{!wallet.wallet && (
|
|
||||||
<b>
|
|
||||||
<FormattedMessage
|
|
||||||
defaultMessage="Please connect a wallet {here} to be able to pay this invoice"
|
|
||||||
values={{
|
|
||||||
here: (
|
|
||||||
<Link to="/settings/wallet" onClick={e => e.stopPropagation()}>
|
|
||||||
<FormattedMessage defaultMessage="here" description="Inline link text pointing to another page" />
|
|
||||||
</Link>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</b>
|
|
||||||
)}
|
|
||||||
{error && <b className="error">{error}</b>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.mime.startsWith("image/")) {
|
if (props.mime.startsWith("image/")) {
|
||||||
if (!(props.disableSpotlight ?? false)) {
|
return <ProxyImg key={props.url} src={props.url} onClick={props.onMediaClick} />;
|
||||||
return (
|
|
||||||
<SpotlightMedia>
|
|
||||||
<ProxyImg key={props.url} src={url} onError={() => probeFor402()} />
|
|
||||||
</SpotlightMedia>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return <ProxyImg key={props.url} src={url} onError={() => probeFor402()} />;
|
|
||||||
}
|
|
||||||
} else if (props.mime.startsWith("audio/")) {
|
} else if (props.mime.startsWith("audio/")) {
|
||||||
return <audio key={props.url} src={url} controls onError={() => probeFor402()} />;
|
return <audio key={props.url} src={props.url} controls />;
|
||||||
} else if (props.mime.startsWith("video/")) {
|
} else if (props.mime.startsWith("video/")) {
|
||||||
return <video key={props.url} src={url} controls onError={() => probeFor402()} />;
|
return <video key={props.url} src={props.url} controls />;
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
@ -198,33 +39,3 @@ export function MediaElement(props: MediaElementProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SpotlightMedia({ children }: { children: React.ReactNode }) {
|
|
||||||
const [showModal, setShowModal] = useState(false);
|
|
||||||
|
|
||||||
function onClick(e: MouseEvent<HTMLDivElement>) {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
setShowModal(s => !s);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onClose(e: MouseEvent<HTMLDivElement>) {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
setShowModal(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{showModal && (
|
|
||||||
<Modal onClose={onClose} className="spotlight">
|
|
||||||
<div className="close" onClick={onClose}>
|
|
||||||
<Icon name="close" />
|
|
||||||
</div>
|
|
||||||
{children}
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
<div onClick={onClick}>{children}</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
@ -8,7 +8,7 @@ import { MediaElement } from "Element/MediaElement";
|
|||||||
interface RevealMediaProps {
|
interface RevealMediaProps {
|
||||||
creator: string;
|
creator: string;
|
||||||
link: string;
|
link: string;
|
||||||
disableSpotlight?: boolean;
|
onMediaClick?: (e: React.MouseEvent<HTMLImageElement>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RevealMedia(props: RevealMediaProps) {
|
export default function RevealMedia(props: RevealMediaProps) {
|
||||||
@ -53,12 +53,10 @@ export default function RevealMedia(props: RevealMediaProps) {
|
|||||||
return (
|
return (
|
||||||
<Reveal
|
<Reveal
|
||||||
message={<FormattedMessage defaultMessage="Click to load content from {link}" values={{ link: hostname }} />}>
|
message={<FormattedMessage defaultMessage="Click to load content from {link}" values={{ link: hostname }} />}>
|
||||||
<MediaElement mime={`${type}/${extension}`} url={url.toString()} disableSpotlight={props.disableSpotlight} />
|
<MediaElement mime={`${type}/${extension}`} url={url.toString()} onMediaClick={props.onMediaClick} />
|
||||||
</Reveal>
|
</Reveal>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return <MediaElement mime={`${type}/${extension}`} url={url.toString()} onMediaClick={props.onMediaClick} />;
|
||||||
<MediaElement mime={`${type}/${extension}`} url={url.toString()} disableSpotlight={props.disableSpotlight} />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
45
packages/app/src/Element/SpotlightMedia.css
Normal file
45
packages/app/src/Element/SpotlightMedia.css
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
.modal.spotlight .modal-body {
|
||||||
|
border: none;
|
||||||
|
border-radius: unset;
|
||||||
|
width: unset;
|
||||||
|
height: unset;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.spotlight img,
|
||||||
|
.modal.spotlight video {
|
||||||
|
max-width: 100vw;
|
||||||
|
max-height: 100vh;
|
||||||
|
aspect-ratio: unset;
|
||||||
|
width: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.spotlight .details {
|
||||||
|
text-align: right;
|
||||||
|
position: absolute;
|
||||||
|
top: 28px;
|
||||||
|
right: 28px;
|
||||||
|
gap: 18px;
|
||||||
|
display: flex;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 24px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.spotlight .left {
|
||||||
|
position: absolute;
|
||||||
|
left: 24px;
|
||||||
|
top: 50vh;
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.spotlight .right {
|
||||||
|
position: absolute;
|
||||||
|
right: 24px;
|
||||||
|
top: 50vh;
|
||||||
|
}
|
54
packages/app/src/Element/SpotlightMedia.tsx
Normal file
54
packages/app/src/Element/SpotlightMedia.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import "./SpotlightMedia.css";
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import Modal from "Element/Modal";
|
||||||
|
import Icon from "Icons/Icon";
|
||||||
|
import { ProxyImg } from "Element/ProxyImg";
|
||||||
|
|
||||||
|
interface SpotlightMediaProps {
|
||||||
|
images: Array<string>;
|
||||||
|
idx: number;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SpotlightMedia(props: SpotlightMediaProps) {
|
||||||
|
const [idx, setIdx] = useState(props.idx);
|
||||||
|
|
||||||
|
const image = useMemo(() => {
|
||||||
|
return props.images.at(idx % props.images.length);
|
||||||
|
}, [idx, props]);
|
||||||
|
|
||||||
|
function dec() {
|
||||||
|
setIdx(s => {
|
||||||
|
if (s - 1 === -1) {
|
||||||
|
return props.images.length - 1;
|
||||||
|
} else {
|
||||||
|
return s - 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function inc() {
|
||||||
|
setIdx(s => {
|
||||||
|
if (s + 1 === props.images.length) {
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
return s + 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Modal onClose={props.onClose} className="spotlight">
|
||||||
|
<ProxyImg src={image} />
|
||||||
|
<div className="details">
|
||||||
|
{idx + 1}/{props.images.length}
|
||||||
|
<Icon name="x-close" size={24} onClick={props.onClose} />
|
||||||
|
</div>
|
||||||
|
{props.images.length > 1 && (
|
||||||
|
<>
|
||||||
|
<Icon className="left" name="arrowFront" size={24} onClick={() => dec()} />
|
||||||
|
<Icon className="right" name="arrowFront" size={24} onClick={() => inc()} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import "./Text.css";
|
import "./Text.css";
|
||||||
import { useMemo } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { HexKey, ParsedFragment, transformText } from "@snort/system";
|
import { HexKey, ParsedFragment, transformText } from "@snort/system";
|
||||||
|
|
||||||
import Invoice from "Element/Invoice";
|
import Invoice from "Element/Invoice";
|
||||||
@ -8,6 +8,7 @@ import HyperText from "Element/HyperText";
|
|||||||
import CashuNuts from "Element/CashuNuts";
|
import CashuNuts from "Element/CashuNuts";
|
||||||
import RevealMedia from "./RevealMedia";
|
import RevealMedia from "./RevealMedia";
|
||||||
import { ProxyImg } from "./ProxyImg";
|
import { ProxyImg } from "./ProxyImg";
|
||||||
|
import { SpotlightMedia } from "./SpotlightMedia";
|
||||||
|
|
||||||
export interface TextProps {
|
export interface TextProps {
|
||||||
content: string;
|
content: string;
|
||||||
@ -19,6 +20,15 @@ export interface TextProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Text({ content, tags, creator, disableMedia, depth, disableMediaSpotlight }: TextProps) {
|
export default function Text({ content, tags, creator, disableMedia, depth, disableMediaSpotlight }: TextProps) {
|
||||||
|
const [showSpotlight, setShowSpotlight] = useState(false);
|
||||||
|
const [imageIdx, setImageIdx] = useState(0);
|
||||||
|
|
||||||
|
const elements = useMemo(() => {
|
||||||
|
return transformText(content, tags);
|
||||||
|
}, [content]);
|
||||||
|
|
||||||
|
const images = elements.filter(a => a.type === "media" && a.mimeType?.startsWith("image")).map(a => a.content);
|
||||||
|
|
||||||
function renderChunk(a: ParsedFragment) {
|
function renderChunk(a: ParsedFragment) {
|
||||||
if (a.type === "media" && !a.mimeType?.startsWith("unknown")) {
|
if (a.type === "media" && !a.mimeType?.startsWith("unknown")) {
|
||||||
if (disableMedia ?? false) {
|
if (disableMedia ?? false) {
|
||||||
@ -28,7 +38,21 @@ export default function Text({ content, tags, creator, disableMedia, depth, disa
|
|||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <RevealMedia link={a.content} creator={creator} disableSpotlight={disableMediaSpotlight} />;
|
return (
|
||||||
|
<RevealMedia
|
||||||
|
link={a.content}
|
||||||
|
creator={creator}
|
||||||
|
onMediaClick={e => {
|
||||||
|
if (!disableMediaSpotlight) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
setShowSpotlight(true);
|
||||||
|
const selected = images.findIndex(b => b === a.content);
|
||||||
|
setImageIdx(selected === -1 ? 0 : selected);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
switch (a.type) {
|
switch (a.type) {
|
||||||
case "invoice":
|
case "invoice":
|
||||||
@ -48,13 +72,10 @@ export default function Text({ content, tags, creator, disableMedia, depth, disa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const elements = useMemo(() => {
|
|
||||||
return transformText(content, tags);
|
|
||||||
}, [content]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div dir="auto" className="text">
|
<div dir="auto" className="text">
|
||||||
{elements.map(a => renderChunk(a))}
|
{elements.map(a => renderChunk(a))}
|
||||||
|
{showSpotlight && <SpotlightMedia images={images} onClose={() => setShowSpotlight(false)} idx={imageIdx} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,10 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logo:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 10px 16px;
|
padding: 10px 16px;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import "./Layout.css";
|
import "./Layout.css";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
import { Link, Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { useUserProfile } from "@snort/system-react";
|
import { useUserProfile } from "@snort/system-react";
|
||||||
|
|
||||||
@ -103,7 +103,7 @@ export default function Layout() {
|
|||||||
<div className={pageClass}>
|
<div className={pageClass}>
|
||||||
{!shouldHideHeader && (
|
{!shouldHideHeader && (
|
||||||
<header className="main-content">
|
<header className="main-content">
|
||||||
<div className="logo" onClick={() => navigate("/")}>
|
<Link to="/" className="logo">
|
||||||
<h1>Snort</h1>
|
<h1>Snort</h1>
|
||||||
{currentSubscription && (
|
{currentSubscription && (
|
||||||
<small className="flex">
|
<small className="flex">
|
||||||
@ -111,7 +111,7 @@ export default function Layout() {
|
|||||||
{mapPlanName(currentSubscription.type)}
|
{mapPlanName(currentSubscription.type)}
|
||||||
</small>
|
</small>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Link>
|
||||||
|
|
||||||
{publicKey ? (
|
{publicKey ? (
|
||||||
<AccountHeader />
|
<AccountHeader />
|
||||||
@ -150,8 +150,7 @@ const AccountHeader = () => {
|
|||||||
);
|
);
|
||||||
const unreadDms = useMemo(() => (publicKey ? 0 : 0), [publicKey]);
|
const unreadDms = useMemo(() => (publicKey ? 0 : 0), [publicKey]);
|
||||||
|
|
||||||
async function goToNotifications(e: React.MouseEvent) {
|
async function goToNotifications() {
|
||||||
e.stopPropagation();
|
|
||||||
// request permissions to send notifications
|
// request permissions to send notifications
|
||||||
if ("Notification" in window) {
|
if ("Notification" in window) {
|
||||||
try {
|
try {
|
||||||
@ -163,7 +162,6 @@ const AccountHeader = () => {
|
|||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
navigate("/notifications");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -172,14 +170,14 @@ const AccountHeader = () => {
|
|||||||
<input type="text" placeholder={formatMessage({ defaultMessage: "Search" })} className="w-max" />
|
<input type="text" placeholder={formatMessage({ defaultMessage: "Search" })} className="w-max" />
|
||||||
<Icon name="search" size={24} />
|
<Icon name="search" size={24} />
|
||||||
</div>
|
</div>
|
||||||
<div className="btn" onClick={() => navigate("/messages")}>
|
<Link className="btn" to="/messages">
|
||||||
<Icon name="mail" size={24} />
|
<Icon name="mail" size={24} />
|
||||||
{unreadDms > 0 && <span className="has-unread"></span>}
|
{unreadDms > 0 && <span className="has-unread"></span>}
|
||||||
</div>
|
</Link>
|
||||||
<div className="btn" onClick={goToNotifications}>
|
<Link className="btn" to="/notifications" onClick={goToNotifications}>
|
||||||
<Icon name="bell-v2" size={24} />
|
<Icon name="bell-v2" size={24} />
|
||||||
{hasNotifications && <span className="has-unread"></span>}
|
{hasNotifications && <span className="has-unread"></span>}
|
||||||
</div>
|
</Link>
|
||||||
<Avatar
|
<Avatar
|
||||||
user={profile}
|
user={profile}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user