diff --git a/package.json b/package.json index ec436ed4..71e0208d 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "scripts": { "build": "yarn workspace @snort/shared build && yarn workspace @snort/system build && yarn workspace @snort/system-react build && yarn workspace @snort/app build", "start": "yarn workspace @snort/shared build && yarn workspace @snort/system build && yarn workspace @snort/system-react build && yarn workspace @snort/app start", - "test": "yarn workspace @snort/shared build && yarn workspace @snort/system build && yarn workspace @snort/app test && yarn workspace @snort/system test" + "test": "yarn workspace @snort/shared build && yarn workspace @snort/system build && yarn workspace @snort/app test && yarn workspace @snort/system test", + "pre:commit": "yarn workspace @snort/app intl-extract && yarn workspace @snort/app intl-compile && yarn prettier --write ." }, "devDependencies": { "@cloudflare/workers-types": "^4.20230307.0", diff --git a/packages/app/package.json b/packages/app/package.json index c83e6437..ca86e33a 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -6,7 +6,6 @@ "@lightninglabs/lnc-web": "^0.2.3-alpha", "@noble/curves": "^1.0.0", "@noble/hashes": "^1.2.0", - "@reduxjs/toolkit": "^1.9.1", "@scure/base": "^1.1.1", "@scure/bip32": "^1.3.0", "@scure/bip39": "^1.1.1", @@ -15,6 +14,7 @@ "@snort/system-query": "workspace:*", "@snort/system-react": "workspace:*", "@szhsin/react-menu": "^3.3.1", + "@types/use-sync-external-store": "^0.0.4", "@void-cat/api": "^1.0.4", "debug": "^4.3.4", "dexie": "^3.2.4", @@ -27,11 +27,11 @@ "react-dom": "^18.2.0", "react-intersection-observer": "^9.4.1", "react-intl": "^6.4.4", - "react-redux": "^8.0.5", "react-router-dom": "^6.5.0", "react-textarea-autosize": "^8.4.0", "react-twitter-embed": "^4.0.4", "use-long-press": "^2.0.3", + "use-sync-external-store": "^1.2.0", "uuid": "^9.0.0", "workbox-core": "^6.4.2", "workbox-precaching": "^7.0.0", diff --git a/packages/app/public/icons.svg b/packages/app/public/icons.svg index a59d8b8a..8da33944 100644 --- a/packages/app/public/icons.svg +++ b/packages/app/public/icons.svg @@ -330,6 +330,15 @@ + + + + + + + + + \ No newline at end of file diff --git a/packages/app/src/Cache/GiftWrapCache.ts b/packages/app/src/Cache/GiftWrapCache.ts index 67f00b91..f1f6fde0 100644 --- a/packages/app/src/Cache/GiftWrapCache.ts +++ b/packages/app/src/Cache/GiftWrapCache.ts @@ -2,7 +2,7 @@ import { EventKind, EventPublisher, RequestBuilder, TaggedNostrEvent } from "@sn import { UnwrappedGift, db } from "Db"; import { findTag, unwrap } from "SnortUtils"; import { RefreshFeedCache } from "./RefreshFeedCache"; -import { LoginSession } from "Login"; +import { LoginSession, LoginSessionType } from "Login"; export class GiftWrapCache extends RefreshFeedCache { constructor() { @@ -15,7 +15,7 @@ export class GiftWrapCache extends RefreshFeedCache { buildSub(session: LoginSession, rb: RequestBuilder): void { const pubkey = session.publicKey; - if (pubkey) { + if (pubkey && session.type === LoginSessionType.PrivateKey) { rb.withFilter().kinds([EventKind.GiftWrap]).tag("p", [pubkey]).since(this.newest()); } } diff --git a/packages/app/src/Element/Avatar.tsx b/packages/app/src/Element/Avatar.tsx index 0d36c6ac..e43885f4 100644 --- a/packages/app/src/Element/Avatar.tsx +++ b/packages/app/src/Element/Avatar.tsx @@ -21,10 +21,11 @@ const Avatar = ({ pubkey, user, size, onClick, image, imageOverlay, icons }: Ava const [url, setUrl] = useState(""); const { proxy } = useImgProxy(); + const s = size ?? 120; useEffect(() => { const url = image ?? user?.picture; if (url) { - const proxyUrl = proxy(url, size ?? 120); + const proxyUrl = proxy(url, s); setUrl(proxyUrl); } else { setUrl(defaultAvatar(pubkey)); @@ -33,6 +34,10 @@ const Avatar = ({ pubkey, user, size, onClick, image, imageOverlay, icons }: Ava const backgroundImage = `url(${url})`; const style = { "--img-url": backgroundImage } as CSSProperties; + if (size) { + style.width = `${s}px`; + style.height = `${s}px`; + } const domain = user?.nip05 && user.nip05.split("@")[1]; return (
{showModal && ( - setShowModal(false)}> + setShowModal(false)}>
setShowModal(false)}> diff --git a/packages/app/src/Element/CashuNuts.css b/packages/app/src/Element/CashuNuts.css new file mode 100644 index 00000000..f82778b6 --- /dev/null +++ b/packages/app/src/Element/CashuNuts.css @@ -0,0 +1,8 @@ +.cashu { + background: var(--cashu-gradient); +} + +.cashu h1 { + font-size: 44px; + line-height: 1em; +} diff --git a/packages/app/src/Element/CashuNuts.tsx b/packages/app/src/Element/CashuNuts.tsx index 4e124e38..74be20d0 100644 --- a/packages/app/src/Element/CashuNuts.tsx +++ b/packages/app/src/Element/CashuNuts.tsx @@ -1,8 +1,10 @@ +import "./CashuNuts.css"; import { useEffect, useState } from "react"; -import { FormattedMessage } from "react-intl"; +import { FormattedMessage, FormattedNumber } from "react-intl"; +import { useUserProfile } from "@snort/system-react"; import useLogin from "Hooks/useLogin"; -import { useUserProfile } from "@snort/system-react"; +import Icon from "Icons/Icon"; interface Token { token: Array<{ @@ -48,33 +50,87 @@ export default function CashuNuts({ token }: { token: string }) { if (!cashu) return <>{token}; + const amount = cashu.token[0].proofs.reduce((acc, v) => acc + v.amount, 0); return ( -
-
-
-

- -

-

- acc + v.amount, 0), - }} - /> -

- - - -
-
- - +
+
+
+ + + + + + + + + + + + + + + + + + + + + +

{c}

, + n: , + }} + />
+ + {c}, + url: new URL(cashu.token[0].mint).hostname, + }} + /> + +
+
+ +
); diff --git a/packages/app/src/Element/DM.tsx b/packages/app/src/Element/DM.tsx index b9a4c5e7..13ce1bf0 100644 --- a/packages/app/src/Element/DM.tsx +++ b/packages/app/src/Element/DM.tsx @@ -3,7 +3,7 @@ import { useEffect, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useInView } from "react-intersection-observer"; -import useEventPublisher from "Feed/EventPublisher"; +import useEventPublisher from "Hooks/useEventPublisher"; import NoteTime from "Element/NoteTime"; import Text from "Element/Text"; import useLogin from "Hooks/useLogin"; diff --git a/packages/app/src/Element/Deck/Articles.tsx b/packages/app/src/Element/Deck/Articles.tsx index 10efb1bc..b63c7498 100644 --- a/packages/app/src/Element/Deck/Articles.tsx +++ b/packages/app/src/Element/Deck/Articles.tsx @@ -2,7 +2,7 @@ import { NostrLink } from "@snort/system"; import { useArticles } from "Feed/ArticlesFeed"; import { orderDescending } from "SnortUtils"; import Note from "../Note"; -import { useReactions } from "Feed/FeedReactions"; +import { useReactions } from "Feed/Reactions"; export default function Articles() { const data = useArticles(); diff --git a/packages/app/src/Element/FollowButton.tsx b/packages/app/src/Element/FollowButton.tsx index c2cb0e54..b47f1445 100644 --- a/packages/app/src/Element/FollowButton.tsx +++ b/packages/app/src/Element/FollowButton.tsx @@ -2,7 +2,7 @@ import "./FollowButton.css"; import { FormattedMessage } from "react-intl"; import { HexKey } from "@snort/system"; -import useEventPublisher from "Feed/EventPublisher"; +import useEventPublisher from "Hooks/useEventPublisher"; import { parseId } from "SnortUtils"; import useLogin from "Hooks/useLogin"; import AsyncButton from "Element/AsyncButton"; diff --git a/packages/app/src/Element/FollowListBase.tsx b/packages/app/src/Element/FollowListBase.tsx index 31e84f34..5e9ae9d7 100644 --- a/packages/app/src/Element/FollowListBase.tsx +++ b/packages/app/src/Element/FollowListBase.tsx @@ -2,13 +2,16 @@ import { ReactNode } from "react"; import { FormattedMessage } from "react-intl"; import { HexKey } from "@snort/system"; -import useEventPublisher from "Feed/EventPublisher"; +import useEventPublisher from "Hooks/useEventPublisher"; import ProfilePreview from "Element/ProfilePreview"; import useLogin from "Hooks/useLogin"; import { System } from "index"; import messages from "./messages"; import { FollowsFeed } from "Cache"; +import AsyncButton from "./AsyncButton"; +import { setFollows } from "Login"; +import { dedupe } from "@snort/shared"; export interface FollowListBaseProps { pubkeys: HexKey[]; @@ -30,13 +33,15 @@ export default function FollowListBase({ profileActions, }: FollowListBaseProps) { const publisher = useEventPublisher(); - const { follows, relays } = useLogin(); + const login = useLogin(); async function followAll() { if (publisher) { - const ev = await publisher.contactList([...pubkeys, ...follows.item], relays.item); - await FollowsFeed.backFill(System, pubkeys); + const newFollows = dedupe([...pubkeys, ...login.follows.item]); + const ev = await publisher.contactList(newFollows, login.relays.item); System.BroadcastEvent(ev); + await FollowsFeed.backFill(System, pubkeys); + setFollows(login, newFollows, ev.created_at); } } @@ -46,9 +51,9 @@ export default function FollowListBase({
{title}
{actions} - +
)} {pubkeys?.map(a => ( diff --git a/packages/app/src/Element/LiveEvent.tsx b/packages/app/src/Element/LiveEvent.tsx index 0300feda..1dd0137c 100644 --- a/packages/app/src/Element/LiveEvent.tsx +++ b/packages/app/src/Element/LiveEvent.tsx @@ -1,24 +1,84 @@ import { NostrEvent, NostrLink } from "@snort/system"; -import { findTag } from "SnortUtils"; import { FormattedMessage } from "react-intl"; import { Link } from "react-router-dom"; +import { findTag } from "SnortUtils"; +import ProfileImage from "./ProfileImage"; +import Icon from "Icons/Icon"; + export function LiveEvent({ ev }: { ev: NostrEvent }) { const title = findTag(ev, "title"); - return ( -
-
-
-

{title}

-
-
- - + ); + } + case "ended": { + if (findTag(ev, "recording")) { + return ( + + + + ); + } + } + } + } + + return ( +
+
+ +
+

{title}

+ {statusLine()}
+
{cta()}
); } diff --git a/packages/app/src/Element/LogoutButton.tsx b/packages/app/src/Element/LogoutButton.tsx index 4a941434..cda67067 100644 --- a/packages/app/src/Element/LogoutButton.tsx +++ b/packages/app/src/Element/LogoutButton.tsx @@ -7,15 +7,15 @@ import messages from "./messages"; export default function LogoutButton() { const navigate = useNavigate(); - const publicKey = useLogin().publicKey; + const login = useLogin(); - if (!publicKey) return; + if (!login.publicKey) return; return ( @@ -266,18 +263,18 @@ export function NoteCreator() { } function changePollOption(i: number, v: string) { - if (pollOptions) { - const copy = [...pollOptions]; + if (note.pollOptions) { + const copy = [...note.pollOptions]; copy[i] = v; - dispatch(setPollOptions(copy)); + note.update(v => (v.pollOptions = copy)); } } function removePollOption(i: number) { - if (pollOptions) { - const copy = [...pollOptions]; + if (note.pollOptions) { + const copy = [...note.pollOptions]; copy.splice(i, 1); - dispatch(setPollOptions(copy)); + note.update(v => (v.pollOptions = copy)); } } @@ -292,20 +289,24 @@ export function NoteCreator() {
- dispatch( - setSelectedCustomRelays( - // set false if all relays selected - e.target.checked && selectedCustomRelays && selectedCustomRelays.length == a.length - 1 - ? false - : // otherwise return selectedCustomRelays with target relay added / removed - a.filter(el => - el === r ? e.target.checked : !selectedCustomRelays || selectedCustomRelays.includes(el), - ), - ), - ) - } + checked={!note.selectedCustomRelays || note.selectedCustomRelays.includes(r)} + onChange={e => { + note.update( + v => + (v.selectedCustomRelays = + // set false if all relays selected + e.target.checked && + note.selectedCustomRelays && + note.selectedCustomRelays.length == a.length - 1 + ? undefined + : // otherwise return selectedCustomRelays with target relay added / removed + a.filter(el => + el === r + ? e.target.checked + : !note.selectedCustomRelays || note.selectedCustomRelays.includes(el), + )), + ); + }} />
@@ -345,163 +346,167 @@ export function NoteCreator() { } }; + if (!note.show) return null; return ( - <> - {show && ( - dispatch(setShow(false))}> - {replyTo && ( - - )} - {preview && getPreviewNote()} - {!preview && ( -
-