From fba99b47133b07c648760cb241838036ae62f225 Mon Sep 17 00:00:00 2001 From: Semisol <45574030+Semisol@users.noreply.github.com> Date: Fri, 27 Jan 2023 18:50:50 +0300 Subject: [PATCH 1/7] Add more relays (high performance) For eden, @Cameri has said it has a load balanced setup. For nostr-pub.semisol, it's mostly running on idle right now with 8 threads and 16GB RAM. --- src/Const.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Const.ts b/src/Const.ts index ad23cc0..6934438 100644 --- a/src/Const.ts +++ b/src/Const.ts @@ -24,7 +24,9 @@ export const ProfileCacheExpire = (1_000 * 60 * 5); * Default bootstrap relays */ export const DefaultRelays = new Map([ - ["wss://relay.snort.social", { read: true, write: true }] + ["wss://relay.snort.social", { read: true, write: true }], + ["wss://eden.nostr.land", { read: true, write: true }], + ["wss://nostr-pub.semisol.dev", { read: true, write: true }] ]); /** @@ -107,4 +109,4 @@ export const SoundCloudRegex = /soundcloud\.com\/(?!live)([a-zA-Z0-9]+)\/([a-zA- * Mixcloud regex */ -export const MixCloudRegex = /mixcloud\.com\/(?!live)([a-zA-Z0-9]+)\/([a-zA-Z0-9-]+)/ \ No newline at end of file +export const MixCloudRegex = /mixcloud\.com\/(?!live)([a-zA-Z0-9]+)\/([a-zA-Z0-9-]+)/ From 385b4479ae3cee74ac5da1af63c29767266f7c33 Mon Sep 17 00:00:00 2001 From: Kieran Date: Mon, 23 Jan 2023 17:36:49 +0000 Subject: [PATCH 2/7] feat: nostr.build --- src/Element/NoteCreator.tsx | 21 ++++++++------------- src/Feed/FileUpload.ts | 30 ++++++++++++++++++++++++++++++ src/Feed/NostrBuildUpload.ts | 26 ++++++++++++++++++++++++++ src/Feed/VoidUpload.ts | 20 ++++++++++++++++---- src/Pages/settings/Preferences.tsx | 12 ++++++++++++ src/Pages/settings/Profile.tsx | 16 ++++++++-------- src/State/Login.ts | 10 ++++++++-- 7 files changed, 108 insertions(+), 27 deletions(-) create mode 100644 src/Feed/FileUpload.ts create mode 100644 src/Feed/NostrBuildUpload.ts diff --git a/src/Element/NoteCreator.tsx b/src/Element/NoteCreator.tsx index c9e4e3e..719645b 100644 --- a/src/Element/NoteCreator.tsx +++ b/src/Element/NoteCreator.tsx @@ -6,10 +6,9 @@ import "./NoteCreator.css"; import useEventPublisher from "Feed/EventPublisher"; import { openFile } from "Util"; -import VoidUpload from "Feed/VoidUpload"; -import { FileExtensionRegex, VoidCatHost } from "Const"; import Textarea from "Element/Textarea"; -import Event, { default as NEvent } from "Nostr/Event"; +import { default as NEvent } from "Nostr/Event"; +import useFileUpload from "Feed/FileUpload"; export interface NoteCreatorProps { replyTo?: NEvent, @@ -23,6 +22,7 @@ export function NoteCreator(props: NoteCreatorProps) { const [note, setNote] = useState(); const [error, setError] = useState(); const [active, setActive] = useState(false); + const uploader = useFileUpload(); async function sendNote() { if (note) { @@ -41,16 +41,11 @@ export function NoteCreator(props: NoteCreatorProps) { try { let file = await openFile(); if (file) { - let rx = await VoidUpload(file, file.name); - if (rx?.ok && rx?.file) { - let ext = file.name.match(FileExtensionRegex); - - // extension tricks note parser to embed the content - let url = rx.file.meta?.url ?? `${VoidCatHost}/d/${rx.file.id}${ext ? `.${ext[1]}` : ""}`; - - setNote(n => `${n}\n${url}`); - } else if (rx?.errorMessage) { - setError(rx.errorMessage); + let rx = await uploader.upload(file, file.name); + if (rx.url) { + setNote(n => `${n}\n${rx.url}`); + } else if (rx?.error) { + setError(rx.error); } } } catch (error: any) { diff --git a/src/Feed/FileUpload.ts b/src/Feed/FileUpload.ts new file mode 100644 index 0000000..1610c85 --- /dev/null +++ b/src/Feed/FileUpload.ts @@ -0,0 +1,30 @@ +import { useSelector } from "react-redux"; +import { RootState } from "State/Store"; +import NostrBuildUpload from "./NostrBuildUpload"; +import VoidUpload from "./VoidUpload"; + +export interface UploadResult { + url?: string, + error?: string +} + +export interface Uploader { + upload: (f: File | Blob, filename: string) => Promise +} + +export default function useFileUpload(): Uploader { + const fileUploader = useSelector((s: RootState) => s.login.preferences.fileUploader); + + switch (fileUploader) { + case "nostr.build": { + return { + upload: NostrBuildUpload + } as Uploader; + } + default: { + return { + upload: VoidUpload + } as Uploader; + } + } +} \ No newline at end of file diff --git a/src/Feed/NostrBuildUpload.ts b/src/Feed/NostrBuildUpload.ts new file mode 100644 index 0000000..7f8105c --- /dev/null +++ b/src/Feed/NostrBuildUpload.ts @@ -0,0 +1,26 @@ +import { UploadResult } from "./FileUpload"; + +export default async function NostrBuildUpload(file: File | Blob): Promise { + let fd = new FormData(); + fd.append("fileToUpload", file); + fd.append("submit", "Upload Image"); + + let rsp = await fetch("https://nostr.build/api/upload/", { + body: fd, + method: "POST", + headers: { + "content-type": "multipart/form-data", + "accept": "application/json" + } + }); + if(rsp.ok) { + let data = await rsp.json(); + console.debug(data); + return { + url: data.url + } + } + return { + error: "Upload failed" + } +} \ No newline at end of file diff --git a/src/Feed/VoidUpload.ts b/src/Feed/VoidUpload.ts index 53c4aeb..794d2bd 100644 --- a/src/Feed/VoidUpload.ts +++ b/src/Feed/VoidUpload.ts @@ -1,11 +1,12 @@ import * as secp from "@noble/secp256k1"; -import { VoidCatHost } from "Const"; +import { FileExtensionRegex, VoidCatHost } from "Const"; +import { UploadResult } from "./FileUpload"; /** * Upload file to void.cat * https://void.cat/swagger/index.html */ -export default async function VoidUpload(file: File | Blob, filename: string) { +export default async function VoidUpload(file: File | Blob, filename: string): Promise { const buf = await file.arrayBuffer(); const digest = await crypto.subtle.digest("SHA-256", buf); @@ -25,9 +26,20 @@ export default async function VoidUpload(file: File | Blob, filename: string) { if (req.ok) { let rsp: VoidUploadResponse = await req.json(); - return rsp; + if (rsp.ok) { + let ext = filename.match(FileExtensionRegex); + return { + url: rsp.file?.meta?.url ?? `${VoidCatHost}/d/${rsp.file?.id}${ext ? `.${ext[1]}` : ""}` + } + } else { + return { + error: rsp.errorMessage + } + } } - return null; + return { + error: "Upload failed" + }; } export type VoidUploadResponse = { diff --git a/src/Pages/settings/Preferences.tsx b/src/Pages/settings/Preferences.tsx index 3304868..ee1f50f 100644 --- a/src/Pages/settings/Preferences.tsx +++ b/src/Pages/settings/Preferences.tsx @@ -59,6 +59,18 @@ const PreferencesPage = () => { dispatch(setPreferences({ ...perf, autoShowLatest: e.target.checked }))} /> +
+
+
File upload service
+ Pick which upload service you want to upload attachments to +
+
+ +
+
Debug Menus
diff --git a/src/Pages/settings/Profile.tsx b/src/Pages/settings/Profile.tsx index 76f42ac..09ffd46 100644 --- a/src/Pages/settings/Profile.tsx +++ b/src/Pages/settings/Profile.tsx @@ -9,13 +9,12 @@ import { faShop } from "@fortawesome/free-solid-svg-icons"; import useEventPublisher from "Feed/EventPublisher"; import { useUserProfile } from "Feed/ProfileFeed"; -import VoidUpload from "Feed/VoidUpload"; import { logout } from "State/Login"; import { hexToBech32, openFile } from "Util"; import Copy from "Element/Copy"; import { RootState } from "State/Store"; import { HexKey } from "Nostr"; -import { VoidCatHost } from "Const"; +import useFileUpload from "Feed/FileUpload"; export default function ProfileSettings() { const navigate = useNavigate(); @@ -24,6 +23,7 @@ export default function ProfileSettings() { const dispatch = useDispatch(); const user = useUserProfile(id!); const publisher = useEventPublisher(); + const uploader = useFileUpload(); const [name, setName] = useState(); const [displayName, setDisplayName] = useState(); @@ -77,25 +77,25 @@ export default function ProfileSettings() { let file = await openFile(); if (file) { console.log(file); - let rsp = await VoidUpload(file, file.name); - if (!rsp?.ok) { - throw "Upload failed, please try again later"; + let rsp = await uploader.upload(file, file.name); + if (!rsp?.error) { + throw new Error("Upload failed, please try again later"); } - return rsp.file; + return rsp.url; } } async function setNewAvatar() { const rsp = await uploadFile(); if (rsp) { - setPicture(rsp.meta?.url ?? `${VoidCatHost}/d/${rsp.id}`); + setPicture(rsp); } } async function setNewBanner() { const rsp = await uploadFile(); if (rsp) { - setBanner(rsp.meta?.url ?? `${VoidCatHost}/d/${rsp.id}`); + setBanner(rsp); } } diff --git a/src/State/Login.ts b/src/State/Login.ts index f1e9683..66950a3 100644 --- a/src/State/Login.ts +++ b/src/State/Login.ts @@ -40,7 +40,12 @@ export interface UserPreferences { /** * Show debugging menus to help diagnose issues */ - showDebugMenus: boolean + showDebugMenus: boolean, + + /** + * File uploading service to upload attachments to + */ + fileUploader: "void.cat" | "nostr.build" } export interface LoginStore { @@ -117,7 +122,8 @@ const InitState = { theme: "system", confirmReposts: false, showDebugMenus: false, - autoShowLatest: false + autoShowLatest: false, + fileUploader: "void.cat" } } as LoginStore; From ab120e114f6cc01e65c1b109a11b1811ff42dfc3 Mon Sep 17 00:00:00 2001 From: Kieran Date: Sun, 29 Jan 2023 12:19:53 +0000 Subject: [PATCH 3/7] feat: profile qr --- src/Element/QrCode.tsx | 8 ++- src/Pages/ProfilePage.css | 109 ++++++++++++++++++++++---------------- src/Pages/ProfilePage.tsx | 38 +++++++++---- 3 files changed, 93 insertions(+), 62 deletions(-) diff --git a/src/Element/QrCode.tsx b/src/Element/QrCode.tsx index 09d3a07..f44f789 100644 --- a/src/Element/QrCode.tsx +++ b/src/Element/QrCode.tsx @@ -6,7 +6,8 @@ export interface QrCodeProps { link?: string, avatar?: string, height?: number, - width?: number + width?: number, + className?: string } export default function QrCode(props: QrCodeProps) { @@ -26,9 +27,6 @@ export default function QrCode(props: QrCodeProps) { }, cornersSquareOptions: { type: 'extra-rounded' - }, - imageOptions: { - crossOrigin: "anonymous" } }); qrRef.current.innerHTML = ""; @@ -46,6 +44,6 @@ export default function QrCode(props: QrCodeProps) { }, [props.data, props.link]); return ( -
+
); } \ No newline at end of file diff --git a/src/Pages/ProfilePage.css b/src/Pages/ProfilePage.css index 5479f19..6ba6bd6 100644 --- a/src/Pages/ProfilePage.css +++ b/src/Pages/ProfilePage.css @@ -1,5 +1,5 @@ .profile { - flex-direction: column; + flex-direction: column; } .profile .banner { @@ -7,18 +7,18 @@ height: 210px; margin-bottom: -80px; object-fit: cover; - mask-image: linear-gradient(to bottom, var(--bg-color) 60%, rgba(0,0,0,0)); - -webkit-mask-image: linear-gradient(to bottom, var(--bg-color) 60%, rgba(0,0,0,0)); + mask-image: linear-gradient(to bottom, var(--bg-color) 60%, rgba(0, 0, 0, 0)); + -webkit-mask-image: linear-gradient(to bottom, var(--bg-color) 60%, rgba(0, 0, 0, 0)); z-index: 0; } @media (min-width: 720px) { - .profile .banner { - width: 100%; - max-width: 720px; - height: 300px; - margin-bottom: -120px; - } + .profile .banner { + width: 100%; + max-width: 720px; + height: 300px; + margin-bottom: -120px; + } } .profile p { @@ -30,12 +30,12 @@ } @media (min-width: 720px) { - .profile .banner { - width: 100%; - max-width: 720px; - height: 300px; - margin-bottom: -120px; - } + .profile .banner { + width: 100%; + max-width: 720px; + height: 300px; + margin-bottom: -120px; + } } .profile .avatar-wrapper { @@ -89,36 +89,50 @@ margin-left: 4px; } -.profile .copy .body { font-size: 12px } +.profile .copy .body { + font-size: 12px +} @media (min-width: 360px) { - .profile .copy .body { font-size: 14px } - .profile .details-wrapper, .profile .avatar-wrapper { margin-left: 21px; } - .profile .details { width: calc(100% - 21px); } + .profile .copy .body { + font-size: 14px + } + + .profile .details-wrapper, .profile .avatar-wrapper { + margin-left: 21px; + } + + .profile .details { + width: calc(100% - 21px); + } } @media (min-width: 720px) { - .profile .details-wrapper, .profile .avatar-wrapper { margin-left: 30px; } - .profile .details { width: calc(100% - 30px); } + .profile .details-wrapper, .profile .avatar-wrapper { + margin-left: 30px; + } + + .profile .details { + width: calc(100% - 30px); + } } -.profile .follow-button { +.profile .p-buttons { position: absolute; top: -30px; right: 20px; } -.profile .message-button { - position: absolute; - top: -30px; - right: 74px; +.profile .p-buttons>div { + margin-right: 10px; } .profile .no-banner .follow-button { - right: 0px; + right: 0px; } + .profile .no-banner .message-button { - right: 54px; + right: 54px; } .tabs { @@ -128,8 +142,8 @@ margin: 10px 0; } -.tabs > div { - margin-right: 0; +.tabs>div { + margin-right: 0; } .tab { @@ -137,6 +151,7 @@ padding: 8px 0; border-bottom: 3px solid var(--gray-secondary); } + .tab.active { border-bottom: 3px solid var(--highlight); } @@ -156,24 +171,26 @@ } .profile .no-banner .avatar-wrapper, .profile .no-banner .details-wrapper { - margin: 0 auto; + margin: 0 auto; } @media (min-width: 720px) { - .profile .no-banner { - width: 100%; - flex-direction: row; - justify-content: space-around; - margin-top: 21px; - } - .profile .no-banner .avatar-wrapper { - margin: auto 10px; - } - .profile .no-banner .details-wrapper { - margin-left: 10px; - margin-top: 21px; - max-width: 420px; - } + .profile .no-banner { + width: 100%; + flex-direction: row; + justify-content: space-around; + margin-top: 21px; + } + + .profile .no-banner .avatar-wrapper { + margin: auto 10px; + } + + .profile .no-banner .details-wrapper { + margin-left: 10px; + margin-top: 21px; + max-width: 420px; + } } .profile .links { @@ -210,4 +227,4 @@ .profile .zap { margin-right: .3em; -} +} \ No newline at end of file diff --git a/src/Pages/ProfilePage.tsx b/src/Pages/ProfilePage.tsx index 6871f06..cdf8962 100644 --- a/src/Pages/ProfilePage.tsx +++ b/src/Pages/ProfilePage.tsx @@ -3,7 +3,7 @@ import "./ProfilePage.css"; import { useEffect, useMemo, useState } from "react"; import { useSelector } from "react-redux"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faGear, faEnvelope } from "@fortawesome/free-solid-svg-icons"; +import { faGear, faEnvelope, faQrcode } from "@fortawesome/free-solid-svg-icons"; import { useNavigate, useParams } from "react-router-dom"; import { useUserProfile } from "Feed/ProfileFeed"; @@ -21,6 +21,8 @@ import FollowsList from "Element/FollowsList"; import { RootState } from "State/Store"; import { HexKey } from "Nostr"; import FollowsYou from "Element/FollowsYou" +import QrCode from "Element/QrCode"; +import Modal from "Element/Modal"; enum ProfileTab { Notes = "Notes", @@ -39,6 +41,7 @@ export default function ProfilePage() { const isMe = loginPubKey === id; const [showLnQr, setShowLnQr] = useState(false); const [tab, setTab] = useState(ProfileTab.Notes); + const [showProfileQr, setShowProfileQr] = useState(false); const about = Text({ content: user?.about || '', tags: [], users: new Map() }) useEffect(() => { @@ -119,17 +122,30 @@ export default function ProfilePage() { return (
{username()} - {isMe ? ( -
navigate("/settings")}> - + +
+
setShowProfileQr(true)}> +
- ) : <> -
navigate(`/messages/${hexToBech32("npub", id)}`)}> - -
- - - } + {showProfileQr && ( setShowProfileQr(false)}> +
+ +
+
)} + {isMe ? ( +
navigate("/settings")}> + +
+ ) : <> +
navigate(`/messages/${hexToBech32("npub", id)}`)}> + +
+ + + } +
+ {bio()}
) From 40d8f911151a7070e11e8fc979df592ea6e2bdff Mon Sep 17 00:00:00 2001 From: Kieran Date: Sun, 29 Jan 2023 14:55:51 +0000 Subject: [PATCH 4/7] bug: avatar not loading --- src/Element/QrCode.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Element/QrCode.tsx b/src/Element/QrCode.tsx index f44f789..b72c397 100644 --- a/src/Element/QrCode.tsx +++ b/src/Element/QrCode.tsx @@ -27,6 +27,9 @@ export default function QrCode(props: QrCodeProps) { }, cornersSquareOptions: { type: 'extra-rounded' + }, + imageOptions: { + crossOrigin: "anonymous" } }); qrRef.current.innerHTML = ""; From bc16134f6d8f90348f12f0815fc19388b7554f53 Mon Sep 17 00:00:00 2001 From: Kieran Date: Sun, 29 Jan 2023 19:52:26 +0000 Subject: [PATCH 5/7] fix upload url --- src/Feed/NostrBuildUpload.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Feed/NostrBuildUpload.ts b/src/Feed/NostrBuildUpload.ts index 7f8105c..9c934ef 100644 --- a/src/Feed/NostrBuildUpload.ts +++ b/src/Feed/NostrBuildUpload.ts @@ -5,19 +5,17 @@ export default async function NostrBuildUpload(file: File | Blob): Promise Date: Sun, 29 Jan 2023 16:06:44 -0500 Subject: [PATCH 6/7] Show absolute time on hover --- src/Element/NoteTime.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Element/NoteTime.tsx b/src/Element/NoteTime.tsx index 5e27f90..11ce062 100644 --- a/src/Element/NoteTime.tsx +++ b/src/Element/NoteTime.tsx @@ -12,6 +12,8 @@ export interface NoteTimeProps { export default function NoteTime(props: NoteTimeProps) { const [time, setTime] = useState(); const { from, fallback } = props; + const absoluteTime = new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'long'}).format(from); + const isoDate = new Date(from).toISOString(); function calcTime() { let fromDate = new Date(from); @@ -46,5 +48,5 @@ export default function NoteTime(props: NoteTimeProps) { return () => clearInterval(t); }, [from]); - return <>{time} + return } From d8b7dbc91d6ced38ba51f2e6743a26555d638b5b Mon Sep 17 00:00:00 2001 From: Kieran Date: Sun, 29 Jan 2023 21:23:19 +0000 Subject: [PATCH 7/7] bug: remove avatar from profile QR (CORS) --- src/Pages/ProfilePage.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Pages/ProfilePage.tsx b/src/Pages/ProfilePage.tsx index cdf8962..e4091c6 100644 --- a/src/Pages/ProfilePage.tsx +++ b/src/Pages/ProfilePage.tsx @@ -129,8 +129,7 @@ export default function ProfilePage() {
{showProfileQr && ( setShowProfileQr(false)}>
- +
)} {isMe ? (